cakeforcat

NixOS, channels, flakes and my take on a fully reproducible config

Intro

If you had a chance to stumble upon my NixOS configuration repository on GitHub, you might have read deep enough to find holy-mother-of-scripts.fish. Every NixOS user has to write their own deployment script at some point and mine started ballooning pretty quickly. The primary goal of course is to perform nixos-rebuild and track the config using git. You can then add extra stuff like committing only on a successful build, pushing, cleaning etc. It quickly becomes a NixOS update all-in-one solution.

Eventually the problem of full reproducibility started staring me right in the face. I have my config, I commit the changes sure, but what about the pesky sudo nix-channel --update? How can you track that? It seems to be a one way street. If you follow the official guides, you're told to just keep your stable channel up to date and don't worry about it. This has some obvious problems (pinning, rollbacks?), but what do we do about it? what even are channels??

Channels, Hydra, flakes?

Let's roll back a little and define what is needed to fully define a single nixos-rebuild execution. There's always 2 parts:

  • Nixpkgs i.e. your input. The exact version, branch, fork, whatever. You need a nixpkgs store to even have nix in the first place
  • your nix configuration. Declaration of your system and package derivation. How nixos-rebuild will derive your system from nixpkgs
The need for tracking and pinning the first part (nixpkgs) has been around for a while. This can get a bit confusing when the concept of channels is introduced. This is because nix channels can be understood as 3 very different and distinct things.
  • Channel branch of nixpkgs
  • Channel output of a Hydra job
  • the nix-channel command
The first 2 are related but can be somewhat distinct (especially when talking about individual commits vs successful Hydra jobs). The third is completely different. For more info about this I'd suggest having a look here. In *very* short terms:

Hydra is a CI/CD build tool for nix/nixpkgs. The nixos-unstable branch of nixpkgs contains the output of nixos:unstable:tested aggregate jobs (?), which basically is the master branch getting tested periodically and if the build succeeds it ends up in nixos-unstable. The bi-annual stability campaign for nixos-stable is a bit more complex, but both are basically Hydra tested branches of the nixpkgs repository with different frequencies/policies. You can track whichever one you want as your input.

Now for your derivation, you need some way of grabbing and maintaining one of these channel branches. There are a few different ways, but I present 3 of them here:

  • the nix-channel command (still the default)
  • flakes (bleh)
  • npins (cool)
My distaste for flakes aside, the default option is very limited. It is officially supported, but incredibly barebones. You can add multiple channels, they will be fetched and kept in a standard location for the entire system, and you can update/upgrade them. That's pretty much it. No way of rolling back, changing channels, tracking them etc.

The flakes topic is too big for this blog post. I don't like flakes as a feature, because they are doing too much all at once and make simple stuff unnecessarily complex. You like using flakes? Fantastic, I'm happy for you. Please let me keep not using them thanks.

To the point

So the concept is actually quite simple. Arriving here took a bit of digging and trial and error, as with all nix development, but here I am.

We start with npins as mentioned earlier. Self described as a tool for "Nix dependency pinning". It works as described, you can use the CLI to add dependencies including, but not limited to channels. They all get saved in sources.json and thus can be edited and tracked by git, allowing for transparent and clear version pinning and rollbacks.

Once you have an initialized npins folder with a nixpkgs pin there are only 2 components in your config/deployment script that do the magic. First is this nix code snippet I have saved as pinning.nix

{
  config,
  pkgs,
  ...
}:
{
  # kill channels
  nix = {
    channel.enable = false;
    nixPath = [
      "nixpkgs=/etc/nixos/nixpkgs"
      "nixos-config=/home/USERNAME/nixos-config/configuration.nix"
    ];
  };
  environment.etc = {
    "nixos/nixpkgs".source = builtins.storePath pkgs.path;
  };
  # command-not-found fix
  programs.command-not-found.dbPath = "/etc/nixos/nixpkgs/programs.sqlite";
}
First off channel.enable = false; disables the nixos channels feature. Then the nixPath is configured to point to some arbitrary 2 locations. nixpkgs points it to a new location to save the current gen of nixpkgs (our channel essentially) and nixos-config just helps us with not having to deal with /etc/nixos/. Important bit is happening at environment.etc = { "nixos/nixpkgs".source = builtins.storePath pkgs.path; }; where the actual source of nixpkgs (store path) gets linked to the location from above. You might ask at this point, ok but where is the store path resolution happening? This is done every time we run nixos-rebuild using holy-mother-of-scripts.fish. Specifically these 2 lines
set -l nixpkgs_path (nix-instantiate --json --eval npins/default.nix -A nixpkgs.outPath | jq -r .)
sudo nixos-rebuild $rebuild_type -I nixos-config=/home/USERNAME/nixos-config/configuration.nix -I nixpkgs=$nixpkgs_path --show-trace 2>&1 | tee rebuild.log
Here, nix-instantiate (modern npins has a built in command for it but I cba) is used to extract the store location of the current nixpkgs as pinned by npins. Then nixos-rebuild is run with the -I options to force the nixpkgs and config locations. That's it. The nix config code earlier makes sure the store is linked to our known location meaning everything else that needs it can find it easily.

Extras, sources, acknowledgments, idk everything else

So this is a result of some digging around and I think it's important how I actually arrived here. I'm by no means a computer scientist, software engineer or a sysadmin. I'm a hardware engineer who learned about compiler pre-processors while writing SystemVerilog. If anyone else is particularly autistic about NixOS the way I am, but struggles with the whole math and nix language behind it, these blog posts are your only source of documentation. (bless the modern official wiki.nixos.org, but we still have a way to go).