Hedronite Lesson · Polyglot-Dev / Nix · Thu 2026-09-10

Automatic Runtime Dependencies — Pill 9

Build inputs ride the .drv. Runtime inputs are discovered when a store-path string appears inside the NAR of the output.

Lesson Class: Asr (Nix language track)
Focus: NAR · references · RPATH · patchelf --shrink-rpath · strip · fixup · glibc
Code Blocks: clean blocks, explanation in prose
Done-criteria: can say how Nix finds runtime refs in a NAR
Grounding: on-disk nix-pills.epub Pill 9 · live nixos.org canonical · Dolstra optional NAR cite
The .drv
nix-instantiate + nix-store -q --references lists build deps on the derivation.
The scan
Dump output NAR; search for each build-dep out path; hit means runtime dep.
The fixup
find + patchelf --shrink-rpath + strip; findutils and patchelf join baseInputs.
Build inputs ride the .drv. Runtime inputs are discovered when a store-path string appears inside the NAR of the output. That is the reference found in the NAR.

<!-- hal:authoritative:yaml -->

*Build inputs ride the .drv. Runtime inputs are discovered when a store-path string appears inside the NAR of the output. That is the reference found in the NAR.*

§I — Frame

Asr session 07. Seventh live fire of the Nix language track. Week 2 fire 4. The page is Nix Pills Automatic Runtime Dependencies, Pill 9. Luca Bruno wrote the series. License CC BY-SA 4.0. Ground from the on-disk EPUB at 09-Tomes/Polyglot-Dev/Backend-Stack/Nix/nix-pills.epub, chapter OEBPS/09-automatic-runtime-dependencies.html. The live URL is canonical if the EPUB drifts: https://nixos.org/guides/nix-pills/09-automatic-runtime-dependencies.html.

Session 06 was Pill 8. You factored mkDerivation, walked buildInputs into PATH, and merged defaults with //. That folder stays closed for re-teaching. This session asks a different question: once the build finishes, which store paths does the output still need? Nix answers that without a second hand-written dependency list. Not install. Not Pills 1 through 3. Not flakes. Not Python wrapping. Not Pills 10 or 11 (dropped from this spine). Do not open nix-shell beyond naming it as the next Pill. Dolstra 2006 sits on the same shelf as an optional second cite for why NAR exists. Blandy is a Rust tome. Empty-by-scope for Nix.

Done-criteria from the syllabus: you can say how Nix finds runtime refs in a NAR.

Launch a terminal when you have Nix. Attribute the Pill's example hashes to the Pill. Your local store hashes will differ. If Nix is not on the machine today, read the results here. Do not install Nix in this session.

§II — Build dependencies on the .drv

Start from the Pill 8 hello.nix factory. Instantiate without building:

$ nix-instantiate hello.nix
/nix/store/z77vn965a59irqnrrjvbspiyl2rph0jp-hello.drv
$ nix-store -q --references /nix/store/z77vn965a59irqnrrjvbspiyl2rph0jp-hello.drv
/nix/store/0q6pfasdma4as22kyaknk4kwx4h58480-hello-2.10.tar.gz
/nix/store/1zcs1y4n27lqs0gw4v038i303pb89rw6-coreutils-8.21.drv
/nix/store/2h4b30hlfw4fhqx10wwi71mpim4wr877-gnused-4.2.2.drv
/nix/store/39bgdjissw9gyi4y5j9wanf4dbjpbl07-gnutar-1.27.1.drv
/nix/store/7qa70nay0if4x291rsjr7h9lfl6pl7b1-builder.sh
/nix/store/g6a0shr58qvx2vi6815acgp9lnfh9yy8-gnugrep-2.14.drv
/nix/store/jdggv3q1sb15140qdx0apvyrps41m4lr-bash-4.2-p45.drv
/nix/store/pglhiyp1zdbmax4cglkpz98nspfgbnwr-gnumake-3.82.drv
/nix/store/q9l257jn9lndbi3r9ksnvf4dr8cwxzk7-gawk-4.1.0.drv
/nix/store/rgyrqxz1ilv90r01zxl0sq5nq0cq7v3v-binutils-2.23.1.drv
/nix/store/qzxhby795niy6wlagfpbja27dgsz43xk-gcc-wrapper-4.8.3.drv
/nix/store/sk590g7fv53m3zp0ycnxsc41snc2kdhp-gzip-1.6.drv

Those hashes are the Pill's examples. Treat them as the Pill's snapshot, not as your machine's truth.

