Patching Packages

Updated

September 12, 2026

Patching Packages

Upstream is almost right. The boring default is: a .patch file in git, patches = [ ./desk-fix.patch ]; or prev.pkg.overrideAttrs, not a fork of nixpkgs.

Mental model

stdenv patchPhase runs patch -p1 for each entry in patches. Fetch a patch with fetchpatch / fetchpatch2 when it lives on GitHub; vendor it in-tree when it is your fix.

src tarball  →  patchPhase  →  configure/build
                  ▲
                  └── ./desk-fix.patch  (in git)

Overlays compose: prev.hello.overrideAttrs (old: { patches = (old.patches or []) ++ [ ./x.patch ]; }).

fetchpatch2 is the 24.11+ spelling that hashes the applied result more reliably. On 26.05, prefer fetchpatch2 for remote patches. A patch is an input: change the diff → new hash → rebuild.

Worked examples

Case 1: In-tree patch on your package

Save as desk-fix.patch (illustrative unified diff):

# desk-fix.patch — unified diff against src root
--- a/main.c
+++ b/main.c
@@ -1,3 +1,3 @@
 int main(void) {
-  return 1;
+  return 0;
 }

Save as patched.nix:

# patched.nix
{ stdenv }:

stdenv.mkDerivation {
  pname = "desk-c";
  version = "1.0";
  src = ./src;
  patches = [ ./desk-fix.patch ];
}
nix-build -E 'with import <nixpkgs> {}; callPackage ./patched.nix {}'

The patch is an input. Change the diff → new drv. That is what you want. Generate with git diff from the unpacked src root so -p1 matches.

patchFlags = [ "-p1" ]; is the stdenv default. A diff generated from inside a subdirectory needs -p2 — set patchFlags, do not hand-edit the hunk headers unless you must. chmod +x after a patch that adds a script: postPatch.

Case 2: Overlay on nixpkgs hello

Save as overlay.nix:

# overlay.nix
final: prev: {
  hello = prev.hello.overrideAttrs (old: {
    patches = (old.patches or [ ]) ++ [ ./hello-desk.patch ];
  });
}

You do not copy hello’s expression. You append. final.hello.overrideAttrs here is infinite recursion — prev.

If upstream already has patches, (old.patches or [ ]) ++ is required. Replacing patches = [ ./x.patch ]; drops nixpkgs’s patches and is a silent CVE.

Case 3: fetchpatch2 from a PR

Save as fetch-patch.nix:

# fetch-patch.nix
{ pkgs ? import <nixpkgs> { } }:

pkgs.hello.overrideAttrs (old: {
  patches = (old.patches or [ ]) ++ [
    (pkgs.fetchpatch2 {
      url = "https://github.com/example/hello/commit/abc123.patch";
      hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
      # decode = "unzip";  # only if the URL is a zip of a patch
    })
  ];
})
nix-build fetch-patch.nix

Probe the hash. When GitHub force-pushes the PR, the hash mismatches — good. Pin a commit URL (/commit/<full-sha>.patch), not /pull/1.patch which moves. fetchpatch (v1) is the old hasher; fetchpatch2 on 26.05.

GitHub may wrap the patch (CRLF, subject prefix). If patchPhase fails after a good hash, fetchpatch2 still did its job — the hunk does not apply to this src. Rebase the patch against 26.05’s tarball, do not patchFlags = [ "-p0" "--force" ].

Case 4: sed in postPatch (last resort)

# postpatch.nix fragment
{
  postPatch = ''
    substituteInPlace Makefile --replace-fail /usr/local "$out"
  '';
}

--replace-fail (not --replace, which was removed) so a silent no-op cannot ship. --replace-warn is for a transition. Prefer a real patch when the change is more than one substitution. substituteInPlace runs in postPatch after patches — order matters if the hunk and the sed touch the same line.

Case 5: Drop an upstream patch

# drop.nix
final: prev:
let
  pkgs = prev;
in
{
  hello = prev.hello.overrideAttrs (old: {
    patches = builtins.filter
      (p: !(pkgs.lib.hasSuffix "bad.patch" (baseNameOf (toString p))))
      (old.patches or [ ]);
  });
}

Rare. Document why in a comment. Next nixpkgs bump may make the filter a no-op — check the diff in the lock PR.

The trap

The trap is editing files in /nix/store/...-hello “to test.” Read-only. Copy the expression, patch, overlay.

The other trap is a patch with Windows line endings or the wrong -p strip level. patchPhase fails. Generate with git diff from the unpacked src root.

A third: patches = [ ./x.patch ]; wiping nixpkgs’s list. A fourth: /pull/N.patch as a FOD URL.

The boring rule

  • Your fix: ./foo.patch in git + patches = (old.patches or []) ++ [ ].
  • Upstream PR: fetchpatch2 + commit URL + hash. Not /pull/1.patch.
  • Overlay overrideAttrs from prev; do not fork nixpkgs for one hunk.
  • substituteInPlace --replace-fail for path rewrites, after patches.
  • Patch is an input. It must apply cleanly on 26.05’s src.

Try this

  1. git diff a one-line change; save as .patch; add to patches; nix-build.
  2. Break the hunk context; read patchPhase failure in nix log.
  3. Overlay hello with an extra empty patch file and nix-build under that overlay.
  4. fetchpatch2 with fakeHash, paste got.