Day 49 — Patching & overrides
Day 49 — Patching & overrides
Stage V · ~4h (lab-heavy)
Goal: Fix or pin packages with overrideAttrs, patches, and overlays—without maintaining a permanent hard fork of nixpkgs.
Why this day exists
Upstream will be wrong for your window of time: a CVE pin, a broken release, a flag your hardware needs. The professional skill is minimal, reviewable deltas that compose, not copy-pasting half of nixpkgs into your flake.
Theory 1 — Three layers of change
| Mechanism | Scope | Reuse | Maintenance |
|---|---|---|---|
override / overrideAttrs |
One package instance | Call-site | Low–medium |
| Overlay | Package set view for a flake/host | Shared modules | Medium |
Fork / fetch your tree |
Anything | High control | High—avoid until needed |
nixpkgs → overlay(s) → pkgs.yourTool
↑
overrideAttrs / patches
Overlays you already touched lightly in Stage III; today they become packaging tools.
Theory 2 — overrideAttrs
pkgs.hello.overrideAttrs (old: {
patches = (old.patches or []) ++ [ ./patches/hello-msg.patch ];
postPatch = (old.postPatch or "") + ''
echo "day49" >> README
'';
})| Form | Notes |
|---|---|
overrideAttrs (old: { … }) |
Preferred; see previous attrs |
overrideAttrs { … } |
OK for total replace of listed keys; easy to drop old patches by mistake |
package.override { stdenv = …; } |
Change callPackage args (dependencies), not phases |
override vs overrideAttrs
# Change dependencies / function args
pkgs.ffmpeg.override { withXcb = true; }
# Change derivation attributes / phases
pkgs.ffmpeg.overrideAttrs (old: {
configureFlags = (old.configureFlags or []) ++ [ "--enable-something" ];
})Language builders often expose both (e.g. overridePythonAttrs).
Theory 3 — Patches as first-class inputs
stdenv.mkDerivation {
# …
patches = [
./patches/0001-fix-build-on-musl.patch
(fetchpatch {
url = "https://github.com/org/proj/commit/abc123.patch";
hash = "sha256-…";
})
];
}| Practice | Why |
|---|---|
| Numbered filenames | Apply order is list order |
| Keep patches small | Rebase pain scales with hunk size |
| Prefer upstream PRs | Local patches rot on every release |
| Test without patch once | Confirm the patch is still required |
patchPhase applies these automatically; fail logs show hunk rejects.
Theory 4 — Overlay composition
# overlays/hello-patch.nix
final: prev: {
hello = prev.hello.overrideAttrs (old: {
patches = (old.patches or []) ++ [ ../patches/hello-msg.patch ];
});
}# flake.nix fragment
pkgs = import nixpkgs {
inherit system;
overlays = [
(import ./overlays/hello-patch.nix)
(import ./overlays/my-tool.nix)
];
};Order matters
overlay1 final:prev → overlay2 final:prev → …
Later overlays see earlier results via prev/final fixed-point.
Use final when depending on other packages you also overlay; use prev to modify the incoming package without recursion traps.
final: prev: {
myApp = prev.myApp.overrideAttrs (_: { /* … */ });
# depend on overlaid hello:
myAppWrapped = final.hello;
}NixOS / HM consumption
nixpkgs.overlays = [
(import ./overlays/hello-patch.nix)
];
# or per-flake pkgs passed into modulesTheory 5 — Pinning and “don’t rebuild the world”
| Goal | Technique |
|---|---|
| One package newer than channel | Overlay with fetchFromGitHub src bump + hashes |
| One package older (pin) | Overlay src to known-good rev; note security debt |
| Only your app sees the change | Don’t put overlay in global NixOS; inject via callPackage scope |
| Temporary CI fix | Overlay in flake only; ticket to drop it |
final: prev: {
curl = prev.curl.overrideAttrs (old: rec {
version = "8.11.1";
src = prev.fetchurl {
url = "https://curl.se/download/curl-${version}.tar.bz2";
hash = "sha256-…";
};
});
}Bumping shared libraries can rebuild huge closures—check nix why-depends before celebrating.
Worked examples
Example A — local patch on hello
# patches/hello-msg.patch
--- a/src/hello.c
+++ b/src/hello.c
@@ -1,5 +1,5 @@
/* … */
- printf ("Hello, world!\n");
+ printf ("Hello, Stage V!\n");# overlays/hello-stagev.nix
final: prev: {
hello = prev.hello.overrideAttrs (old: {
pname = old.pname;
patches = (old.patches or []) ++ [ ../patches/hello-msg.patch ];
});
}Example B — overlay adding your Day 47 package
final: prev: {
hello-go = prev.callPackage ../pkgs/hello-go.nix { };
}Example C — packageOverrides legacy note
Older blogs use packageOverrides. Prefer overlays in modern flakes; recognize the old name when reading.
Lab — multi-step
Suggested workspace: ~/lab/90daysofx/02-nixos/day49
Step 1 — Baseline
nix build nixpkgs#hello -o baseline-hello
./baseline-hello/bin/helloStep 2 — Write a patch
Create a minimal patch (message change or README touch). If patching C is painful on your platform, patch a script package you own from Day 45 instead.
Step 3 — overrideAttrs only (no overlay)
In a default.nix / flake package:
pkgs.hello.overrideAttrs (old: {
patches = (old.patches or []) ++ [ ./patches/hello-msg.patch ];
})nix build .#hello-patched -L
./result/bin/helloStep 4 — Promote to overlay
Move the same change to overlays/…. Import overlay into flake pkgs. Confirm:
nix eval .#packages.x86_64-linux.hello.drvPath
# differs from unpatched when patch appliedStep 5 — Stack two overlays
- Overlay patches
hello
- Overlay adds
myTooldepending onfinal.hello
Prove myTool sees the patched hello if it references it (even a trivial wrap).
Step 6 — Dependency override
Pick a package with a boolean flag (ffmpeg, qemu, etc.—choose something not enormous if you can):
pkgs.ffmpeg.override { /* one flag */ }Build or evaluate and document why you stopped short if closure is huge.
Step 7 — Maintenance note
In NOTES.md: expiry conditions for the patch (“remove when nixpkgs ≥ rev …” / “upstream PR #”).
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| Patch rejects | Refresh patch against actual src version |
| Overlay silently not applied | Ensure same pkgs instance reaches the host/package |
| Infinite recursion in overlay | Use prev for the package you modify; avoid final.pkg = final.pkg… |
| Dropped upstream patches | Always (old.patches or []) ++ [ ours ] |
| Rebuilt half of nixpkgs | You overrode a low-level lib; narrow scope |
| Unmaintainable mega-patch | Split; upstream; or wait for release |
| Darwin vs Linux patch drift | Conditional patches with lib.optionals stdenv.isLinux |
Checkpoint
overrideAttrspatch builds and changes behavior
- Overlay form works from flake
- Two overlays composed with known order
- Difference
overridevsoverrideAttrswritten down
- Patch expiry / upstream plan noted
Commit
git add .
git commit -m "day49: patches, overrideAttrs, overlays"Write three personal gotchas before continuing.
Tomorrow
Day 50 — Multiple outputs: split out/dev/doc and shrink closures on purpose.