Pinning nixpkgs for faster nix shells

Posted on Saturday, Mar 25, 2023
Last updated Sunday, Mar 26, 2023

I use Nix Flakes to define my NixOS and nix-darwin system configurations, including home-manager configs for my dotfiles.

One of the cool things about flakes is the "flake registry", which lets you refer to popular flake sources by a short name. For example, you can run nix shell nixpkgs#vim to open a new shell with vim in the path, which uses the registered short name nixpkgs instead of the full flake URL github:NixOS/nixpkgs/nixpkgs-unstable.

This is cool, but because the nixpkgs flake tracks nixpkgs-unstable, it gets updated super frequently and is pretty much always ahead of the revision I used to build the system config. This means that every time I run nix shell, it needs to download a ~30 MB tarball of the current nixpkgs revision, and depending on what I'm installing it may need to download a bunch of updated dependencies for things that have had version bumps since I last updated my system.

It turns out that you can add your own entries to the flake registry, and "user" and "system" entries take precedence over the global default entries. So if you register your own nixpkgs flake, it will use whatever URL you registered when running nix shell nixpkgs#whatever.

New nixy approach

Thanks to a kind soul on Mastodon, I learned that there's a NixOS config option to set the flake registry entry.

Assuming that you have a flake input named nixpkgs in scope, all you need to do is set nix.registry.nixpkgs.flake = nixpkgs;. After building the config, you can run nix registry list, and you should see something like this:

system flake:nixpkgs path:/nix/store/vj10h6bzy9fldd1a4p917h0kjx7gr4sz-source?lastModified=1679172431&narHash=sha256-XEh5gIt5otaUbEAPUY5DILUTyWe1goAyeqQtmwaFPyI=&rev=1603d11595a232205f03d46e635d919d1e1ec5b9
global flake:agda github:agda/agda

etc...

Running nix shell nixpkgs#whatever should now be much snappier, since you'll already have the nixpkgs derivation and a bunch of system dependencies in your nix store.

Old janky approach

Below is the hacky imperative approach I took initially, preserved for posterity or whatever :)

I added a little script to pin the nixpkgs revision in my local flake registry to the revision used to build my system config. The script just runs this commmand:

nix registry add nixpkgs "github:NixOS/nixpkgs/$(jq -r '.nodes.nixpkgs.locked.rev' flake.lock)"

This uses jq to parse out the revision of nixpkgs from my flake.lock file, and adds a registry entry for nixpkgs with the URL set to that specific revision. Note that the command assumes that you're running it in the directory containing the flake.lock file.

I recently started managing my "housekeeping" tasks with just, a command runner that supports dependencies between build tasks. This makes it easy to run the pin-nixpkgs task after another task completes.

The relevant bit of my justfile looks like this:

# store the hostname of the current machine in a variable, removing anything after the first '.'
hostname := `hostname | cut -d '.' -f 1`

# Build the NixOS configuration and switch to it. Also pins nixpkgs to rev in flake.lock
[linux]
switch target_host=hostname: && pin-nixpkgs
  sudo nixos-rebuild switch --flake .#

# Pin the revision of nixpkgs in the local flake registry to the rev from flake.lock
pin-nixpkgs:
  nix registry add nixpkgs "github:NixOS/nixpkgs/$(jq -r '.nodes.nixpkgs.locked.rev' flake.lock)"

So now when I run just switch to switch to a new system config, it will automatically update my local flake registry to point to the nixpkgs revision that was used to build the config.

Now when I run nix shell nixpkgs#thing, the nix store already has the derivation of nixpkgs itself, so there's no need to download a big tarball, and there's a good chance that I've already got most of the dependencies for thing in the store also. This makes the whole thing much snappier and cuts down on a lot of wasted disk space due to duplicate dependencies.