Day 49 — Patching & overrides

Updated

July 30, 2026

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 modules

Theory 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/hello

Step 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/hello

Step 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 applied

Step 5 — Stack two overlays

  1. Overlay patches hello
  2. Overlay adds myTool depending on final.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

  • overrideAttrs patch builds and changes behavior
  • Overlay form works from flake
  • Two overlays composed with known order
  • Difference override vs overrideAttrs written 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.