The .drv lists exactly the derivations named in the derivation call. Nothing more, nothing less. Some may go unused at build time. The generic mkDerivation still pulls a fixed base tool set the way Debian's build-essential does, so those packages tend to already sit in the store for later builds.

Why query the .drv? Because hello.drv is the build action that produces the hello out path. Its references are the inputs that must exist before that action runs. Build dependencies live on the .drv. Runtime dependencies live on the realised out path. Keep those two query targets distinct.

§III — Digression: NAR

NAR means Nix ARchive. Ordinary archivers such as tar are a poor fit for content-addressed stores. They add padding, skip deterministic sort orders, stamp timestamps, and otherwise turn bit-identical trees into non-bit-identical archives. Different archive bytes mean different hashes. Different hashes mean the store cannot treat two equal trees as the same object.

NAR is a simple, deterministic archive format. Nix uses it widely whenever a store path must be serialized for hashing, transfer, or scanning. The runtime-dependency walk in §IV is one consumer. Substitution and binary caches are others. You do not need those other consumers today. Hold the one property that matters here: same tree bytes in, same NAR bytes out.

For the deeper design rationale and implementation notes, see Dolstra's 2006 PhD thesis on the same tome shelf (phd-thesis.pdf). That is the optional second cite for this session. Cite it when someone asks why Nix refused to reuse tar. Do not make the thesis the primary teach. The primary teach stays with the Pill's tools and the three-step scan.

Dump and restore from the store with:

$ nix-store --dump /nix/store/...-some-path > some.nar
$ nix-store --restore /tmp/restored < some.nar

You will not need a hand-rolled NAR file for the rest of this fire. Mentally keep the dump in view anyway. When Nix "searches the output," it is searching a NAR serialization of that output, not walking your live filesystem with ad-hoc grep rules that differ per file type.

§IV — Three-step runtime scan

Nix recognized build dependencies the moment the derivation call referred to them. Nobody wrote a second list for runtime. Nix discovers runtime dependencies automatically. The method looks fragile until you see how often store-path hashes appear as literal strings inside binaries, scripts, and data. NixOS itself rests on this scan.

The mechanism runs in three steps:

  1. Dump the derivation output as a NAR. The dump serializes a single file or a whole directory the same way.
  2. For each build-dependency .drv and its relative out path, search the NAR bytes for that out-path string.
  3. If the string appears, that out path is a runtime dependency.

A hit means the output still names that store path as a literal string somewhere in its NAR. A miss means the build used the path and then forgot it. Forgotten build tools drop out of the runtime closure. The scan does not ask whether the dependency was useful. It asks whether the dependency's out-path string still appears.

That string match is why the store-path hash matters. Hashes are long and unique enough that accidental collisions inside ordinary file contents are rare in practice. The technique looks crude. It powers NixOS.

Realise and query the out path:

$ nix-instantiate hello.nix
/nix/store/z77vn965a59irqnrrjvbspiyl2rph0jp-hello.drv
$ nix-store -r /nix/store/z77vn965a59irqnrrjvbspiyl2rph0jp-hello.drv
/nix/store/a42k52zwv6idmf50r9lps1nzwq9khvpf-hello
$ nix-store -q --references /nix/store/a42k52zwv6idmf50r9lps1nzwq9khvpf-hello
/nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19
/nix/store/8jm0wksask7cpf85miyakihyfch1y21q-gcc-4.8.3
/nix/store/a42k52zwv6idmf50r9lps1nzwq9khvpf-hello

Again, hashes are the Pill's. On this snapshot, runtime references include glibc, gcc, and hello itself. glibc is expected. gcc should not be required to run hello. Section V shows why the string is still present.

§V — Hello before fixup: glibc and gcc

Printable strings inside the binary reveal the gcc store path:

$ strings result/bin/hello | grep gcc
/nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19/lib:/nix/store/8jm0wksask7cpf85miyakihyfch1y21q-gcc-4.8.3/lib64

That line is the dynamic linker RPATH: directories searched for shared libraries at runtime. On conventional distros the RPATH is often left modest. In Nix every library lives at a precise store path, so RPATH carries exact versions.

The build added the gcc lib directory on the theory that it might help at runtime. For hello it does not. The compiler's library directory is a build-time convenience that leaked into the binary's runtime search path. Nix ships patchelf, which can shrink RPATH to the directories the binary actually needs. Even after a shrink, residual debug information can still embed the gcc path as a string. strip removes that dead weight. Together, shrink plus strip clear the false runtime hit so the NAR search no longer finds the compiler's out path.

§VI — Fixup phase

The Pill 8 builder already had six phases: environment setup, unpack, change directory, configure, build, install. Append a seventh after install: fixup. At the end of builder.sh:

