Day 2 — Store & closures

Updated

July 30, 2026

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

Diagram of /nix/store hash and name components

Store 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

hello package referencing libraries forming a closure

Closure as a reference graph

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:

  1. P is in S
  2. 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 sizing

Compare single vs recursive. The gap is the theory lesson.


Theory 4 — Evaluation vs realization (store-centric)

Recall Day 1:

Evaluation versus realization reminder diagram

Evaluate then realize
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" | head

You can evaluate without realizing in some workflows; beginners usually realize first because they want a runnable binary.


Theory 5 — Profiles, generations, and roots

User profiles result links and system generations as GC roots

GC roots keep paths live

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.

Important

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:

  1. Ask configured substituters (e.g. cache.nixos.org)
  2. Verify signatures against trusted keys
  3. 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 -l

Expect requisites count ≫ references count.

Example C — Tree view

nix-store -q --tree "$HELLO" | head -60

Example 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 result

Example E — Path info metadata

nix path-info -sh "$HELLO"
nix path-info -rS "$HELLO" | tail

Lab 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 -5

Write one sentence: What dominates the closure?


Lab 3 — Follow one dependency

nix-store -q --references "$HELLO"
# pick a non-hello path, ls it

Observe: 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 || true

Perform Example D (result create/remove). Note difference.


Lab 5 — Teach-back

Without notes, define:

  1. Store path
  2. Reference
  3. Closure
  4. GC root
  5. 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 ./result as a GC root
  • List ≥2 root kinds
  • Agree not to blind -d GC yet

Journal

  1. Closure vs single-path size surprise
  2. Where you will accidentally create roots
  3. 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.