Bootstrapping Nix channels in NixOS
2021-06-19
category: tech
A few years after starting to use NixOS, I realized that the experience of reproducing my setup on a new machine has become harder, not easier, especially after setting up encryption and opt-in state. This is rather embarrassing, so here’s my first step in remedying that.
My NixOS configuration currently depends on multiple
channels (<nixos-20.09>
,
<nixos-unstable>
,
<nixpkgs-unstable>
, and home-manager):
https://channels.nixos.org/nixos-20.09/nixexprs.tar.xz nixos
https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz unstable
https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz actual-unstable
https://github.com/nix-community/home-manager/archive/release-20.09.tar.gz home-manager
What’s painful about this is that on a clean install the
various nix channels aren’t present on my machine, and I
need to manually set up the required Nix channels for the
root user via the nix-channel
command or by
writing to /root/.nix-channels
. I always forget
to do this, and get a variant of the following error:
nix-repl> <foo>
error: file 'foo' was not found in the Nix search path (add it using $NIX_PATH or -I), at (string):1:1
The recommended fix I usually hear is to avoid using Nix channels entirely and pin the various nixpkgs in configuration.nix. However, I don’t want to do this. It’s painful to have to update the pin every so often, and my nixpkgs lags behind Nix channels so I get a lot of binary cache misses and end up building many packages from source, making the experience not unlike Gentoo.
On the other hand, doing
builtins.fetchTarball
every time also isn’t
ideal. Who wants to require an Internet connect every time
you need to change the font size for your decleratively
configured terminal emulator? If we sacrifice just a bit of
determinism (but not too much) we can do better.
In the example above, when the import path
foo
doesn’t exist, evaluation of
<foo>
results in a plain old exception
which can be caught with builtins.tryEval
:
let
resolvedPath = builtins.tryEval <nixos-unstable>;
in
if resolvedNixPath.success then
resolvedNixPath.valueelse
builtins.fetchTarball "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
By checking if evaluation of the Nix import path threw an
exception or not, we can determine if a particular
import <foo>
succeeded or not. We can
then import nixpkgs normally if the import succeeds, or fall
back to fetching a tarball and importing from there. This
can be further extended to automatically create
.nix-channels
files for root and home-manager
users:
let
resolve = nixPath: defaultUrl:
let
resolvedNixPath = builtins.tryEval nixPath;
in
if resolvedNixPath.success then
resolvedNixPath.valueelse
builtins.trace
"WARNING: could not resolve path, defaulting to '${defaultUrl}'"
(builtins.fetchTarball defaultUrl);
channelUrl = channel: "https://channels.nixos.org/${channel}/nixexprs.tar.xz";
version = "20.09";
channels = [
{
importPath = <nixos>;
name = "nixos";
url = channelUrl "nixos-${version}";
}
{
importPath = <unstable>;
name = "unstable";
url = channelUrl "nixos-unstable";
}
{
importPath = <actual-unstable>;
name = "actual-unstable";
url = channelUrl "nixpkgs-unstable";
}
];
allNixpkgs =
builtins.listToAttrs (
builtins.map (
{ importPath, name, url }:
{
inherit name;
value = import (resolve importPath url) {};
}
) channels
);
in
allNixpkgs// rec {
nix-channels = allNixpkgs.nixos.writeTextFile {
name = ".nix-channels";
text =
builtins.concatStringsSep "\n"
(builtins.map ({ name, url, ... }: "${url} ${name}") channels);
};
home-manager-config = { pkgs, lib, ... }: {
home.file.".nix-channels".source = nix-channels;
};
nixos-config = { ... }: {
systemd.tmpfiles.rules = [
# unlike "L", "L+" will remove the existing file if it exists and replace
# it a symlink
"L+ /root/.nix-channels - - - - ${nix-channels}"
];
};
}
where importing home-manager-config
for
home-manager and nixos-config
for root users
will just work.
Upgrading the NixOS version just works as well; simply
bumping version
and running
sudo nixos-rebuild switch --upgrade
twice will
do it, where the first nixos-rebuild switch
updates the .nix-channel
file and the second
nixos-rebuild switch
does the actual
upgrading.
I’m pretty happy that I no longer have to think (too
hard) about this part of the clean install process anymore.
Unfortunately, since I use VirtualBox
with the extension pack enabled, every time I
nixos-rebuild switch --upgrade
I still get the
Gentoo experience anyway :cry:.
index | atom/rss | generated off rev d866987