Project 4: Overlay pin and patch

Updated

July 30, 2026

Project 4 — Overlay: pin or patch a package

Goal: Change nixpkgs without forking the tree: apply a patch and/or pin behavior via an overlay, consume it from a flake package output and a devShell, and write a short comparison of overlay vs extra flake input.

Field Value
Baseline Nix 2.34, nixpkgs nixos-26.05
Time ~2–4 hours
Depends on the overlays chapter (overlays light), the patching and overrides chapter (patching); Projects 1–3 helpful
Artifact Working overlay + NOTES.md (overlay vs input)

Why this project exists

Sooner or later you need:

  • A one-line fix before upstream merges.
  • A temporary pin while 26.05 carries a broken minor.
  • The same patched tool in a devShell and a NixOS host.

Overlays modify the pkgs fixed-point. Done lightly, they are elegant. Done heavily, they make upgrades impossible. This project practices the light path with an observable demo (patched hello greeting or equivalent).

Important

Overlays are sharp tools. Prefer module options, single-site overrideAttrs, or separate flake inputs when they suffice. Learn overlays so you are not helpless—not so every problem becomes one.


Prerequisites

  • Can import nixpkgs { overlays = [ ... ]; } from a flake.
  • Comfortable reading overrideAttrs (old: { ... }).
  • Optional: nix build nixpkgs#hello works on your machine.

Theory

Overlay shape

final: prev: {
  # attrs to add or replace
}
Argument Meaning
prev Package set before this overlay
final Fixed-point after all overlays—use when your new pkg depends on another overridden pkg
final: prev: {
  hello = prev.hello.overrideAttrs (old: {
    patches = (old.patches or [ ]) ++ [ ./patches/hello-msg.patch ];
  });
}

Use prev.hello for the package you are overriding. Use final when composing packages that should see other overlays (e.g. final.myLib).

Three layers of change (recap)

Mechanism Scope Maintenance
override / overrideAttrs at call site One use Lowest
Overlay Whole pkgs view for flake/host Medium
Fork / replace input Anything Highest
nixpkgs  →  overlay(s)  →  pkgs.hello (patched)
                ↑
         overrideAttrs / patches

Overlay vs separate flake input

Approach When it shines Cost
Overlay patch on 26.05 Tiny delta, same channel Patch may fail on bump
Second input at a rev Need many packages from another commit Two evaluations; version skew
pkgs.hello.overrideAttrs only in one module Single consumer Easy to forget other consumers

You will document this in NOTES.md after Lab B.

Infinite recursion classic

# BAD: final.hello depending on final.hello via careless composition
final: prev: {
  hello = final.hello.overrideAttrs (_: { }); # boom
}

Prefer prev for the base package you modify.


Lab tree

~/lab/nixos-projects/overlay-lab/
  flake.nix
  flake.lock
  overlay.nix
  patches/
    hello-msg.patch
  NOTES.md
mkdir -p ~/lab/nixos-projects/overlay-lab/patches
cd ~/lab/nixos-projects/overlay-lab

Step 1 — Flake wiring (overlay applied once)

flake.nix:

{
  description = "overlay-lab — Project 4";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

  outputs = { self, nixpkgs }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];
      forAll = nixpkgs.lib.genAttrs systems;

      pkgsFor = system:
        import nixpkgs {
          inherit system;
          overlays = [ (import ./overlay.nix) ];
        };
    in {
      packages = forAll (system: {
        default = (pkgsFor system).hello;
        hello = (pkgsFor system).hello;
      });

      devShells = forAll (system:
        let pkgs = pkgsFor system;
        in {
          default = pkgs.mkShell {
            packages = [ pkgs.hello pkgs.jq ];
          };
        });

      # Optional: prove NixOS would see the same pkgs if you import similarly
      # nixosConfigurations.lab = ... nixpkgs.overlays = [ (import ./overlay.nix) ];
    };
}
Tip

legacyPackages does not automatically apply your overlay. Construct pkgs with import nixpkgs { overlays = ...; } (or nixpkgs.legacyPackages + extend) deliberately and reuse that pkgs everywhere.


Step 2 — Lab A: message patch on hello

A1. Discover the string to patch

nix build nixpkgs/nixos-26.05#hello -o /tmp/hello-stock
/tmp/hello-stock/bin/hello
# note the exact greeting line on your pin

