Dependency Closures and Graphs
Dependency Closures and Graphs
A store path is never alone. The boring default is: know the runtime closure (what you must copy to production) versus the build-time closure (what the sandbox needed), and measure both with nix-store / nix path-info before you ship.
Mental model
Nix records direct references by scanning output files for the 32-character hash of other store paths. If rg contains the string /nix/store/7r44p…-glibc-2.39, glibc is a runtime reference. gcc was used to compile rg but does not appear in the binary, so it is not in the runtime closure.
| Term | Command | What you get |
|---|---|---|
| Direct references | nix-store -q --references P |
Immediate edges out of P |
| Referrers | nix-store -q --referrers P |
Paths that point at P |
| Runtime closure | nix-store -q --requisites P |
P plus everything it needs to run |
| Deriver | nix-store -q --deriver P |
The .drv that built P |
build-time: gcc, headers, cmake, fetchers
│ (sandbox only)
▼
output: rg ──references──► glibc, libgcc, …
│
▼
runtime closure = rg + those references, transitively
Deploy the runtime closure. Do not deploy gcc because you compiled on this machine. result is a symlink to one path; the closure is that path plus requisites.
Worked examples
Case 1: Runtime closure of ripgrep
nix-store -q --requisites $(nix path-info nixpkgs#ripgrep)Output (typical shape on 26.05):
/nix/store/7r44p12l4b72j9m9l09k96yghz02f8n7-glibc-2.39
/nix/store/…-libgcc-…
/nix/store/z1k947f6y788sfaivs931h26hsvb3506-ripgrep-14.1.1
A handful of paths. Not a compiler toolchain.
Case 2: Sizes, not just names
nix path-info -Shr nixpkgs#ripgrep| Flag | Meaning |
|---|---|
-S |
Size of this path |
-h |
Human units |
-r |
Closure (requisites) |
The last line is the closure you would copy with nix copy. If that number is hundreds of megabytes for a “small” CLI, a wrapper script has pulled in Python or glib.
/nix/store/…-ripgrep-… 5.2M
…
narSize (closure) 32M
Numbers move with the 26.05 pin; the shape does not.
Case 3: Direct references versus the whole tree
echo 'references:'
nix-store -q --references $(nix path-info nixpkgs#ripgrep)
echo 'requisites:'
nix-store -q --requisites $(nix path-info nixpkgs#ripgrep)References are one hop. Requisites are the transitive closure. For rg they are often almost the same. For a desktop environment they are not — nix path-info -Shr /run/current-system is the scary number.
Case 4: Why gcc is missing from runtime
out=$(nix path-info nixpkgs#hello)
drv=$(nix-store -q --deriver "$out")
echo "output: $out"
echo "deriver: $drv"
nix-store -q --references "$drv" | grep -E 'gcc|stdenv' | head
nix-store -q --references "$out"The .drv references stdenv (gcc, binutils, …). The output of hello does not. Reference scanning dropped the toolchain. That is the entire point of separate build-time and runtime graphs.
Case 5: Copy what production needs
nix copy --to file://$PWD/desk-nar nixpkgs#ripgrep
ls desk-nar/nar | wc -l
nix path-info --store file://$PWD/desk-nar -r nixpkgs#ripgrep | grep gcc || echo 'no gcc in the NAR'The NAR cache contains the runtime closure, not gcc. A colleague can nix copy --from file://$PWD/desk-nar and run rg without compiling.
On a live cache this is nix copy --to ssh://desk-cache or a Cachix push. Same graph.
If the runtime closure of desk-api contains gcc or python3 you did not mean to ship:
nix why-depends $(nix path-info nixpkgs#ripgrep) nixpkgs#gcc || echo 'gcc not in runtime (good)'Cut the edge (wrapper, propagatedBuildInputs, shebang) rather than deleting gcc from the store by hand.
nix path-info -Sh nixpkgs#hello
nix path-info -Shr nixpkgs#helloThe first number is one path. The second is the closure you would nix copy. Write both down before you call a package “small.”
Interactive tree (optional extra):
nix run nixpkgs#nix-tree -- $(nix path-info nixpkgs#ripgrep)Arrow keys walk references. nix-tree is a viewer, not a substitute for path-info -Shr in scripts.
The trap
The trap is shipping result by scp — one directory — and discovering the binary dies on the server because glibc is not there. result is a symlink to one path. The closure is that path plus requisites.
The other trap is reading --requisites on a .drv and panicking at the size. A .drv’s requisites include build-time inputs. Query the output path (nix path-info nixpkgs#ripgrep), not the deriver, when you mean “what runs.”
The boring rule
- Runtime closure =
--requisitesof the output path. - Measure with
nix path-info -Shrbefore you call a package “small.” - Deploy with
nix copy(or a binary cache), never with a singlescpofbin/. - Build-time tools belong in the
.drv, not in production. - If a runtime closure contains a compiler, a wrapper or
propagatedBuildInputsleaked. Fix the package.
Try this
- Compare
nix path-info -Sh nixpkgs#hello(one path) withnix path-info -Shr nixpkgs#hello(closure). Write down both numbers. - Run
nix-store -q --graph $(nix path-info nixpkgs#hello)and pipe tohead. You should seehelloandglibcnodes. nix-store -q --referrers $(nix path-info nixpkgs#glibc | head -1)— many packages point at glibc. That is why it survives GC.- Intentionally
scponly thehellostore directory to a tmp dir and try to run it. Then explain the interpreter error withreadelf -l(the RPATH chapter).