Last year, Jade posted Pinning NixOS with npins, or how to kill channels forever without flakes. In short, what we want is:
- Get rid of channels (as in
nix-channel
, and not any other overloaded meanings of that word) as a source of impurity. - Have one blessed Nixpkgs pinned with
npins
being used for the system's configuration as well as all<nixpkgs>
invocations system-wide. - The solution should behave like
nix-channel
: When runningnpins update
–and then deploying the configuration–, the entire system should switch over to using the updated Nixpkgs.- The currently used Nixpkgs being tied to the current configuration is the key feature here. This means that they always stay in sync, even across rollbacks—unlike
nix-channel
.
- The currently used Nixpkgs being tied to the current configuration is the key feature here. This means that they always stay in sync, even across rollbacks—unlike
- The NixOS configuration should be built with the currently pinned Nixpkgs, and not the current system's Nixpkgs. (Otherwise deployments would take two iterations to converge.)
A naive solution might simply export NIX_PATH=nixpkgs=${pkgs.path}
in the system configuration.
This, however, comes with a show-stopping drawback: Due to the way environment variables propagate, any updates to it would not propagate until the next relog or reboot.
The common solution to this is, as usual in this industry, adding a layer of indirection.
For that, Jade's post cleverly uses flake infrastructure:
By writing an entry that points the nixpkgs
"flake" to the local store path of our nixpkgs in the Flake registry at /etc/nix/registry.json
, and then simply setting NIX_PATH=nixpkgs=flake:nixpkgs
.
This works, but it comes with downsides.
First of all, it requires the nix-command
and flakes
experimental features, and the goal of the exercise is to avoid flakes completely.
But more importantly, the flake registry suffers from some of the same issues that plagued us with nix-channel
in the first time (albeit they are not surfacing in this usage scenario), and therefore the flake registry is on its way out as well.
There is a much simpler solution to using the flake registry: A plain old symlink.
It's only five lines
We define that /etc/nixos/nixpkgs
now is a symlink to our current Nixpkgs (any other path will do, but I liked this one) and simply hardcode that path in NIX_PATH
forever.
{
nix.channel.enable = false;
nix.nixPath = [ "nixpkgs=/etc/nixos/nixpkgs" ];
environment.etc = {
"nixos/nixpkgs".source = builtins.storePath pkgs.path;
};
}
Now, with this setup all ad-hoc shells should already work, however when rebuilding your system configuration after bumping the pins it would still take the current Nixpkgs from /etc/nixos/nixpkgs
instead of the freshly bumped one.
This means that any updates would take two rebuilds to fully propagate.
(Note: This mainly applies to nixos-rebuild
, YMMV when using different deployment tools. Some of them allow doing the right thing and using a pinned Nixpkgs instead of <nixpkgs>
out of the box.)
To fix this, we must override the Nix path when rebuilding. direnv and a Nix shell to our rescue:
let
pkgs = import (import ./npins).nixpkgs { };
in
pkgs.mkShell {
nativeBuildInputs = with pkgs; [
# I recommend also pinning all the local deployment tools while we're at it
nix
home-manager
git
npins
openssh
colmena
];
shellHook = ''
export NIX_PATH="nixpkgs=builtins.storePath pkgs.path"
'';
}
Sneak peek: If you prefer a minimalistic setup with only direnv and no
shell.nix
, in a future release ofnpins
the following will also work:
Note that the shell env approach requires to always nixos-rebuild
from that folder, in order to have the path override activated.
This isn't a big deal for me as it has always been part of the workflow for various reasons, but if this itches you an alternative solution is to patch nixos-rebuild
itself:
[
(self: super: {
nixos-rebuild = super.nixos-rebuild.overrideAttrs (old: {
nativeBuildInputs = old.nativeBuildInputs or [] ++ [ self.makeWrapper ];
postInstall = old.postInstall or "" + ''
wrapProgram $out/bin/nixos-rebuild --set NIX_PATH nixpkgs=/etc/nixos/nixpkgs:nixos-config=/path/to/my/configuration.nix
'';
});
})
]
Conclusion
This approach is not free of gotchas, but neither are flakes or nix-channel
.
In the future, I'd like to see a replacement for nix-channel
that makes such a use case a first-class citizen (maybe leveraging profiles?) instead of having to put a symlink somewhere in /etc
.
The long-term goal is to have feature parity between flakes and third-party tools.