Day 2 — Store & closures
Day 2 — Store & closures
Stage I · ~4h (theory-heavy)
Goal: Read /nix/store paths as a graph database of software, define closures precisely, and explain GC roots—with labs that only observe (no aggressive GC yet).
Why this day exists
Every advanced Nix topic—binary caches, deploy, Docker-from-Nix, NixOS activations—is a story about store paths and references. Without that theory, later tools feel like superstition.
Theory 1 — The store is the database
The directory /nix/store is not a random cache folder. It is the authoritative object store for:
- Package outputs
- Derivation files (
.drv)
- Fixed-output download results
- Sometimes source mirrors
Invariants (practical)
| Invariant | Meaning |
|---|---|
| Paths are immutable | You do not vim a store path to “fix prod” |
| Names include hashes | Collision-resistant identity |
| References are explicit | Runtime deps encoded as string refs inside outputs |
| Mutation is via new paths | “Upgrade” means new hash, not overwrite |
Hand-editing the store voids the model (and often permissions prevent it).
Theory 2 — Path anatomy
/nix/store/<hash>-<name>-<version>
│ │ │
│ │ └─ convenience label (not trust)
│ └─ cryptographic fingerprint of inputs (typical case)
└─ global namespace of this Nix installation
Examples (illustrative shapes)
/nix/store/abc…-hello-2.12.1
/nix/store/def…-glibc-2.40-36
/nix/store/ghi…-bash-5.2-p37
Theory punchline: two paths with the same pretty name and different hashes are different objects. Tools that only show names are lying by omission.
Input-addressed vs content-addressed (awareness)
| Model | Hash depends on | Note |
|---|---|---|
| Input-addressed (classic default) | Build inputs / derivation | Rebuild if inputs change |
| Content-addressed (experimental/advanced) | Output content | Same bytes → same hash potential |
You will live mostly in input-addressed land until Stage VII.
Theory 3 — References and the closure
Direct references
A store path’s references are other store paths it mentions (dynamic linker paths, wrapped scripts, metadata). Query:
nix-store -q --references "$P"Closure (requisites)
The closure of a path P is the smallest set S such that:
- P is in S
- If x is in S and x references y, then y is in S
Operationally:
nix-store -q --requisites "$P"
nix path-info -r "$P"Why closures matter — worked scenarios
| Scenario | Why closure is the unit |
|---|---|
| Copy software to another host | Need all runtime deps |
| Binary cache upload | Serve complete graphs |
| Disk usage of “hello” | Sum of requisites, not one path |
| Container image from Nix | Pack a closure |
Example narrative
hello
├─ glibc
│ ├─ …
├─ (other runtime inputs)
hello is a thin node; glibc often dominates size. That is normal.
Closure size theory
nix path-info -S "$P" # approx size of one path
nix path-info -rS "$P" # recursive / closure-oriented sizingCompare single vs recursive. The gap is the theory lesson.
Theory 4 — Evaluation vs realization (store-centric)
Recall Day 1:
| Artifact | Typical location / form |
|---|---|
| Expression | .nix / flake |
| Derivation | .drv in store (build plan) |
| Output path | realized directory in store |
Example commands
# realize outputs (modern)
nix build nixpkgs#hello --print-out-paths
# classic query tools still educational
nix-store -q --tree "$HELLO" | headYou can evaluate without realizing in some workflows; beginners usually realize first because they want a runnable binary.
Theory 5 — Profiles, generations, and roots
Why GC exists
Every experiment can add paths. Without collection, disks fill. Collection must not delete paths still needed.
Root types (non-exhaustive)
| Root kind | Example |
|---|---|
| User profile generations | ~/.nix-profile, state profiles |
| Result symlinks | ./result from nix build |
| NixOS system profiles | /nix/var/nix/profiles/system* (later) |
| Indirect roots under gcroots | Registered links |
The ./result trap (critical theory)
nix build nixpkgs#hello
# creates ./result -> /nix/store/…-hello-…That symlink is a GC root. Leave hundreds of result links in temp dirs → permanent pins → “mystery” disk use.
Discipline: rm result when finished experimenting.
Generations theory (preview)
Profiles are versioned. Rolling back a profile generation re-points the user environment. Deleting old generations makes previously live closures collectable if nothing else roots them.
Do not run nix-collect-garbage -d on a machine you care about until you understand generations. Observation only today.
Theory 6 — Substituters, signatures, trust (preview)
When realizing path P:
- Ask configured substituters (e.g.
cache.nixos.org)
- Verify signatures against trusted keys
- Else build locally
Trust model punchline: a random HTTP server offering store paths is not enough; signatures matter. Self-hosted caches (later) require key management.
Theory 7 — What “uninstall” means
| Action | Effect |
|---|---|
| Remove package from profile | Drop root edge; paths may remain |
| Delete project directory | Does not delete store |
rm result |
Drop one root |
| GC | Delete unreferenced paths |
Example misconception
"I deleted ~/code/foo, so Nix freed 2GB"
→ False unless GC ran and nothing else rooted those paths
Worked examples bank
Example A — Build and print path
HELLO=$(nix build nixpkgs#hello --print-out-paths)
echo "$HELLO"
"$HELLO/bin/hello"Example B — Direct vs full graph
nix-store -q --references "$HELLO"
nix-store -q --requisites "$HELLO" | wc -lExpect requisites count ≫ references count.
Example C — Tree view
nix-store -q --tree "$HELLO" | head -60Example D — Result root lifecycle
cd /tmp
rm -f result
nix build nixpkgs#hello
readlink result
# inspect roots (command variants exist)
nix-store --gc --print-roots 2>/dev/null | grep -i result || true
rm resultExample E — Path info metadata
nix path-info -sh "$HELLO"
nix path-info -rS "$HELLO" | tailLab 1 — Realize hello and dissect the path
cd /tmp
nix build nixpkgs#hello --print-out-paths
HELLO=$(nix build nixpkgs#hello --print-out-paths)
echo "$HELLO"
ls -la "$HELLO"
ls -la "$HELLO/bin"Journal: full path, name segment, whether ./result exists in cwd.
Lab 2 — Closure measurements
echo "direct refs: $(nix-store -q --references "$HELLO" | wc -l)"
echo "requisites: $(nix-store -q --requisites "$HELLO" | wc -l)"
nix path-info -S "$HELLO"
nix path-info -rS "$HELLO" | tail -5Write one sentence: What dominates the closure?
Lab 3 — Follow one dependency
nix-store -q --references "$HELLO"
# pick a non-hello path, ls itObserve: another complete package prefix, not a classic /usr layout.
Lab 4 — Roots observation
ls -la /nix/var/nix/gcroots 2>/dev/null | head
ls -la ~/.nix-profile 2>/dev/null
nix profile list 2>/dev/null || truePerform Example D (result create/remove). Note difference.
Lab 5 — Teach-back
Without notes, define:
- Store path
- Reference
- Closure
- GC root
- Why
rm -rf project≠ free disk
Common gotchas
| Symptom | Theory |
|---|---|
| Disk full after demos | Roots + caches + no GC |
result broken link |
GC collected after root removed |
| “Uninstalled but still huge” | Roots remain; GC not run |
| Different hash on friend machine | Different nixpkgs revision |
| Permission denied writing store | Correct — immutability |
Checkpoint
- Parse a store path into hash + name
- Define closure as a transitive reference set
- Show requisites count for
hello
- Explain
./resultas a GC root
- List ≥2 root kinds
- Agree not to blind
-dGC yet
Journal
- Closure vs single-path size surprise
- Where you will accidentally create roots
- One deploy implication of closures
Tomorrow
Day 3 — Language: values & attrsets. Theory of Nix values, let, nested sets, and inherit—enough to read nixpkgs snippets and model config-shaped data.