overlays and overrides
modifying and extending nixpkgs without forking it.
modifying and extending nixpkgs without forking it.
most local nixpkgs changes do not need a fork.
if one package needs a different patch, source, or build flag, override that package. if other packages should see the changed package through pkgs, use an overlay.
each one changes a different layer.
overridestdenv, fetchurl, opensslbefore mkDerivationoverrideAttrssrc, patches, phases, metainside mkDerivationoverlayhello, openssl, my-toolwhile constructing pkgsoverrideAttrs changes the attribute set passed to mkDerivation.
pkgs.hello.overrideAttrs (old: {
pname = "hello-local";
})
on the pinned nixpkgs used here:
$ nix eval --impure --raw --expr '
let pkgs = import /nix/store/6bwyhcqsr8pj2gyvdzsr742v9jf2zn52-source {
system = "aarch64-darwin";
};
in (pkgs.hello.overrideAttrs (old: { pname = "hello-local"; })).name
'
hello-local-2.12.1
the original derivation path:
/nix/store/5mh0djicybhh26h0m8qciaizc1iwsy83-hello-2.12.1.drv
the overridden one:
/nix/store/nvphx4l6wci2fqi3fqj9kyiz8l01jccf-hello-local-2.12.1.drv
the original package is unchanged. the override produces another derivation.
the override function receives the previous derivation attributes:
pkgs.hello.overrideAttrs (old: {
configureFlags = (old.configureFlags or [ ]) ++ [
"--enable-something"
];
})
old.configureFlags or [ ] handles packages that do not set configureFlags. derivations do not all define the same attributes.
you can change phases:
pkgs.hello.overrideAttrs (old: {
postInstall = (old.postInstall or "") + ''
mkdir -p $out/share/example
echo local > $out/share/example/source
'';
})
you can add patches:
pkgs.hello.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [
./fix-build.patch
];
})
start with the package in nixpkgs, then change the part that differs.
overrideAttrs can also take two arguments:
pkgs.hello.overrideAttrs (final: previous: {
pname = "hello-local";
postInstall = (previous.postInstall or "") + ''
echo ${final.version} > $out/share/hello-version
'';
})
previous is the old attribute set. final is the new one after overrides are applied.
use previous when extending an existing value. use final when the new value should refer to another new value.
avoid using final to decide which attribute names exist. attribute names are needed before the final set is fully known, and that can produce infinite recursion. values can depend on final; names should not.
override works one layer earlier. it changes the arguments passed to the package function.
for hello, the package function starts with this:
{
callPackage,
lib,
stdenv,
fetchurl,
nixos,
testers,
versionCheckHook,
hello,
}:
those names are overrideable function arguments:
pkgs.hello.override {
stdenv = pkgs.clangStdenv;
}
that reruns the package function with a different stdenv. the derivation is not edited after the fact; the package function receives different inputs before mkDerivation is called.
use the layer that matches the change:
| use | when |
|---|---|
override | swap a function argument, such as a dependency, builder, or feature flag |
overrideAttrs | change derivation attributes, such as source, patches, phases, or metadata |
| overlay | make the change part of a package set |
an overlay changes the package set itself. it is a function:
final: prev: {
hello-local = prev.hello.overrideAttrs (old: {
pname = "hello-local";
});
}
two arguments:
| name | meaning |
|---|---|
final | the package set after all overlays are applied |
prev | the package set before this overlay |
the overlay returns an attribute set. those attributes are merged into nixpkgs while the package set is being constructed.
prev.hello means "the old hello". final.hello-local means "the hello-local attribute after overlays have been applied".
hellocurlhello = prev.hello.overrideAttrs ...hello-local = prev.hello ...hellopatched hellocurlunchangedhello-localnew attran overlay can add a new attribute:
final: prev: {
hello-local = prev.hello.overrideAttrs (old: {
pname = "hello-local";
});
}
after applying it:
pkgs.hello-local
exists next to:
pkgs.hello
the original package is unchanged. when experimenting, add a new name instead of replacing an existing one.
you can also replace an existing attribute:
final: prev: {
hello = prev.hello.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [ ./hello.patch ];
});
}
now pkgs.hello means the patched package in this package set.
anything in the same final package set that depends on hello can see the patched version, depending on how that dependency is wired. the change is now part of pkgs, not a local variable next to it.
wide dependencies are different. replacing openssl, zlib, or stdenv can rebuild a lot.
most package modifications start from prev:
final: prev: {
hello = prev.hello.overrideAttrs (old: {
pname = "hello-local";
});
}
use prev when you want the package as it existed before this overlay.
this is wrong:
final: prev: {
hello = final.hello.overrideAttrs (old: {
pname = "hello-local";
});
}
final.hello is the new hello, but this expression is defining the new hello. infinite recursion.
use final when a dependency should come from the final package set:
final: prev: {
my-tool = prev.callPackage ./my-tool.nix {
openssl = final.openssl;
};
}
if another overlay changed openssl, my-tool sees that changed openssl.
inside a flake, pass overlays when importing nixpkgs:
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
outputs = { nixpkgs, ... }:
let
system = "aarch64-darwin";
overlays = [
(final: prev: {
hello-local = prev.hello.overrideAttrs (old: {
pname = "hello-local";
});
})
];
pkgs = import nixpkgs {
inherit system overlays;
};
in
{
packages.${system}.hello-local = pkgs.hello-local;
};
}
import nixpkgs { inherit system overlays; } constructs a package set with the overlay applied.
flakes do not make overlays special. they give you a pinned nixpkgs input and a place to expose the result.
overlays are applied in order:
overlays = [
overlayA
overlayB
overlayC
];
overlayB sees the result of overlayA as prev. overlayC sees the result of both.
example:
overlayA = final: prev: {
hello = prev.hello.overrideAttrs (_: {
pname = "hello-a";
});
};
overlayB = final: prev: {
hello-copy = prev.hello;
};
hello-copy is based on hello-a, because overlayB runs after overlayA.
you do not need an overlay for every local package.
for one package:
let
pkgs = import nixpkgs { inherit system; };
my-tool = pkgs.callPackage ./my-tool.nix { };
in
{
packages.${system}.my-tool = my-tool;
}
no overlay needed.
use an overlay when the package should become part of pkgs, or when other packages should find the modified package through normal callPackage lookup.
small overrides can still cause large builds.
change hello.pname, and only hello changes.
change openssl, and everything that depends on that openssl may change.
change stdenv, and you have asked nix to rebuild the platform.
before changing a common dependency, check what depends on it:
nix path-info -r nixpkgs#your-package
if the closure is large, the rebuild probably is too.
use overrideAttrs for one package. use override when the package already exposes the thing you need as an argument. use an overlay when the change should become part of pkgs.
fork nixpkgs when the change belongs upstream or when you need to carry a large patch series for a while.
in all three cases, you are changing the inputs to a derivation or to the package set that produces it.