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

Inputs Design Pattern — Pill 12

A package expression is a function of its inputs. A tiny repo is an attribute set that calls those functions with the inputs you choose.

Lesson Class: Asr (Nix language track)
Focus: inputs pattern · default.nix · inherit · graphviz · gdSupport · repo attrset
Code Blocks: clean blocks, explanation in prose
Done-criteria: write a package as { input1, input2 }: … and compose a tiny repo set
Grounding: on-disk nix-pills.epub Pill 12 · live nixos.org canonical
The repo
One lazy attrset maps names to packages; nix-build -A selects without file paths.
The function
Package files take { mkDerivation, gd, … } and stop importing themselves.
The call site
default.nix imports nixpkgs once, inherits inputs, and builds variants like graphvizCore.
A package expression is a function of its inputs. A tiny repo is an attribute set that calls those functions with the inputs you choose.

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

A package expression is a function of its inputs. A tiny repo is an attribute set that calls those functions with the inputs you choose.

§I — Frame

Asr session 08. Eighth live fire of the Nix language track. Week 3 fire 1. The page is Nix Pills Package Repositories and the Inputs Design Pattern, Pill 12. Luca Bruno wrote the series. License CC BY-SA 4.0. Ground from the on-disk EPUB at , chapter OEBPS/12-inputs-design-pattern.html. The live URL is canonical if the EPUB drifts: https://nixos.org/guides/nix-pills/12-inputs-design-pattern.html.

Session 07 was Pill 9. You learned that runtime deps are discovered when a store-path string appears inside the NAR of the output. That folder stays closed for re-teaching. Pills 10 and 11 (nix-shell as ops, garbage collector) stay dropped on this spine. This session resumes packaging composition: how several package expressions become one repository, and how each package stops importing <nixpkgs> itself.

Done-criteria from the syllabus: you can write a package as { input1, input2 }: … and compose a tiny repo set.

Launch a terminal when you have Nix. Attribute the Pill's example hashes and package versions 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. Do not open callPackage beyond naming it as the next Pill.

§II — Single repository, lazy set

Nix itself does not prescribe a packaging policy. Repositories arose because people needed to organize packages. Over time nixpkgs settled on a useful habit: put all package descriptions in one repository, then expose them as one top-level attribute set.

That habit is the single-repository pattern. Debian scatters packages across many small repos. Gentoo keeps descriptions together. Nixpkgs follows the single-repo side. The natural Nix shape is a top-level expression that imports each package expression and maps names to packages.

Laziness makes that affordable. In a strict language, stuffing every package description into one data structure would force the whole tree into memory before you could touch one package. Nix only evaluates what you ask for. Selecting hello does not force graphviz to build.

Hold that shape in mind: one attrset, many packages, evaluate on demand. The rest of the Pill teaches how each package becomes independent of that attrset.

Why independence matters: a package that hard-codes import <nixpkgs> { } can only see whatever that path resolves to on the caller's NIX_PATH. A package that takes mkDerivation and gd as arguments can be reused from another repository, from a test harness that injects a stub toolchain, or from a variant that pins a different gd. The repository file becomes the composition root. The package file becomes a pure description of how inputs become an output.

§III — Second package: graphviz and pkg-config

You already have GNU hello from earlier Pills. Pill 12 adds graphviz so the repository has more than one member. The first cut reuses autotools.nix the same way hello.nix did:

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

nix-build graphviz.nix yields runnable binaries under result/bin. By default that build cannot emit PNG. The configure script needs libgd (and friends) on the build inputs, and it finds libraries through pkg-config.

Classic POSIX systems keep .pc files under /usr/lib/pkgconfig. Isolated Nix builds do not. The Pill extends setup.sh so each entry in baseInputs and buildInputs that carries lib/pkgconfig is prepended to PKG_CONFIG_PATH, the same way bin directories already feed PATH:

for p in $baseInputs $buildInputs; do
    if [ -d $p/bin ]; then
        export PATH="$p/bin${PATH:+:}$PATH"
    fi
    if [ -d $p/lib/pkgconfig ]; then
        export PKG_CONFIG_PATH="$p/lib/pkgconfig${PKG_CONFIG_PATH:+:}$PKG_CONFIG_PATH"
    fi
done

With that hook in place, the graphviz expression can ask for pkg-config and both halves of a split gd:

let
  pkgs = import <nixpkgs> { };
  mkDerivation = import ./autotools.nix pkgs;
in
mkDerivation {
  name = "graphviz";
  src = ./graphviz-2.49.3.tar.gz;
  buildInputs = with pkgs; [
    pkg-config
    (pkgs.lib.getLib gd)
    (pkgs.lib.getDev gd)
  ];
}

After the build, dot -Tpng works. The with pkgs; form only avoids repeating pkgs. on the list. It is not yet the inputs pattern. Both hello.nix and graphviz.nix still import <nixpkgs> themselves. That is the problem §V fixes.

§IV — Tiny repo via default.nix

Two packages are enough to compose a repository. Mimic nixpkgs: one attribute set of derivations. Create default.nix:

{
  hello = import ./hello.nix;
  graphviz = import ./graphviz.nix;
}

