Hedronite Lesson · Polyglot-Dev / Nix · Mon 2026-09-07

Generic Builders — Pill 8

A one-off builder proves the contract. A shared builder plus mkDerivation proves the pattern: defaults merge with //, and buildInputs become PATH.

Lesson Class: Asr (Nix language track)
Focus: mkDerivation · buildInputs · baseInputs · // · autotools · hello
Code Blocks: clean blocks, explanation in prose
Done-criteria: factor mkDerivation + buildInputs; merge default attrs with //
Grounding: on-disk nix-pills.epub Pill 8 · live nixos.org canonical · Dolstra/Blandy not cited
The builder
Generic builder.sh walks buildInputs into PATH. No package name baked in.
The merge
defaultAttrs // attrs. Right-hand wins. Packages only declare differences.
The factory
import ./autotools.nix pkgs yields mkDerivation. Hello shrinks to name + src.
A one-off builder proves the contract. A shared builder plus mkDerivation proves the pattern: defaults merge with //, and buildInputs become PATH.

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

*A one-off builder proves the contract. A shared builder plus mkDerivation proves the pattern: defaults merge with //, and buildInputs become PATH.*

§I — Frame

Asr session 06. Sixth live fire of the Nix language track. Week 2 fire 3. The page is Nix Pills Generic Builders, Pill 8. 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/08-generic-builders.html. The live URL is canonical if the EPUB drifts: https://nixos.org/guides/nix-pills/08-generic-builders.html.

Session 05 was Pill 7. You ran bash with a store-copied builder.sh, wrote $out, and hung gcc, coreutils, and src on the derivation so they became environment variables. That folder stays closed for re-teaching. This session is the next move: package GNU hello with autotools, then factor a generic builder and a thin mkDerivation that merges common attrs with //. Not install. Not Pills 1 through 3. Not automatic runtime dependencies. Pill 9 waits for session 07. Dolstra 2006 stays on the shelf. Blandy is a Rust tome. Empty-by-scope for Nix. No flakes. No Python wrapping.

Done-criteria from the syllabus: you can factor mkDerivation + buildInputs and merge default attrs with //.

Launch nix repl when you have Nix. Type the small builtins.toString checks below. Exit with :q. Store hashes may differ on your machine. Attribute the Pill's example hashes to the Pill. If Nix is not on the machine today, read the results here. Do not install Nix in this session.

§II — Package GNU hello first

Pill 7 packaged a single .c file with a raw gcc call. Real projects often use autotools. GNU hello is a small complete autotools project. Fetch the tarball named in the Pill (hello-2.12.1.tar.gz from ftp.gnu.org) into the working directory beside your Nix files. The Pill's examples assume the tarball sits next to hello.nix as a Nix path.

Write a dedicated hello_builder.sh:

export PATH="$gnutar/bin:$gcc/bin:$gnumake/bin:$coreutils/bin:$gawk/bin:$gzip/bin:$gnugrep/bin:$gnused/bin:$bintools/bin"
tar -xzf $src
cd hello-2.12.1
./configure --prefix=$out
make
make install

Then hello.nix:

let
  pkgs = import <nixpkgs> { };
in
derivation {
  name = "hello";
  builder = "${pkgs.bash}/bin/bash";
  args = [ ./hello_builder.sh ];
  inherit (pkgs)
    gnutar
    gzip
    gnumake
    gcc
    coreutils
    gawk
    gnused
    gnugrep
    ;
  bintools = pkgs.binutils.bintools;
  src = ./hello-2.12.1.tar.gz;
  system = builtins.currentSystem;
}

Build with nix-build hello.nix. Run result/bin/hello. The --prefix=$out line is the same contract from Pill 7: install into the reserved store path, not into a shared /usr.

On darwin, the Pill swaps gcc for clang and adjusts bintools. Note that early example difference. Later pills show how Nix hides the platform split. For this fire, treat the darwin variant as an awareness note, not a second spine.

§III — One builder for many autotools packages

A dedicated hello_builder.sh works once. It still hardcodes the hello directory name and repeats the tool list. Write a generic builder.sh instead:

set -e
unset PATH
for p in $buildInputs; do
    export PATH=$p/bin${PATH:+:}$PATH
done

tar -xf $src

for d in *; do
    if [ -d "$d" ]; then
        cd "$d"
        break
    fi
done

./configure --prefix=$out
make
make install

What changed:

  1. set -e stops the build on the first failing command.
  2. unset PATH clears the fake /path-not-set value from the builder environment.
  3. Each entry in $buildInputs contributes $p/bin to PATH.
  4. The script extracts $src, finds the first directory, and enters it. No package name is baked into the script.
  5. Configure, make, and install still target $out.

Rewrite hello.nix to pass tools as a list:

let
  pkgs = import <nixpkgs> { };
in
derivation {
  name = "hello";
  builder = "${pkgs.bash}/bin/bash";
  args = [ ./builder.sh ];
  buildInputs = with pkgs; [
    gnutar
    gzip
    gnumake
    gcc
    coreutils
    gawk
    gnused
    gnugrep
    binutils.bintools
  ];
  src = ./hello-2.12.1.tar.gz;
  system = builtins.currentSystem;
}

The builder no longer names hello. The derivation still names the package and the source. That split is the first half of the Pill's pattern.

§IV — Why a list becomes PATH

