Nixpkgs Overriding Packages — Pill 17
Override one package so dependents re-pick it via config.packageOverrides.
<!-- hal:authoritative:yaml -->
*Override one package in the set so dependents re-pick it. Use config.packageOverrides and the fixed-point pattern. Keep old dependents on old inputs until they rebuild.*
§I — Frame
Asr session 12. Twelfth live fire of the Nix language track. Week 4 fire 2. The page is Nix Pills Nixpkgs Overriding Packages, Pill 17. 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/17-nixpkgs-overriding-packages.html.
Session 11 opened nixpkgs as a function: you pass system and config. Session 10 gave you .override on a single call. This session asks the next question: when package P depends on graphviz, how does P see your overridden graphviz instead of the stock one?
Done-criteria from the syllabus: you can override one package so dependents pick it via config.packageOverrides / fixed point.
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 — Recall .override, then the dependent problem
Pill 14 made the call (function + parameters) overridable. graphviz accepts withXorg. Null or false drops X support:
$ nix repl
nix-repl> :l <nixpkgs>
nix-repl> :b graphviz.override { withXorg = false; }
That builds one derivation. It does not rewrite every attribute that already closed over stock graphviz. If P was composed against pkgs.graphviz, your local override of one expression does not travel into P unless the set itself is recomposed.
Musashi declarative: a lone override changes one leaf; dependents keep the leaf they already bound.
§III — Imperative assignment vs pure Nix
In an imperative package manager you might write:
pkgs = import <nixpkgs> {};
pkgs.graphviz = pkgs.graphviz.override { withXorg = false; };
build(pkgs.P)
That mutation would make P see the new graphviz if P looked up the name at build time. Nix assigns once. You cannot rebind pkgs.graphviz after the set exists and expect dependents to notice. You need a design that recomposes the set so dependents read the overridden attribute when they force their inputs.
§IV — Fixed point (named technique: Lazy Fix)
Yagyu names the cut once. Lazy Fix: define fix so a function receives its own result, and force attributes only when needed.
From nixpkgs (shape the Pill shows):
{
fix =
f:
let
result = f result;
in
result;
}
fix takes f, binds result = f result, and returns result. It looks like f(f(f(…. Lazy evaluation stops the loop: inner calls run only for attributes someone forces.
nix-repl> fix = f: let result = f result; in result
nix-repl> pkgs = self: { a = 3; b = 4; c = self.a + self.b; }
nix-repl> fix pkgs
{ a = 3; b = 4; c = 7; }
Without rec, c still refers to a and b through self. First the outer call holds an unevaluated thunk. Forcing c evaluates self.a and self.b. Those force another call of pkgs, but that call does not need c, so evaluation terminates.
If-then-thus: if dependents must see overridden inputs, then recompose the set through a fixed point that merges overrides into self, thus later attributes read the new bindings instead of the stock ones.
§V — Override a set with fixed point
Because self.a and self.b come from the passed set, you can inject overrides and recompute dependents:
nix-repl> overrides = { a = 1; b = 2; }
nix-repl> let newpkgs = pkgs (newpkgs // overrides); in newpkgs
{ a = 3; b = 4; c = 3; }
nix-repl> let newpkgs = pkgs (newpkgs // overrides); in newpkgs // overrides
{ a = 1; b = 2; c = 3; }
First form: compute with overrides, keep original a/b on the result surface. Second form: also put the override attrs on the result. c becomes 1+2=3 in both because the inner self saw the override.
§VI — config.packageOverrides (named technique: Dependent Pick)
Dependent Pick: put the override in config.packageOverrides so nixpkgs returns a fixed point that injects your change for the whole graph.
config.nix somewhere on disk:
{
packageOverrides = pkgs: {
graphviz = pkgs.graphviz.override {
withXorg = false;
};
};
}
Import nixpkgs with that config, then build a dependent:
nix-repl> pkgs = import <nixpkgs> { config = import ./config.nix; }
nix-repl> :b pkgs.asciidoc-full
asciidoc-full depends on graphviz (pkgs.asciidoc is the lighter package and does not use graphviz). With no binary-cache hit for asciidoc against graphviz-without-X, Nix rebuilds what it must. Pass config at import time; that is the same Policy Gate from session 11, now carrying packageOverrides.
§VII — ~/.config/nixpkgs/config.nix auto-import
Pill 16 already named the resolution path. The config.nix above can live at ~/.config/nixpkgs/config.nix (older docs: ~/.nixpkgs/config.nix). Then plain import <nixpkgs> {} loads it. Explicit config = import ./config.nix still wins when you pass it.
§VIII — Precision: old keeps old, new rebuild depends on new
Imperative managers replace a library and hope every app follows. Nix pins inputs by store path. Old asciidoc that already closed over stock graphviz keeps that graphviz. Newly built asciidoc-full depends on the overridden graphviz. Two generations can coexist. That precision is the point of the pattern, not a bug.
§IX — Common mistakes
- Calling
graphviz.override { … }once and expectingasciidoc-fullto follow withoutpackageOverridesor a fixed-point recompose. - Mutating
pkgs.graphviz = …after import and treating Nix like an imperative registry. - Forgetting to pass
configwhen the override file is not in the auto-import location. - Jumping to flakes overlays before you can state Lazy Fix + Dependent Pick in Pill language.
- Confusing Pill 15 search paths with this session (Pill 15 stays dropped).
§X — Boundaries
- Not Pill 15 NIX_PATH.
- Not flakes.
- Not install (Pills 1–3).
- Not Python
withPackages/ wrap (later spine). - Do not re-teach
system/configbasics beyond naming them as the import knobs that carrypackageOverrides.
§XI — Close
Session 11 opened the repository function. Session 12 wires overrides through the fixed point so dependents re-pick: config.packageOverrides, Lazy Fix, Dependent Pick, and the precision that old outputs stay on old inputs. Next unmarked: session 13, Pills 18, store paths.
Related:
Write a tiny config.nix with packageOverrides for graphviz without X, import it into <nixpkgs>, and say aloud whether asciidoc-full will rebuild before you leave the keyboard.