Load it in nix repl with :l default.nix. Access members by name. Build one member with nix-build default.nix -A hello. A directory that contains default.nix uses that file as the implicit expression, so nix-build -A hello from the directory is enough. The Pill also shows nix-env -f . -iA graphviz for a user-environment install. Treat that as the Pill's illustration of selecting an attribute from the set. Do not make user-environment management the teach for this fire.

You have reproduced the basic shape of nixpkgs: many derivations behind one top-level set, selected by attribute name instead of by file path.

§V — The inputs pattern

The approach so far still has three defects the Pill names:

  1. Each package imports nixpkgs directly. Better: receive what it needs as arguments, the way autotools.nix already received pkgs.
  2. You cannot cleanly compile variants (graphviz with or without libgd) without editing the expression.
  3. You cannot hand a package a particular libgd version from the call site.

The inputs pattern answers all three. Declare the package as a function of an attribute set. Those attributes are the inputs: derivations, flags, or any other customization Nix can pass.

Rewrite graphviz.nix:

{ mkDerivation, lib, gdSupport ? true, gd, pkg-config }:

mkDerivation {
  name = "graphviz";
  src = ./graphviz-2.49.3.tar.gz;
  buildInputs =
    if gdSupport
      then [
        pkg-config
        (lib.getLib gd)
        (lib.getDev gd)
      ]
      else [];
}

{ ... }: ... is an ordinary function that takes an attrset. gdSupport ? true is an optional argument with a default. When the caller passes gdSupport = false;, PNG support drops out without editing the package body. When the caller wants a different gd, it passes gd = …;. When the caller wants a different toolchain, it passes a different mkDerivation.

./src is also an input in the broad sense, but nixpkgs prefers a new expression for version bumps that need different patches or inputs. Do not teach source-swapping as the primary lever today.

Now rewrite the repository so packages no longer import nixpkgs themselves:

let
  pkgs = import <nixpkgs> { };
  mkDerivation = import ./autotools.nix pkgs;
in
with pkgs;
{
  hello = import ./hello.nix { inherit mkDerivation; };
  graphviz = import ./graphviz.nix {
    inherit
      mkDerivation
      lib
      gd
      pkg-config
      ;
  };
  graphvizCore = import ./graphviz.nix {
    inherit
      mkDerivation
      lib
      gd
      pkg-config
      ;
    gdSupport = false;
  };
}

Read the syntax carefully:

hello.nix is left as an exercise in the Pill: apply the same { mkDerivation }: … shape. Do that exercise before you claim the Done-criteria. Both packages must be independent of the repository file that calls them.

What you gained relative to session 06: Pill 8 taught you to factor mkDerivation and merge defaults with //. Pill 12 teaches you to stop embedding the package set inside each package. The factory still builds. The call site now chooses which inputs the factory sees. That is the design pattern, not a new builder.

What you did not gain yet: callPackage. Naming every input at the call site and again in the function head is tedious. Pill 13 removes that duplication with functionArgs and intersectAttrs. Keep that name in the "next" slot. Using it today would skip the proof that the package is already a function.

§VI — Self-check

Proof 1. Package as function. Write (or rewrite) one package expression whose top form is { input1, input2, … }: … and that does not contain import <nixpkgs>.

Proof 2. Optional flag. Add a boolean input with a default. Call the function twice from default.nix — once with the default, once with the flag flipped — and get two attributes (graphviz and graphvizCore in the Pill).

Proof 3. Tiny repo set. A default.nix returns an attrset of at least two packages. nix-build -A <name> (or the Pill's nix repl load) selects one member without naming its file path.

Proof 4. inherit + with. In the repository expression, pass inputs with inherit under with pkgs; and say aloud what name binding that expands to.

Proof 5. Independence. Change only the call-site attrset (different gd, different mkDerivation, or gdSupport = false) without editing the package body. The package stays a pure function of its inputs.

Proof 6. No hidden nixpkgs. Grep the package files for import <nixpkgs>. A hit means the inputs pattern is incomplete. The only import <nixpkgs> in the tiny repo should live in default.nix (or the composition root you named).

If any proof fails, re-open the Pill's "inputs pattern" section and the default.nix rewrite. Do not skip ahead to callPackage. That is session 09. Do not re-open Pill 9's NAR scan or Pill 8's // merge as the main teach. Those remain prior.

Closing

Session 08 seats the composition habit that nixpkgs rests on. You named the single-repository pattern as one lazy attrset of packages. You added a second package (graphviz), wired PKG_CONFIG_PATH from buildInputs, and composed hello plus graphviz behind default.nix. Then you inverted the dependency: packages became functions of { mkDerivation, … }, and the repository became the place that imports nixpkgs once and calls those functions with inherit.

Name the mechanism when someone asks how a Nix package stays reusable outside one repo file: the package is a function of its inputs; the repository chooses the inputs.

Session 09 is Pill 13 callPackage — removing the tedium of naming inputs twice. Do not start it in this folder. Do not write it today. Pills 10–11 remain dropped.

Examine well. The single-repo attrset is the first proof. The second package and pkg-config hook are the second. The function-of-inputs form is the third. The call-site variants are the fourth. Independence from import <nixpkgs> inside the package is the fifth. The door is { input1, input2 }: … composed by a tiny repo set.

Related