Project 4: Overlay pin and patch
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).
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#helloworks 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-labStep 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) ];
};
}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 pinYou need a patch against the source of that package version. Approaches:
- Proper patch file against upstream
hellosources (best practice).
- Lab-only
postPatchsed (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
'';
});
}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)
- Unpack hello sources (from the derivation or upstream tarball matching the pin).
- Edit the greeting.
diff -u→patches/hello-msg.patch.
- 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 ./resultIf 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:
- When is a second input cleaner than an overlay?
- What skew risk appears when
pkgsandpkgsToolboth provide libraries?
- 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
packagesanddevShell.
NOTES.md: overlay vs extra input (≥2 scenarios) + drop-after-upstream plan.
- Patch file or labeled lab-only
postPatch.
flake.lockcommitted; no infinite recursion.
Stretch goals
passthru.testsasserting the new string.
- Second package depending on patched
helloviafinal.
- Wire overlay into a tiny
nixosConfigurations.demo.
lib.composeManyExtensionswith two overlays.
Self-check
overridevsoverrideAttrs?
- When is
finalrequired?
- When is a second input better than an overlay?
- Prove an overlay is active in 30 seconds.