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. What is unique here is the way that nixos-rebuild is invoked and how that ties to the way I "kill" "channels" on my NixOs system

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.

First there's npins. It is a very simple source manager for nix, and usage is quite simple. It operates similar to the system channels. You have to perform updates manually, the great thing here is that it supports many more input sources and options. Last very important thing is that each source is saved in sources.json and thus can be edited and tracked by git, allowing for transparent and clear version pinning and rollbacks.

A nixpkgs channel (Hydra release basically, but I'm not gonna get into that right now) can be added as a source and tracked with a single command. Take a look at the npins repo to check out all the options.

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