buildInputs looks like black magic until you remember two facts from earlier sessions.

First, a derivation stringifies to its out path. Second, Nix converts a list to a string by stringifying each element and joining with spaces:

builtins.toString 123
# "123"
builtins.toString [ 123 456 ]
# "123 456"

After :l <nixpkgs>:

builtins.toString gnugrep
# "/nix/store/...-gnugrep-..."
builtins.toString [ gnugrep gnused ]
# "/nix/store/...-gnugrep-... /nix/store/...-gnused-..."

So $buildInputs in bash is a space-separated string of store paths. The for p in $buildInputs loop walks those paths and prepends each bin directory to PATH. No special builtin is required beyond ordinary stringification.

§V — mkDerivation and //

The generic builder still leaves a long tool list in every package expression. Factor the common attrs into a function. Create autotools.nix:

pkgs: attrs:
let
  defaultAttrs = {
    builder = "${pkgs.bash}/bin/bash";
    args = [ ./builder.sh ];
    baseInputs = with pkgs; [
      gnutar
      gzip
      gnumake
      gcc
      coreutils
      gawk
      gnused
      gnugrep
      binutils.bintools
    ];
    buildInputs = [ ];
    system = builtins.currentSystem;
  };
in
derivation (defaultAttrs // attrs)

The file evaluates to a function of pkgs that returns a function of attrs. Inside, defaultAttrs holds the shared builder, the shared tool set as baseInputs, an empty caller buildInputs, and system. The expression defaultAttrs // attrs is set union. Right-hand values win on key collision:

{ a = "b"; } // { c = "d"; }
# { a = "b"; c = "d"; }
{ a = "b"; } // { a = "c"; }
# { a = "c"; }

Complete the builder exercise from the Pill: loop $baseInputs together with $buildInputs when assembling PATH. Keep the two lists separate so the caller still sees buildInputs as the package-specific extras. That separation is a design choice, not a language requirement.

§VI — Hello collapses to name and src

Rewrite hello.nix:

let
  pkgs = import <nixpkgs> { };
  mkDerivation = import ./autotools.nix pkgs;
in
mkDerivation {
  name = "hello";
  src = ./hello-2.12.1.tar.gz;
}

Read the partial application carefully. import ./autotools.nix yields pkgs: attrs: …. Applying pkgs yields attrs: derivation (defaultAttrs // attrs). The package file only supplies what differs: name and src. Extra tools, when needed later, go in buildInputs.

Two remarks from the Pill, kept short:

  1. Assigning pkgs = import <nixpkgs> { }; is the same import you used inside earlier with expressions. Naming it first makes the partial application readable.
  2. Special C flags and linker flags for other libraries are not in this fire. The Pill notes that compile-time and link-time search paths wait for later design. Stay with PATH and autotools for today.

§VII — Composition picture

Nix grows by creating derivations and composing them. Store paths name prior results the way pointers name heap objects in C. Pill 8 is the first clear composition step in this track: one shared builder, one mkDerivation wrapper, many package files that only declare differences.

Keep the analogy as a picture, not as a claim that Nix is C. The operational fact is enough: defaults live in one place; packages merge overrides with //; tools reach the builder through buildInputs and baseInputs.

§VIII — Proofs (stop and check)

Proof 1. Hello builds with a dedicated script.

hello_builder.sh plus the long inherit list produces result/bin/hello. If that failed, fix PATH wiring and --prefix=$out before factoring.

Proof 2. The generic builder has no package name.

builder.sh finds the first directory after extract. If you still hardcoded hello-2.12.1, walk §III again.

Proof 3. Lists stringify for bash.

builtins.toString [ gnugrep gnused ] is two store paths separated by a space. If $buildInputs felt like a mysterious Nix type, walk §IV again.

**Proof 4. // prefers the right-hand set.**

defaultAttrs // attrs keeps shared defaults and lets the package override name, src, and buildInputs. If merge order was unclear, walk §V again.

**Proof 5. mkDerivation is partial application.**

import ./autotools.nix pkgs returns the attrs function. Hello only passes name and src. If the package file still repeated the tool list, walk §VI again.

Closing

Pill 8 turns the working derivation from Pill 7 into a reusable factory. You packaged GNU hello with autotools and --prefix=$out. You wrote a builder that walks $buildInputs into PATH and never names the package. You saw that lists of derivations stringify to space-separated store paths. You wrote autotools.nix so mkDerivation merges defaultAttrs with caller attrs using //. You left hello as name plus src.

The through-line from session 05 is intact. Bash still creates $out. Paths in args still stage into the store. What changed is who owns the common pieces: the wrapper owns the builder and the base tools; the package owns the differences.

Name the mechanism when someone asks how generic builders differ from Pill 7's one-off script: mkDerivation merges defaults with //, and buildInputs become PATH.

Session 07 is Pills 9: automatic runtime dependencies. Do not start it in this folder. Do not write it today.

Examine well. Generic builder.sh is the first proof. List stringification is the second. // merge is the third. Partial application of autotools.nix is the fourth. A two-field package expression is the fifth. The door is a factory that still writes $out.

Related

🛡️ ⚖️ 📜 Leo.Syri — Praetor Consulate, Imperium Luminaura Filed 2026-09-07 · Asr · session 06 · Nix Pills 8 · generic builders (mkDerivation, buildInputs, //)