These days, it is fashionable to have the Nixpkgs for your NixOS system managed alongside its configuration, pinned by some tool of choice.
In Pinning NixOS with npins, this time without flakes for real, I've talked about how I want the same Nixpkgs to be used for both my system configuration as well as all my ad-hoc nix-shell
s.
However, there is a key distinction between any import <nixpkgs> {}
used in the wild and the pkgs
provided within the NixOS module system: Only the latter has its overlays applied.
I'm a heavy user of overlays, and this discrepancy has bitten me more than once. For example, if I pin a certain version of a tool used for a service, and then do nix-shell -p that-tool
to do some maintenance on the service, I suddenly have a version mismatch.
The reason for this dilemma is that NIX_PATH
contains, well, a path, and that overlays are configuration for a Nixpkgs instance: import <nixpkgs> { overlays = …; }
.
Evaluating pkgs.path
will yield the original path from <nixpkgs>
, with any applied configuration erased.
Spray'n'pray: Using global overlays
Nixpkgs has a slightly niche feature:
It can automatically load overlays impurely from the environment at instantiation time.
This is implemented in <nixpkgs>/pkgs/top-level/impure.nix
:
39 overlays ? let
40 isDir = path: builtins.pathExists (path + "/.");
41 pathOverlays = try (toString <nixpkgs-overlays>) "";
42 homeOverlaysFile = homeDir + "/.config/nixpkgs/overlays.nix";
43 homeOverlaysDir = homeDir + "/.config/nixpkgs/overlays";
as we can see here, it will source and automatically apply overlays from the following locations:
<nixpkgs-overlays>
, set by puttingnixpkgs-overlays=/path/to/my-overlay.nix
intoNIX_PATH
,~/.config/nixpkgs/overlays.nix
,- and all files in
~/.config/nixpkgs/overlays/*
(n.b.: hard-coding~/.config
is not XDG basedir spec compliant!).
This means that if any of these provide any overlays, then any call to import <nixpkgs> {}
will behave as if overlays = [ … ]
had been passed to it.
Equipped with this, we can simply have:
{
channel.enable = false;
nix.nixPath = [
"nixpkgs-overlays=/etc/nixos/nixpkgs-overlays"
];
environment.etc = {
"nixos/nixpkgs-overlays".source = put path to overlays here;
};
}
I decided against putting my overlay into ~/.config/nixpkgs/overlays.nix
, because I wanted a truly global solution; I'm afraid things could break pre-login when /home
is still encrypted.
If Nixpkgs implemented the full XDG basedir specification, then we'd be able to simply put our overlay somewhere into /etc/xdg
and wouldn't have to bother with also setting NIX_PATH
.
With this solution, the overlay will be applied to all Nixpkgs instances, not only those of our system's Nixpkgs as found via import <nixpkgs>
or behind the scenes in nix-shell -p
.
Notably, this will also apply to any pinned dev shell à la import sources.pkgs {}
, no matter how old or new it is.
Impurely meddling with dev environments is a recipe for confusing error messages, and likely to fail:
Overlays are typically written to work against a specific Nixpkgs version, and trying to apply it to some very old pinned Nixpkgs pin will likely throw on a missing attribute.
This can be a desired outcome, and can work with an overlay carefully designed for such an environment. However, in many other cases it is desirable to only override Nixpkgs instances that aren't explicitly pinned by their own tooling.
A more selective approach: applyPatches
So, NIX_PATH
wants a path. Let's give it one.
The plan is to patch our system's Nixpkgs so that it will automatically apply a specific overlay when imported.
Other Nixpkgs instances, pinned from other sources, will be unaffected.
We can simply inject ourselves into Nixpkgs's default.nix
entry point, like this:
- import ./pkgs/top-level/impure.nix
+ args: (import ./pkgs/top-level/impure.nix) (args // { overlays = [ @PUT_OVERLAY_HERE@ ] ++ args.overlays or []; })
Applying the patch is slightly involved but straightforward.
We need an unpatched Nixpkgs instance for bootstrapping, for accessing applyPatches
and other library functions.
{
pkgsPath = pkgsBootstrap.applyPatches {
src = sources.pkgs; # Nixpkgs pin here
patches = [
(
# Replace `${./overlay.nix}` with the correct path in there
pkgsBootstrap.writeText "overlays.patch" ''
diff --git a/default.nix b/default.nix
index bdab048245e2..617d15b6bfd5 100644
--- a/default.nix
+++ b/default.nix
@@ -27,4 +27,4 @@ if !builtins ? nixVersion || builtins.compareVersions requiredVersion builtins.n
" "else
- import ./pkgs/top-level/impure.nix
+ args: (import ./pkgs/top-level/impure.nix) (args // { overlays = [ ./overlay.nix ] ++ args.overlays or []; })
''
)
];
};
}
Side note: This is why one does not put patches inline in Nix code. All lines of code that start with a space will be fucked up by the editor any time the indentation changes, moreover the
patches
list wants paths anyways so it requires wrapping inwriteText
. The${" "}
in the string is a hack to work around the editor eating that one whitespace. Putting the patch in a file with@SUBSTITUTIONS@
for the overlay path is the way to go here.
This solution still has a kink that needs to be ironed out:
As it is, the provided overlay.nix
would have to be self-contained, i.e. not import any other Nix code.
Often times, this is not the case:
A system configuration's overlay may import packages from npins or locally vendored packages.
One solution is to put the entire configuration surrounding our overlay into the store (this is what flakes do), but this means that the source path changes any time any bit of configuration is touched, even if it has nothing to do with the overlay.
Instead, pkgs.lib.fileset
offers some functionality to only include the files we care about:
{
overlaySource = pkgsBootstrap.lib.fileset.toSource {
root = ./.;
fileset = pkgsBootstrap.lib.fileset.unions [
./overlay.nix
# Add all files/folders that the overlay imports here
./npins
./pkgs
];
};
}
In the patch, we then simply replace ${overlay.nix}
with ${overlaySource}/overlay.nix
.
Conclusion
If you have an overlay that you want to apply to all Nixpkgs instances, use global overlays. If you have an overlay that you want to globally apply to a specific Nixpkgs instance, apply the overlay as a patch instead.
I'm honestly unsure if I'd recommend this approach to anybody else. It works for me and solves this very specific issue I have, though I can imagine most people don't care much about it.