Stop lying about pkgs!
2025-08-08 | Nix
Consider the following Nix code:
{ pkgs, ... }: {
environment.systemPackages = [
pkgs.foo
];
}
NixOS users will be very familiar with this configuration pattern: it's a NixOS module that adds a package to your system. We can
confidently reason about this piece of configuration:
- The package comes from the Nix package repository
nixpkgs,
- and it's the package
foo.
But those are only assumptions we make, and there's nothing stopping us from lying about pkgs.foo. Are you absolutely sure
you're installing foo and not bar, for instance?
{ pkgs, ... }: {
nixpkgs.overlays = [
(final: prev: {
foo = final.bar;
})
];
environment.systemPackages = [
# pkgs.bar in a disguise
pkgs.foo
];
}
The code above uses an overlay, which is a tool to change and extend nixpkgs. It works like this:
In short, each overlay is a Nix function accepting the two arguments final and prev, with final
representing the "finalized" nixpkgs state and prev representing the previous stage of overlaying.
How overlaying works (credit: wiki.nixos.org)
In the example given, we took the final bar and assigned it to foo. This change gets applied to the { pkgs, ... }
module argument, which obviously in turn changes what pkgs.foo evaluates to in our environment.systemPackages list.
In other words, we have now successfully lied about pkgs.
Why do people lie? Because they're taught it as a solution.
In fact, there's a multitude of problems that arise from using Nix that overlays solve:
- "I want to use the absolutely newest version of a package but nixpkgs doesn't have it"
- "I need to patch this package in order for it to work"
- "I want to distribute custom packages or derivation builders that I've made"
Overlays solves these problems and with quite little code too. Just import a single overlay into your configuration and you
get all of this magic at your fingertips. But the cost is that you have to lie. There is a beauty in knowing that pkgs is
a pristine snapshot of github:NixOS/nixpkgs at a point in time. Overlays ruin that.
Any given module adding an overlay propagates the lie across your entire configuration: Our module above
played nice; overlay defined and consumed in the same module. Let's play dirty.
./bar.nix
{
nixpkgs.overlays = [
(final: prev: {
foo = final.bar;
})
];
}
./foo.nix
{ pkgs, ... }: {
programs.foo = {
enable = true;
...
};
}
./configuration.nix
{
imports = [
./bar.nix
./foo.nix
...
];
}
Now it's suddenly not so easy to troubleshoot foo's mysterious bar tendencies if you're spending an entire evening debugging ./foo.nix.
Any given module adding an overlay propagates the lie across all callPackage calls: Perhaps a more serious problem for intermediate NixOS users who
have begun to package software is that their callPackage calls now have been infected with mystery meat derivation arguments. This might not be a problem
for $CURRENT_DAY you which is familiar with the overlayed args, but it might be for $CURRENT_DAY+1 you, or someone other than you reading the code.
{ pkgs, ... }: {
environment.systemPackages = [
(pkgs.callPackage ({
stdenv,
fetchFromGitHub
# who the hell are you?
impostorPkg,
# grep nixpkgs: 0 results
mysteriousSetupHook
}: stdenv.mkDerivation {
name = "foo";
nativeBuildInputs = [
impostorPkg
mysteriousSetupHook
];
installPhase = ''
# ?????
impostor-pkg \
--flub-with-grubs \
$src $out
'';
}) { })
];
}
All of this implicit magic is extremely confusing to new Nix(OS) users! Luckily, you usually don't need overlays. Let us walk through your options.
Attentive readers might notice the lack of override and overrideAttrs in these overlay examples, two very important functions for modifying derivations in Nix.
You might then ask yourself: "OK, overlays are bad. But if I shouldn't use overlays, how do I override?
Just as you did before! You can write overrides anywhere, not just in overlays! That's right, override and overrideAttrs work everywhere, they are intrinsic features of a derivation.
environment.systemPackages = [
(pkgs.foo.override ... )
];
programs.bar = {
enable = true;
package = pkgs.bar.overrideAttrs ...
...
};
Wherever Nix allows you to place a derivation, you can also place an overridden derivation, and that's huge.
So that's one-off overrides taken care of, and where overlays are mostly used. But then there's overlaying in the name of simplicity:
"if I add this to a nixpkgs overlay, it'll be available across my entire configuration". For this purpose, I strongly recommend specialArgs.
I love specialArgs. I am not seeing them nearly enough in people's configs. You should
learn to love specialArgs, too. If you want to make your NixOS configuration more idiomatic, elegant and DRY instantly
there's 3 things you should be littering all your config with: custom options, custom derivations, and specialArgs. Here
follows how to use the latter of the 3.
./flake.nix
nixosConfigurations.bobs-pc = let
system = "x86_64-linux";
in nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./foo.nix
./hyprpaper.nix
# inlined for brevity
({ pkgs, weirdlib, ... }: {
environment.systemPackages = [
(pkgs.callPackage
./weird-hello.nix {
inherit weirdlib;
}
)
];
})
...
];
specialArgs = {
specialFooPackages =
inputs.foo.packages.${system};
inherit (inputs) wallpapers;
weirdlib = {
inherit (inputs.weird)
mkWeirdPackage
mkWeirdApplication
;
};
};
};
Note:
A very common pattern is putting the entire 'inputs' attrset
into 'specialArgs', i.e. specialArgs = { inherit inputs; };.
That's probably what you should do in real code instead of this,
but it's done this way for demonstration purposes
(and to show that `specialArgs.inputs` is *just* an attribute
like any other!)
./foo.nix
{ specialFooPkgs, ... }: {
programs.foo = {
enable = true;
package = specialFooPkgs.foo;
};
}
./hyprpaper.nix
{ lib, wallpapers, ... }: {
services.hyprpaper = {
enable = true;
settings = {
splash = lib.mkDefault false;
wallpaper = [
"DP-2,${wallpapers.nkk7wq}"
"DP-1,${wallpapers.9djdkw}"
];
};
};
}
./weird-hello.nix
{ weirdlib
, fetchFromGitHub
, symlinkJoin
}: weirdlib.mkWeirdPackage {
pname = "hello-weird";
version = "0.1.0";
src = fetchFromGitHub ...
...
}
Notice we're still pulling in custom stuff from outside of our configuration and being just as ergonomic as an overlay,
but now the custom stuff is easily traceable, and easily recognized as foreign attributes,
as we can see it clearly written in the module function args - no more lies, only honesty.
And If you're ever unsure of what a specialArg is or what it does, it's trivial to open up the flake.nix and follow
the breadcrumb trail.
No. There are 3 instances where they actually make sense:
- Overriding driver packages & similar. A large majority of NixOS modules are written such that they allow
you to set a custom
.package option. Certain modules aren't, for example services.xserver.videoDrivers = [ ... ].
Notice the lack of entrypoint to set your own package here, even if some individual drivers do have entrypoints
elsewhere (e.g. hardware.nvidia.package).
For modules like these, you have no other option than to use an overlay (though sometimes the lack of entrypoint is due to
an incompetent module author, in which case make your own PR fixing that!)
- Overriding very deep & wide dependencies. There are certain
pkgs members which are so immensely
base that it would be ridiculous to try and override all of the time that they're consumed by other packages
(think stdenv' = stdenv.override { ... }). In cases like these, overlays make sense though you should
think twice about making such base overrides!
- Temporary ad-hoc code. As long as you are aware of the code being a dirty hack to get something working quickly,
I think it's fine to temporarily add an overlay to tinker with
pkgs. Just be prepared for the resulting tech debt.
In short, overlays have their niche use cases, but they are widely overused today.
Together, let's stop lying and start telling the truth again.
<<< back to emanueljg.com ^ Top
email: emanueljohnsongodin@gmail.com