Basic Dependencies and Hooks — Pill 20
Choose buildInputs vs propagatedBuildInputs. Say what a setup hook is for.
<!-- 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:
for i in $buildInputs; do findInputs $i; donefor i in $pkgs; do addToEnv $i; donePATH="${_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:
| Need | Attribute |
|---|---|
This build needs the dep on PATH / in pkgs | buildInputs |
| Dependents of this package should inherit that PATH convenience without re-listing | propagatedBuildInputs |
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:
- **
buildInputs**: Direct Dep Path for this derivation’s PATH /pkgs. - **
propagatedBuildInputs**: Propagate Path so downstreamfindInputsrecurses via$out/nix-support/propagated-build-inputs. - Setup hook: bash file at
$pkg/nix-support/setup-hook, sourced by dependents; last-resort escape hatch. - envHooks: functions applied to every package in
addToEnv(compiler-style sibling pattern).
§VII — Common mistakes
- Listing every transitive dep in every leaf’s
buildInputswhen a middle package should propagate. - Propagating everything “just in case,” bloating every consumer’s env walk.
- Writing a setup hook for a problem PATH already solves.
- Treating setup hooks as safe defaults instead of last resort.
- Re-opening Pill 19 as if the four-step spine were today’s unmarked topic.
- Jumping to flakes or Python
withPackagesbefore this choice is fluent.
§VIII — Boundaries
- Not flakes.
- Not install (Pills 1-3).
- Not Python
withPackages/ wrap (Topics #16+). - Not Pill 19 re-teach beyond citing the spine.
- Not cross-compilation dependency taxonomy (Pill notes complexity grew; core first).
- Do not re-teach store-path naming.
§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.