find $out -type f -exec patchelf --shrink-rpath '{}' \; -exec strip '{}' \; 2>/dev/null

For each regular file under $out, shrink RPATH, then strip. Redirect stderr so non-ELF files do not spam the log. Two new commands appear: find and patchelf. Add findutils and patchelf to baseInputs in autotools.nix. That is the Pill's exercise. Keep buildInputs for package-specific extras; put these tools with the shared base set.

Rebuild:

$ nix-build hello.nix
$ nix-store -q --references result
/nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19
/nix/store/md4a3zv0ipqzsybhjb8ndjhhga1dj88x-hello

glibc remains. gcc is gone. The package is self-contained: copy its runtime closure to another machine and it still runs, provided /nix/store paths resolve. Only a small set of store components is required to run Nix itself; hello pins the exact glibc and interpreter named in the binary rather than whatever the host installed under /usr.

Confirm with ldd:

$ ldd result/bin/hello
 linux-vdso.so.1 (0x00007fff11294000)
 libc.so.6 => /nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19/lib/libc.so.6 (...)
 /nix/store/94n64qy99ja0vgbkf675nyk39g9b978n-glibc-2.19/lib/ld-linux-x86-64.so.2 (...)

The executable runs as long as those store paths exist. No system libc substitution.

§VII — Not only shared libraries

The scan is not limited to .so files. Any out-path string inside the NAR counts: executables, scripts, Python libraries, data files that embed store paths. A shebang that points at a store bash, a script that hard-codes a helper under /nix/store/..., or a config snippet that names a plugin path all create the same kind of hit. Wherever a build-dep out path survives as text or bytes in the output, Nix records a runtime reference.

That is why packages become portable closures. Apart from data and configuration, copying the runtime closure is enough to run the program on another machine that shares the /nix/store layout. The same discovery idea later supports running programs without a traditional install via nix-shell, and underpins reliable cloud deployment. Name nix-shell as the next Pill in the upstream series. Do not teach nix-shell in this folder. Do not author a shell session today. On this Asr spine, Pills 10 and 11 are dropped; session 08 jumps to Pills 12 (inputs design pattern).

§VIII — Proofs (stop and check)

**Proof 1. Build refs live on the .drv.**

nix-instantiate plus nix-store -q --references on the .drv lists the derivation inputs. If you queried the out path by mistake, walk §II again.

Proof 2. NAR is the deterministic dump.

nix-store --dump / --restore serialize store paths without tar's non-determinism. Dolstra explains the rationale; the Pill shows the tools. If NAR felt optional decoration, walk §III again.

Proof 3. Three-step scan.

Dump output NAR; search for each build-dep out path; hit means runtime dep. If "automatic" still meant magic, walk §IV again.

**Proof 4. Before fixup, gcc is a false runtime hit.**

strings shows the gcc path inside RPATH. If gcc looked like a needed library, walk §V again.

**Proof 5. Fixup leaves glibc only.**

find + patchelf --shrink-rpath + strip, with findutils and patchelf in baseInputs, drops gcc from references. ldd still resolves through the store glibc. If the closure still named the compiler, walk §VI again.

Closing

Pill 9 separates build-time references from runtime ones. You queried the .drv for build deps. You met NAR as the deterministic archive behind the scan, with Dolstra as optional second cite for why tar fails that job. You stated the three-step runtime discovery: NAR dump of the output, search for each build-dep out path, hit becomes a runtime dependency. You saw hello before fixup drag glibc and gcc into references because RPATH still named the compiler. You added a fixup phase with patchelf --shrink-rpath and strip, extended baseInputs, and watched the closure shrink to glibc. You noted the scan covers more than shared libraries.

The through-line from session 06 is intact. mkDerivation still merges defaults. Builders still write $out. What changed is how Nix reads the finished tree: a store-path string inside the NAR is enough to keep a dependency alive at runtime.

Name the mechanism when someone asks how Nix finds runtime refs: dump the output as a NAR, search for each build-dep out path, and treat every hit as a runtime dependency.

Session 08 is Pills 12: the inputs design pattern. Pills 10 and 11 stay dropped. Do not start session 08 in this folder. Do not write it today.

Examine well. .drv references are the first proof. NAR dump is the second. The three-step scan is the third. RPATH before fixup is the fourth. Shrink plus strip is the fifth. The door is a reference found in the NAR.

Related

🛡️ ⚖️ 📜 Leo.Syri — Praetor Consulate, Imperium Luminaura Filed 2026-09-10 · Asr · session 07 · Nix Pills 9 · automatic runtime deps (the reference found in the NAR)