You need a patch against the source of that package version. Approaches:

  1. Proper patch file against upstream hello sources (best practice).
  2. Lab-only postPatch sed (acceptable for learning; document as lab-only).

A2. Lab-fast path: postPatch (allowed for green)

overlay.nix:

final: prev: {
  hello = prev.hello.overrideAttrs (old: {
    pname = old.pname;
    postPatch = (old.postPatch or "") + ''
      # Lab-only: force a recognizable greeting if source layout matches GNU hello.
      # If this fails, switch to a real patches = [ ./patches/hello-msg.patch ];
      if [ -f src/hello.c ]; then
        substituteInPlace src/hello.c \
          --replace-fail "Hello, world!" "Hello, overlay-lab!"
      fi
    '';
  });
}
Note

substituteInPlace / --replace-fail behavior depends on stdenv helpers on your pin. If flags differ, use sed -i carefully or a unified diff patch. The acceptance bar is observable changed output, not a perfect GNU patch hobby.

A3. Proper patch path (preferred write-up)

  1. Unpack hello sources (from the derivation or upstream tarball matching the pin).
  2. Edit the greeting.
  3. diff -upatches/hello-msg.patch.
  4. Overlay:
final: prev: {
  hello = prev.hello.overrideAttrs (old: {
    patches = (old.patches or [ ]) ++ [ ./patches/hello-msg.patch ];
  });
}

A4. Prove it

nix build -L
./result/bin/hello
# must show your modified greeting

nix develop -c hello
# same binary lineage from overlayed pkgs

nix path-info -Sh ./result

If output is unchanged, the overlay was not applied or postPatch did not match sources—fix, do not proceed to Lab B until this is green.


Step 3 — Lab B: pin via extra input (document, implement lightly)

Sometimes you need an older/newer package graph, not a one-line patch.

flake.nix sketch (second input):

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
  # Example only — replace <rev> with a real commit after you pick one:
  # inputs.nixpkgs-tool.url = "github:NixOS/nixpkgs/<rev>";
  # inputs.nixpkgs-tool.inputs = {}; # not always needed

  outputs = { self, nixpkgs /*, nixpkgs-tool */ }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs {
        inherit system;
        overlays = [ (import ./overlay.nix) ];
      };
      # pkgsTool = import nixpkgs-tool { inherit system; };
    in {
      packages.${system} = {
        default = pkgs.hello;           # patched on 26.05
        # toolFromPin = pkgsTool.curl;  # whole package from another rev
      };
    };
}

In NOTES.md, answer:

  1. When is a second input cleaner than an overlay?
  2. What skew risk appears when pkgs and pkgsTool both provide libraries?
  3. Which approach would you choose for (a) one-line crash fix (b) need cmake 3.x from a different commit?

You do not must ship a second input for green if Lab A works—but the written comparison is required.


Step 4 — Optional NixOS consumer + upgrade drill

On a VM: set nixpkgs.overlays = [ (import ./overlay.nix) ]; and environment.systemPackages = [ pkgs.hello ];, rebuild, confirm the patched string.

In NOTES.md, write recovery if 26.05 → next stable breaks the patch: refresh patch, drop overlay if fixed upstream, switch to input pin, add passthru.tests.


Gotchas

Issue Notes
Overlay not applied legacyPackages without extend / separate import
Infinite recursion final.pkg self-ref; use prev
Patch fails on bump Comment + CI build of the package
Two nixpkgs copies Missing follows; closure duplication
nix shell nixpkgs#hello unpatched Different package set—expected

Acceptance criteria

  • Overlay changes observable behavior.
  • Visible via flake packages and devShell.
  • NOTES.md: overlay vs extra input (≥2 scenarios) + drop-after-upstream plan.
  • Patch file or labeled lab-only postPatch.
  • flake.lock committed; no infinite recursion.

Stretch goals

  1. passthru.tests asserting the new string.
  2. Second package depending on patched hello via final.
  3. Wire overlay into a tiny nixosConfigurations.demo.
  4. lib.composeManyExtensions with two overlays.

Self-check

  1. override vs overrideAttrs?
  2. When is final required?
  3. When is a second input better than an overlay?
  4. Prove an overlay is active in 30 seconds.

Next: Project 5 — Distro identity and branding.