Override Design Pattern — Pill 14
Reuse the repository attribute. Pass only the inputs that change. Keep the result overridable.
<!-- hal:authoritative:yaml -->
Reuse the repository attribute. Pass only the inputs that change. Make sure the result is overridable again so composition does not stop after one tweak.
§I — Frame
Asr session 10. Tenth live fire of the Nix language track. Week 3 fire 3. The page is Nix Pills Override Design Pattern, Pill 14. Luca Bruno wrote the series. License CC BY-SA 4.0. Ground from the on-disk EPUB at , chapter OEBPS/14-override-design-pattern.html. The live URL is canonical if the EPUB drifts: https://nixos.org/guides/nix-pills/14-override-design-pattern.html.
Session 09 was Pill 13. You learned functionArgs, intersectAttrs, and callPackage path { override = …; }. That folder stays closed for re-teaching. This session answers the next pain: you already have graphviz in the repository set, and you want a variant with a different gd without re-importing the expression and re-listing every input.
Dated folder is 2026-09-18 (ship day). Syllabus row 10 was unmarked after the 09-17 Asr HOLD while the lab Mac was down / unreachable for SoT writes; this fire clears that HOLD without backdating the bundle home. Prefer ship-day dating over a fake 2026-09-17 Nix folder.
Done-criteria from the syllabus: you can use .override and say why makeOverridable must return override again.
Not Pill 15 (NIX_PATH; dropped). Not flakes. Not install. Not Python wrapping. Launch a terminal when you have Nix. If Nix is not on the machine today, read the nix-repl numbers from the Pill and from this lesson.
§II — Why callPackage overrides are not enough
Pill 12 and 13 already let you write:
mygraphviz = callPackage ./graphviz.nix { gd = customgd; };
That still names the nix expression again. When the repository already exported graphviz, repeating the path drifts from the attribute everyone else uses. You want:
mygraphviz = graphviz.override { gd = customgd; };
.override is not an object method. Nix is functional. It is an attribute on a set that happens to also carry derivation fields.
§II.b — The repeated-import tax (Pill example)
Without override, a customized graphviz looks like:
{
mygraphviz = import ./graphviz.nix {
inherit
mkDerivation
fontconfig
libjpeg
bzip2
;
gd = customgd;
};
}
Every input except gd is noise you already paid for in the repository attribute. .override deletes that tax. The Pill stresses that this is both easier to maintain and aligned with how a central repository should be consumed: customize at the edge, do not fork the expression list.
§III — makeOverridable (first cut)
Put this in lib.nix:
{
makeOverridable =
f: origArgs:
let
origRes = f origArgs;
in
origRes // { override = newArgs: f (origArgs // newArgs); };
}
makeOverridable takes a function f that returns a set, and the original argument set. It returns the original result merged with an override attribute. That attribute is a function: new args are //-merged onto origArgs, then f runs again.
Pill nix-repl walk:
nix-repl> f = { a, b }: { result = a+b; }
nix-repl> res = makeOverridable f { a = 3; b = 5; }
nix-repl> res
{ override = «lambda»; result = 8; }
nix-repl> res.override { a = 10; }
{ result = 15; }
The first override works. The returned set with result = 15 has no override attribute. A second override is impossible. Composition stops. That breaks the design goal.
§IV — Return override again (the done-criterion)
rec {
makeOverridable =
f: origArgs:
let
origRes = f origArgs;
in
origRes // { override = newArgs: makeOverridable f (origArgs // newArgs); };
}
rec lets makeOverridable call itself. Now each override result is itself overridable:
nix-repl> res2 = res.override { a = 10; }
nix-repl> res2
{ override = «lambda»; result = 15; }
nix-repl> res2.override { b = 20; }
{ override = «lambda»; result = 30; }
Why must makeOverridable return override again? Because utilities that transform derivations (debugVersion, applyPatches, further .override calls) expect the same shape. If override vanishes after one use, debugVersion (graphviz.override { gd = customgd; }) still works only if those utilities do not need a second override. The Pill's point is composability: the result of override should remain a first-class overridable value.
Say it in one sentence for the syllabus check: .override must call makeOverridable again so the new result still carries .override.
§V — Repository shape
Once callPackage is taught to wrap with makeOverridable (Pill exercise), repository users write:
mygraphviz = graphviz.override { gd = customgd; };
Dream composition from the Pill conclusion:
debugVersion (graphviz.override { gd = customgd; })
Central nixpkgs stays untouched. Local overlays and shells customize by override. When upstream graphviz moves, your override rides the new baseline automatically.
§V.b — Composability intuition
Functional packaging wants transformers that preserve structure. The Pill opens with:
debugVersion (applyPatches [ ./patch1.patch ./patch2.patch ] drv)
Each utility accepts a derivation-shaped value and returns one. .override is the inputs-side cousin of that idea: change arguments, rebuild the set, keep the same surface so the next utility still applies.
Without the recursive makeOverridable, the chain dies after one override. With it, local machines customize nixpkgs packages without forking expressions.
§V.c — Common mistakes
- Treating
.overrideas mutation. It returns a new set. The originalgraphvizattribute is unchanged. - Forgetting
recon themakeOverridablebinding so the self-call cannot see the name. - Implementing override as
f newArgsinstead off (origArgs // newArgs), which drops unspecified originals. - Re-importing
./graphviz.nixin every overlay after you already have a repository attribute (defeats the pattern). - Jumping to flakes or
packageOverridesbefore the single-attribute.overridestory is solid.
§V.d — Tie-back to session 09
callPackage already accepted a third-argument override set at the call site. Pill 14 lifts that idea onto the result value. After callPackage is wrapped with makeOverridable, users stop caring which file path produced graphviz. They only write .override { … } against the attribute.
If your scratch callPackage from session 09 does not yet wrap makeOverridable, do that as the Pill's reader exercise before relying on .override in a real repo.
§VI — Boundaries
- Not Pill 15 search paths.
- Not flakes.
- Not a full nixpkgs
packageOverrides/ fixed-point lesson (session 12 / Pill 17). - Do not re-teach
functionArgsbeyond naming it as the callPackage neighbor from session 09.
§VII — Close
Session 09 filled arguments from the set. Session 10 customizes an existing attribute without re-importing the file, and keeps the result overridable. Clears the 09-17 Asr HOLD by shipping session 10 on 2026-09-18. Next unmarked: session 11, Pills 16, nixpkgs as a function.