Patching Packages
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.nixProbe 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.patchin git +patches = (old.patches or []) ++ [ ]. - Upstream PR:
fetchpatch2+ commit URL + hash. Not/pull/1.patch. - Overlay
overrideAttrsfromprev; do not fork nixpkgs for one hunk. substituteInPlace --replace-failfor path rewrites, after patches.- Patch is an input. It must apply cleanly on 26.05’s
src.
Try this
git diffa one-line change; save as.patch; add topatches;nix-build.- Break the hunk context; read
patchPhasefailure innix log. - Overlay
hellowith an extra empty patch file andnix-buildunder that overlay. fetchpatch2withfakeHash, pastegot.