emanueljg.com

Stop lying about pkgs!


2025-08-08 | Nix

1. What are overlays? Why are they bad?

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: 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: 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.

2. What should I replace my overlays with?

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.

3. Does that mean I should never use overlays?

No. There are 3 instances where they actually make sense:
  1. 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!)
  2. 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!
  3. 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