Inspecting Closures: why-depends, path-info, nix-tree
Inspecting Closures: why-depends, path-info, nix-tree
Shipping a “small” service that secretly contains gcc is a weekly desk accident. The boring default is: measure the closure before you copy it, then ask why-depends when a path you did not expect shows up.
Mental model
Three questions, three tools:
| Question | Tool |
|---|---|
| How big is this, including everything it needs? | nix path-info -Shr |
| Why is this path inside that closure? | nix why-depends A B |
| Let me walk the tree with a keyboard | nix-tree |
nix-store -q --tree is the classic ASCII dump. nix-tree is the same graph with navigation. nom (nix-output-monitor) wraps nix build so you watch fetch/build in a table instead of a scrollback.
You already used --requisites in the closures chapter. This chapter is the daily loop: size, blame, browse.
Worked examples
Case 1: Closure size in one line
nix path-info -Shr nixpkgs#helloOutput (shape):
/nix/store/…-glibc-2.39 31.0M 31.0M
/nix/store/…-hello-2.12.1 226K 31.2M
The last column on the last line is the number you put in a review: hello’s runtime closure is ~31 MB, almost all glibc.
Compare a fat wrapper:
nix path-info -Sh nixpkgs#ripgrep
nix path-info -Shr nixpkgs#ripgrep | tail -1If the closure is dozens of times the single-path size, something was propagated that should not have been.
Case 2: why-depends
“Why does the desk API closure contain python3?”
nix why-depends nixpkgs#ripgrep nixpkgs#glibcOutput (shape):
/nix/store/…-ripgrep-14.1.0
└───/nix/store/…-glibc-2.39
A longer chain (hello → wrapper → python) prints as a path of edges. That chain is the fix list: cut the edge you did not intend (a propagatedBuildInputs, a wrapProgram that pulled in a huge interpreter, a share/ file that embeds a store path).
If there is no dependence:
nix why-depends nixpkgs#hello nixpkgs#ripgrepOutput:
'hello' does not depend on 'ripgrep'
Case 3: ASCII tree without extra tools
nix-store -q --tree $(nix path-info nixpkgs#hello)Output (shape):
/nix/store/…-hello-2.12.1
└───/nix/store/…-glibc-2.39
└───…
Useful on a server where you will not install nix-tree. Pipe to less.
Case 4: nix-tree for interactive blame
nix run nixpkgs#nix-tree -- $(nix path-info nixpkgs#hello)or, if nix-tree is in your system packages:
nix-tree $(nix path-info nixpkgs#hello)Keys (typical):
| Key | Action |
|---|---|
j / k |
Move |
| enter | Open a path’s references |
s |
Sort by size |
q |
Quit |
Sort by size first. The top of the list is where the megabytes are.
Case 5: Watch a build with nom
nix run nixpkgs#nix-output-monitor -- build nixpkgs#hellonom is a frontend. It does not change hashes. Use it when a long nixos-rebuild is a wall of identical “copying path” lines and you want to know whether you are substituting or compiling.
On NixOS you can alias:
# configuration.nix fragment
environment.systemPackages = [ pkgs.nix-output-monitor ];Then nom build .#desk-api.
The trap
The trap is reading du -sh result. result is a symlink to one path. du does not follow the glibc reference. You report 200 KB, production downloads 31 MB, and someone “optimises” the wrong thing.
Always nix path-info -Shr.
The second trap is why-depends in the wrong direction. nix why-depends A B means “why does A depend on B” — A is the parent closure, B is the unexpected guest.
The boring rule
- Size with
nix path-info -Shron the output path. - Blame with
nix why-depends parent child. - Browse with
nix-tree(or--treeon a server). duonresultis not a closure size.nomis optional UX. It is not part of the hash.
Try this
- Record
nix path-info -Shr nixpkgs#helloandnix path-info -Shr nixpkgs#git. Git is larger.why-dependsgit against perl or python and see which interpreter, if any, is in the runtime closure. nix-store -q --tree $(nix path-info nixpkgs#hello) | wc -l— that is the node count of a tiny closure.- Install
nix-treein a devShell, open hello, sort by size, and name the largest path. - Run a
nix buildtwice, once plain and once undernom. Confirm the resulting store path is identical (nix path-info).