Using it
There are three ways in and they are the same binary. Which one you want depends on how much of the machine you are willing to let it have.
mounts /nix/store for |
needs root | survives the shell | |
|---|---|---|---|
omnibin-shell |
this shell and its children | no | no |
| NixOS module | the whole machine | yes | yes |
| container | the whole container | --cap-add SYS_ADMIN |
yes |
nix run .#vm also exists. It is the NixOS module with a login and nothing
else, for testing the module without handing it a real machine, rather than a
way anybody should run this.
omnibin-shell
$ nix run github:fzakaria/omnibin
omnibin: tree at /run/user/1000/omnibin, cache at /home/you/.cache/omnibin
$ python3@3.6.2 --version
Python 3.6.2
$ exit
This is the one for a machine you care about. It starts the daemon, then
creates a user namespace and a mount namespace and binds the lazy store over
/nix/store inside them. Your real store is served through it, so everything
the machine already has keeps working. Exit and the mount is gone; the host
never saw it.
Two details worth knowing.
The daemon runs outside the namespace on purpose. omnibin's own libraries
live in /nix/store, and a process serving /nix/store from inside the
namespace can page-fault on itself, which deadlocks with no way out. Outside,
/nix/store is still the real one and nothing can recurse. The module does
mount from inside, as root, and calls mlockall to make that safe.
The tree is not at /omnibin here. Being root in a user namespace is not
being root on the host, and / belongs to real root, so the directory cannot
be created. It goes under XDG_RUNTIME_DIR and $OMNIBIN_TREE names it.
Override with OMNIBIN_TREE=/somewhere/you/own.
Run a single command instead of a shell by passing it:
$ nix run github:fzakaria/omnibin -- python3@3.6.2 -c 'print(1)'
The NixOS module
{
imports = [ inputs.omnibin.nixosModules.default ];
services.omnibin.enable = true;
}
This mounts the lazy store for the whole system and puts /omnibin/bin last
on PATH. It is what a VM or a container wants and it is not what a laptop
wants: it puts a FUSE process in the path of every binary the machine runs,
and if that process dies the machine has no /nix/store until systemd
restarts it.
The real store is bind-mounted to /run/omnibin/real-store before the lazy
one goes over it, and every lookup checks it first. That is what keeps the
machine bootable. The kernel, systemd and omnibin itself are served from the
files that were already there, and are never fetched.
Options:
| option | default | |
|---|---|---|
services.omnibin.tree |
/omnibin |
where the browsable tree goes |
services.omnibin.mountStore |
true |
serve /nix/store lazily |
services.omnibin.cacheDir |
/var/cache/omnibin |
fetched paths and listings |
services.omnibin.addToPath |
true |
append ${tree}/bin to the system PATH |
mountStore = false serves the tree without touching /nix/store. The
symlinks in it are absolute, so they resolve only for packages the machine
already has. It is useful for reading the index, not for running anything new.
The container
fmzakari/omnibin mounts on
start. FUSE in a container needs the device and the capability and nothing
else: SYS_ADMIN is for mount, /dev/fuse is for FUSE.
$ docker run --rm -it --device /dev/fuse --cap-add SYS_ADMIN fmzakari/omnibin
$ ls /omnibin/bin | wc -l
51468
It works as a base image:
# syntax=docker/dockerfile:1
FROM fmzakari/omnibin:latest
COPY <<'SH' /demo.sh
python3@3.6.2 -c 'import sys; print(sys.version.split()[0])'
jq --version
gcc@10.2.0 --version | head -1
SH
CMD ["bash", "/demo.sh"]
$ docker build -t example .
$ docker run --rm --device /dev/fuse --cap-add SYS_ADMIN example
3.6.2
jq-1.8.1
gcc (GCC) 10.2.0
Three eras of toolchain in one image that contains none of them.
The packages are there when the container runs, not when it builds. A RUN
step in docker build has neither /dev/fuse nor the capability to mount, so
RUN jq --version fails exactly as it would on a base image without jq. Put
the work in CMD or ENTRYPOINT.
The image is built by Nix, so its own layers are a real store holding the shell, omnibin and the index. That store is served as the passthrough, which is why everything that shipped in the image keeps working without a fetch.
Naming
/omnibin/bin/<name> the newest package that provides <name>
/omnibin/bin/<name>@<version> that executable at that exact version
ls /omnibin/bin lists the 51,468 bare names only. The versioned forms
resolve on lookup and are not listed, because there are 881,933 of them.
A bare name resolves by four rules, the first that separates two candidates
winning: an attribute named after the executable beats one that is not, a
newer last-seen date beats an older, a shorter attribute name beats a longer,
and the alphabet settles the rest. So python3 comes from the python3
attribute rather than from python3Full, and a bare name resolves to the same
package for everybody.
The version is the package's, not the executable's. bibtex@2023 is the
bibtex that shipped in TeX Live 2023.
Asking the index
Do not walk the tree to find things. The database answers in milliseconds and costs no downloads.
$ omnibin which python3
/nix/store/gxzhl7aaiid7zp3y47jqqiq7zg5mqpwp-python3-3.14.6/bin/python3
$ omnibin which --all python3 | wc -l
416
$ omnibin which --all python3 | head -3
python3@3.11.7-env jupyter 0.8 MB /nix/store/zp0zk4…-python3-3.11.7-env/bin/python3
python3@3.11.9-env jupyter 0.8 MB /nix/store/hn4bkl…-python3-3.11.9-env/bin/python3
python3@3.12.4-env jupyter 0.8 MB /nix/store/xcvryj…-python3-3.12.4-env/bin/python3
416 packages have shipped something called python3, and most of them are not
CPython. A jupyter environment ships one too, which is why @version is a
version of the package rather than of the interpreter. The size is what
running that one costs the first time.
The database is at /omnibin/index.db inside a mount, $OMNIBIN_TREE/index.db
under omnibin-shell, and $OMNIBIN_DB everywhere else:
-- which packages ever shipped an `rg`
SELECT attr, version FROM bins WHERE name = 'rg' ORDER BY version;
-- every executable whose name starts with gcc
SELECT name FROM latest WHERE name LIKE 'gcc%';
-- what a package costs to run
SELECT p.name, p.nar_size FROM latest l JOIN paths p USING (digest)
WHERE l.name = 'ffmpeg';
Tables: paths(digest, name, nar_url, nar_size, has_listing),
pkgs(attr, version, digest, last_seen),
bins(name, attr, version, digest), latest(name, attr, version, digest).
For an agent
Put this in the agent's instructions, or let it read the README.md in the
mount, which says the same thing:
Every executable nixpkgs ever shipped is in
/omnibin/bin. Do not install anything and do not walk the tree. To find a command, query/omnibin/index.db. To pin a version, use<name>@<version>.