CallPackage Design Pattern — Pill 13
Declare the parameter names once. callPackage fills them from the repository set. Overrides ride a third argument.
<!-- hal:authoritative:yaml -->
*Declare the parameter names once. callPackage fills them from the repository set. Overrides ride a third argument, not a second declaration.*
§I — Frame
Asr session 09. Ninth live fire of the Nix language track. Week 3 fire 2. The page is Nix Pills Callpackage Design Pattern, Pill 13. Luca Bruno wrote the series. License CC BY-SA 4.0. Ground from the on-disk EPUB at , chapter OEBPS/13-callpackage-design-pattern.html. The live URL is canonical if the EPUB drifts: https://nixos.org/guides/nix-pills/13-callpackage-design-pattern.html.
Session 08 was Pill 12. You turned each package into a function of its inputs and composed a tiny repo set that called those functions with inherit. That folder stays closed for re-teaching. This session removes the tedium: naming every input twice (once on the function, once at the call site). Pills 10 and 11 stay dropped. Not install. Not flakes. Not Python wrapping. Not Pill 14 .override beyond naming it as the next session. Dolstra stays on the shelf unused; this fire has no NAR or store-hash teach.
Done-criteria from the syllabus: you can explain functionArgs + intersectAttrs and write callPackage path { override = …; }.
Launch a terminal when you have Nix. Attribute the Pill's nix-repl numbers to the Pill. If Nix is not on the machine today, read the results here. Do not install Nix in this session.
§II — The duplication the inputs pattern still leaves
Pill 12 solved reuse. A package no longer hard-codes import <nixpkgs> { } inside its body. The repository chooses mkDerivation, gd, and friends. The cost is repetition. You declare the parameter names on the package function, then you pass the same names again when you call it:
rec {
lib1 = import ./package1.nix { inherit input1 input2; };
program2 = import ./package2.nix { inherit inputX inputY lib1; };
}
Two facts make that repetition constant in a real repo. First, input names usually match attribute names already present on the repository (or on nixpkgs). Second, under rec, one package's input may be another package in the same set. You want the call site to say "take defaults from the big set" and only list the exceptions.
That is the callPackage convention:
{
lib1 = callPackage ./package1.nix { };
program2 = callPackage ./package2.nix { someoverride = overriddenDerivation; };
}
Behavior you will build:
- Import the path. Get a function that uses the inputs pattern.
- Read that function's parameter names at evaluation time.
- Fill those names from the repository set. Merge an overrides set on top when the caller wants a variant.
§III — functionArgs: names without values
Nix gives you the parameter names of a function as an attribute set. Values in that set are booleans: true means the parameter has a default; false means it does not. You care about the names today, not the default flags.
nix-repl> add = { a ? 3, b }: a + b
nix-repl> builtins.functionArgs add
{ a = true; b = false; }
a has a default. b does not. Both names appear. That is enough to know what to pull from the repository.
Hold the distinction: functionArgs introspects the declaration. It does not call the function. It does not look at a repository. It answers "which names does this inputs-pattern package expect?"
§IV — intersectAttrs: fill from a set
Given a big set of possible values and the name set from functionArgs, you want the intersection of names, with values taken from the big set.
nix-repl> values = { a = 3; b = 5; c = 10; }
nix-repl> builtins.intersectAttrs values (builtins.functionArgs add)
{ a = true; b = false; }
nix-repl> builtins.intersectAttrs (builtins.functionArgs add) values
{ a = 3; b = 5; }
Order matters. intersectAttrs A B keeps names that appear in both, and takes values from B. Put functionArgs first and the repository second when you want filled arguments. Put them the other way and you get the boolean flags from functionArgs again, which is useless for calling the package.
c drops out because add never asked for it. An argument missing from the big set errors at call time unless the package uses ... (Pill 5). Named packages in a repo almost never need that escape here.
§V — Three-argument callPackage
Wire the two builtins into one helper. First form: set of possibles, then function.
nix-repl> callPackage = set: f: f (builtins.intersectAttrs (builtins.functionArgs f) set)
nix-repl> callPackage values add
8
nix-repl> with values; add { inherit a b; }
8
Same result as naming a and b by hand. The last missing piece is an overrides set so you are not stuck with the defaults forever:
nix-repl> callPackage = set: f: overrides:
f ((builtins.intersectAttrs (builtins.functionArgs f) set) // overrides)
nix-repl> callPackage values add { }
8
nix-repl> callPackage values add { b = 12; }
15
// puts overrides on the right, so they win. Empty { } means "accept the intersection as-is." That is the whole mechanism: introspect names, intersect with the repo, union overrides, call.
These builtins are packaging tools. You rarely need them inside a single package body. You need them when you write the repository composition layer.
Why this pattern won in nixpkgs: thousands of packages share input names (stdenv, lib, fetchurl, and peer packages). Hand-writing inherit at every call site does not scale and drifts when a package gains a new input. callPackage makes the default path "fill from the set," and keeps the exceptional path as a small overrides attrset. That is why the Pill calls it the de facto standard for importing packages into a repository. You are learning the composition layer, not a toy helper.
§VI — Repository rewrite: allPkgs, laziness, graphvizCore
Fold the helper into default.nix the way the Pill does. Import the file inside callPackage so each package line stays a path plus an overrides set:
let
nixpkgs = import <nixpkgs> { };
allPkgs = nixpkgs // pkgs;
callPackage =
path: overrides:
let
f = import path;
in
f ((builtins.intersectAttrs (builtins.functionArgs f) allPkgs) // overrides);
pkgs = with nixpkgs; {
mkDerivation = import ./autotools.nix nixpkgs;
hello = callPackage ./hello.nix { };
graphviz = callPackage ./graphviz.nix { };
graphvizCore = callPackage ./graphviz.nix { gdSupport = false; };
};
in
pkgs
Read the bindings in order of intent, not textual order.
nixpkgs is the upstream set. The name pkgs now means your repository, so the Pill renames the import.
allPkgs is nixpkgs // pkgs. Package functions may ask for upstream names (stdenv tools via your mkDerivation path, gd, and so on) or for siblings in your repo. One union feeds intersectAttrs.
callPackage imports the path, intersects against allPkgs, merges overrides, and calls. Moving mkDerivation into pkgs lets it auto-fill like any other input.
graphvizCore is the override proof without editing graphviz.nix. Same path, { gdSupport = false; }. Session 08 built that variant with a hand-written call. Session 09 keeps the variant and drops the hand-written argument list for the default case.
The circular binding is intentional. pkgs is defined with callPackage, and callPackage closes over allPkgs, which includes pkgs. Lazy evaluation makes that safe: intersectAttrs needs the keys of allPkgs to decide which names match. It does not need every package value forced before the intersection runs. Selecting hello does not force graphviz to build. The same laziness that made a single-repo attrset affordable in Pill 12 keeps this fixed point affordable here.
Practical reading order when you open a foreign default.nix that uses this pattern:
- Find
callPackage. Confirm it imports a path, intersects against a combined set, and merges overrides. - Find the combined set (
allPkgsor an equivalent union of upstream + local). - Find the local
pkgsattrset. Each member should be either acallPackageline or a small binding that belongs in the auto-fill pool (mkDerivationin the Pill). - Treat a non-empty overrides set as a deliberate variant, not as the normal path.
If you cannot find those four pieces, you are not looking at the Pill 13 pattern yet. You may be looking at a hand-written inputs call (Pill 12) or at a later .override wrapper (Pill 14).
§VII — Proofs (done-criteria)
Do these in order. Stop when one fails; re-open the matching Pill section before moving on.
**Proof 1. functionArgs.** In nix-repl, define { a ? 3, b }: a + b and print builtins.functionArgs. Say which name has a default and which does not.
**Proof 2. intersectAttrs order.** With values = { a = 3; b = 5; c = 10; }, show both argument orders. Point at which result carries integers and which carries booleans.
Proof 3. Autocall equals inherit. Show callPackage values add equals with values; add { inherit a b; }.
Proof 4. Overrides win. Call with { b = 12; } and get 15. Say aloud that // put the override on the right.
Proof 5. Repo shape. In your tiny default.nix, hello and graphviz use callPackage ./….nix { }. graphvizCore uses the same path with { gdSupport = false; }. No package file re-lists every input at the call site.
Proof 6. Name the two builtins. Without looking at notes: functionArgs yields parameter names; intersectAttrs fills those names from the repo set; overrides merge last. That sentence is the syllabus done-criteria.
If any proof fails, re-open the Pill's callPackage implementation section. Do not skip ahead to .override / makeOverridable. That is session 10 (Pill 14). Do not re-open Pill 12's inputs rewrite or Pill 9's NAR scan as the main teach.
Closing
Session 09 seats the composition helper nixpkgs actually uses. You named the duplication the inputs pattern still left. You read parameter names with functionArgs, filled them with intersectAttrs, and merged overrides with //. You rewrote the tiny repo so packages are callPackage path { } lines, with graphvizCore as the one-line variant. You said why pkgs and callPackage may close over each other: laziness intersects on keys before values must exist.
Name the mechanism when someone asks how nixpkgs avoids typing every input twice: callPackage introspects the package function, intersects with the package set, and lets a third argument override.
Session 10 is Pill 14 Override. Starting from pkgs.graphviz and flipping gd, instead of only starting from the .nix file. Do not start it in this folder. Do not write it today. Pills 10–11 remain dropped.
Examine well. functionArgs is the first proof. intersectAttrs order is the second. Autocall equivalence is the third. Overrides on the right are the fourth. The repo rewrite and lazy allPkgs are the fifth. The door is callPackage path { override = …; } with names filled once.
Related
- Syllabus: Asr Nix syllabus, session 09
- Syllabus: Asr Bun/TS syllabus pointer (peer track; Nix-only this fire)
- Prior: Asr session 08 · inputs design pattern
- Prior: Asr session 07 · automatic runtime deps
- Prior: Asr session 06 · generic builders
- Grounding tome: Nix Pills EPUB (Pill 13)
- Tome hub: Nix language tomes (CC BY-SA 4.0 note)
- Live URL: https://nixos.org/guides/nix-pills/13-callpackage-design-pattern.html
- Next fire: session 10, Pills 14 Override (unwritten)