Hedronite Lesson · Polyglot-Dev / Nix · Fri 2026-09-25

Basic Dependencies and Hooks — Pill 20

Choose buildInputs vs propagatedBuildInputs. Say what a setup hook is for.

Lesson Class: Asr (Nix language track)
Focus: buildInputs · propagatedBuildInputs · findInputs · addToEnv · setup-hook · envHooks
Done-criteria: choose buildInputs vs propagatedBuildInputs; say what a setup hook is for
Grounding: on-disk nix-pills.epub Pill 20 · live nixos.org canonical
Note: Not flakes · not install · not Python withPackages · not Pill 19 re-teach · next session 16 withPackages
Direct Dep Path
buildInputs → findInputs → addToEnv → PATH for this derivation.
Propagate Path
propagatedBuildInputs writes nix-support file; findInputs recurses downstream.
Setup Hook Escape
$pkg/nix-support/setup-hook sourced by dependents; last-resort bash callback.
For the current package, propagated and not are the same walk; downstream is where propagation matters.

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

*Choose buildInputs or propagatedBuildInputs for a dep. Say what a setup hook is for, and treat it as last resort.*

§I — Frame

Asr session 15. Fifteenth live fire of the Nix language track. Week 5 fire 2. The page is Nix Pills Basic Dependencies and Hooks, Pill 20. Luca Bruno wrote the series. License CC BY-SA 4.0. Ground from the on-disk EPUB at . Live URL is canonical if the EPUB drifts: https://nixos.org/guides/nix-pills/20-basic-dependencies-and-hooks.html.

Session 14 walked the four-step spine: stdenv.mkDerivation → default-builder.sh → $stdenv/setup → genericBuild. This session asks how packages interact on that spine: direct deps, transitive PATH convenience, and bash callbacks that let a dependency change the dependent build. Dualfire seat for this lane: Nix paves Rust roads; buildInputs and hooks are how Rust toolchains and dep graphs land on PATH / env inside stdenv builds.

Done-criteria from the syllabus: you can choose buildInputs vs propagatedBuildInputs and say what a setup hook is for.

Not flakes. Not Python withPackages (next unmarked). Not Pill 19 re-teach. Not install. Launch a terminal when you have Nix. If Nix is absent today, read the Pill’s wrappedHello drills and the findInputs / addToEnv sketches from this lesson.

§II — Direct Dep Path (named technique)

Musashi declarative: a package that needs another package’s binaries or headers for this build lists it in buildInputs.

Direct Dep Path: put the dependency in buildInputs. Setup walks those attrs, collects them into pkgs, then calls addToEnv so each «pkg»/bin (when present) joins _PATH, which later joins PATH.

Pill shape (GNU Hello, then a wrapper that execs it):

wrappedHello = stdenv.mkDerivation {
  name = "hello-wrapper";
  buildInputs = [ actualHello which ];
  unpackPhase = "true";
  installPhase = ''
    mkdir -p "$out/bin"
    echo "#! ${stdenv.shell}" >> "$out/bin/hello"
    echo "exec $(which hello)" >> "$out/bin/hello"
    chmod 0755 "$out/bin/hello"
  '';
};

The wrapper finds hello on PATH because setup did roughly:

  1. for i in $buildInputs; do findInputs $i; done
  2. for i in $pkgs; do addToEnv $i; done
  3. PATH="${_PATH-}${_PATH:+${PATH:+:}}$PATH"

findInputs records each store path once. addToEnv adds $1/bin to _PATH when that directory exists. That is the whole Direct Dep Path for today’s done-criteria.

If-then-thus: if the current derivation needs hello on PATH during its own phases, then list it in buildInputs, thus you do not invent a host PATH or a hand-written export.

§III — Propagate Path (named technique)

Propagate Path: use propagatedBuildInputs when downstream dependents should get the same PATH / pkgs convenience without re-listing the transitive dep.

Pill intermediary dumps its propagated list at fixup:

# inside fixupPhase (simplified)
if test -n "$propagatedBuildInputs"; then
  mkdir -p "$out/nix-support"
  echo "$propagatedBuildInputs" > "$out/nix-support/propagated-build-inputs"
fi

Then findInputs recurses:

if test -f $pkg/nix-support/propagated-build-inputs; then
  for i in $(cat $pkg/nix-support/propagated-build-inputs); do
    findInputs $i
  done
fi

The call site loops both lists:

for i in $buildInputs $propagatedBuildInputs; do
  findInputs $i
done

Important Pill fact: for the current package alone, propagated vs not is processed the same (findInputs then addToEnv). Downstream is where it differs: only propagated immediate deps land in $out/nix-support/propagated-build-inputs.

Choose guide:

NeedAttribute
This build needs the dep on PATH / in pkgsbuildInputs
Dependents of this package should inherit that PATH convenience without re-listingpropagatedBuildInputs

If-then-thus: if only your installPhase needs hello, use buildInputs; if a middle package should pull hello into every consumer’s env walk, use propagatedBuildInputs on the middle package.

§IV — Setup Hook Escape (named technique)

Setup Hook Escape: when a dependency must change the dependent build in a way PATH propagation cannot express, ship $out/nix-support/setup-hook and let dependents source it.

Last piece of findInputs from the Pill:

if test -f $pkg/nix-support/setup-hook; then
  source $pkg/nix-support/setup-hook
fi

A setup hook is a bash callback. It is strictly more general than propagatedBuildInputs (you can almost re-implement propagation as a hook). Pill language: treat it as an escape hatch around the usual “deps are immutable and inert” story. You are not mutating other store paths; you are allowing arbitrary ad-hoc bash when this package is on the dependency walk. Use only as last resort.

Env hooks are the sibling convenience: functions appended to envHooks run from addToEnv against every package in the walk. Compilers use that pattern for -I / -L style flags without stuffing C-only logic into generic setup. Name the pattern; do not rewrite CC Wrapper today.

If-then-thus: if PATH / propagated files are enough, skip hooks; if the dep must inject flags, env vars, or compiler wiring dependents cannot predict, then write a setup hook (and prefer envHooks helpers when the pattern fits), thus keep hooks rare.

§V — Dualfire: paving Rust roads

Rust toolchains and crate graphs in nixpkgs still ride this same walk. A Rust package that needs rustc, cargo, or a C library on PATH during build lists them (or a wrapper that propagates them) through buildInputs / propagatedBuildInputs. Setup hooks and env hooks are how wrappers inject flags and search paths without every leaf package inventing exports. Remember the Pill spine: hooks run during the dependent’s setup walk, not as a second Nix language.

§VI — Done-criteria check

Say aloud without looking:

  1. **buildInputs**: Direct Dep Path for this derivation’s PATH / pkgs.
  2. **propagatedBuildInputs**: Propagate Path so downstream findInputs recurses via $out/nix-support/propagated-build-inputs.
  3. Setup hook: bash file at $pkg/nix-support/setup-hook, sourced by dependents; last-resort escape hatch.
  4. envHooks: functions applied to every package in addToEnv (compiler-style sibling pattern).

§VII — Common mistakes

  1. Listing every transitive dep in every leaf’s buildInputs when a middle package should propagate.
  2. Propagating everything “just in case,” bloating every consumer’s env walk.
  3. Writing a setup hook for a problem PATH already solves.
  4. Treating setup hooks as safe defaults instead of last resort.
  5. Re-opening Pill 19 as if the four-step spine were today’s unmarked topic.
  6. Jumping to flakes or Python withPackages before this choice is fluent.

§VIII — Boundaries

§IX — Close

Session 14 named how builds start. Session 15 names how deps join PATH and how hooks inject behavior: Direct Dep Path, Propagate Path, Setup Hook Escape. Next unmarked: session 16, Python withPackages.

Related:

Write one sentence: when you pick buildInputs, when you pick propagatedBuildInputs, and what a setup hook is for. Leave the keyboard.