Hedronite Lesson · Polyglot-Dev / Nix · Tue 2026-09-22

Nixpkgs Overriding Packages — Pill 17

Override one package so dependents re-pick it via config.packageOverrides.

Lesson Class: Asr (Nix language track)
Focus: packageOverrides · fixed point · graphviz · asciidoc-full · config.nix
Done-criteria: override one pkg so dependents pick it via config.packageOverrides / fixed point
Grounding: on-disk nix-pills.epub Pill 17 · live nixos.org canonical
Note: Not Pill 15 · not flakes · not install · not Python wrap · next session 13 Pills 18
Lazy Fix
fix f so the set receives its own result; force only needed attrs.
Dependent Pick
config.packageOverrides injects overrides into the pkgs fixed point.
Precision
Old asciidoc keeps old graphviz; new rebuild depends on new.
A lone override changes one leaf; dependents keep the leaf they already bound.

<!-- 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

  1. Calling graphviz.override { … } once and expecting asciidoc-full to follow without packageOverrides or a fixed-point recompose.
  2. Mutating pkgs.graphviz = … after import and treating Nix like an imperative registry.
  3. Forgetting to pass config when the override file is not in the auto-import location.
  4. Jumping to flakes overlays before you can state Lazy Fix + Dependent Pick in Pill language.
  5. Confusing Pill 15 search paths with this session (Pill 15 stays dropped).

§X — Boundaries

§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.