syncing overlays in nixos and nixpkgs


In the last post I described a few basic NixOS config tricks that I use to keep things simple and easy to understand, like keeping nixpkgs and my NixOS config in git repos in my home directory.

This one describes how to add overlays to that setup. For basic info about overlays, see the manual or wiki page.

Goal ¶

As always with Nix, there are many, many ways to accomplish a task. Those links above describe several different ways to configure overlays. To choose between them, I’ll add these (reasonable) requirements:

  • I want a directory that I can drop overlays into without changing a list in some other file.
  • I want overlays to apply consistently when using NixOS and regular Nix tools (when using <nixpkgs>, not when using a pinned nixpkgs).

The second requirement removes any confusion that might result if overlays were applied in my NixOS config but not when using other Nix tools. Note that I don’t want overlays applied in development environments with pinned nixpkgs, because those are supposed to be completely reproducible, for different users, at different times, without reference to the system they’re on.

Solution ¶

The overlays wiki page describes one way of doing this, but I ended up with a slightly simpler (to me) solution. It looks like this:

{ config, pkgs, ... }:
let
  localNixpkgs = "/home/dnr/src/nixpkgs";
  localConfig = "/home/dnr/src/my-nix-config"; # directory containing this file
  localOverlays = localConfig + "/overlays";
in
{
  # nixPath setting:
  nix.nixPath = [
    # Use a local git clone for nixpkgs (instead of a link managed by nix-channel):
    "nixpkgs=${localNixpkgs}"
    # Use this directory for the nixos config (instead of /etc/...):
    "nixos-config=${localConfig}"
    # Instruct nix tools to load overlays from this directory:
    "nixpkgs-overlays=${localOverlays}"
  ];

  # Note that nixos-rebuild will not load overlays from <nixpkgs-overlays>
  # in NIX_PATH. We have to set them explicitly as nixpkgs.overlays.
  # We copy a little code from nixpkgs to import files from a directory.
  nixpkgs.overlays = with builtins;
    map (n: import (localOverlays + "/" + n))
        (filter (n: match ".*\\.nix" n != null)
                (attrNames (readDir localOverlays)));
}

Just those lines in your NixOS configuration should do the trick. The nixpkgs-overlays setting in nixPath applies to all Nix tools that use NIX_PATH, except for nixos-rebuild, which doesn’t load overlays from NIX_PATH.1 So then we explictly set nixpkgs.overlays. It has to be set to an actual list of Nix expressions though, not a directory name, so we need a little code to load all the *.nix files in a directory. There’s some code in nixpkgs that does the trick, and I’ve copied it here.

In a way, this is the reverse of the idea in the wiki: that one lets nixpkgs load overlays from your nixos configuration using a little shim, while mine has nixpkgs load overlays normally, and has nixos take them from there.

Note that development environments with pinned nixpkgs bypass all this, since they don’t use NIX_PATH.

Why ¶

Why bother with this somewhat obscure feature of Nix? Overlays are crazy-powerful! You can make a local patch or change how some package in NixOS is built, and your change becomes part of the OS and is fully integrated as if it was there in the official version.

Suppose I wanted to add a patch to my window manager (I actually had to do this to fix a bug). I can drop in a patch file and an overlay like this:

self: super: {
  notion = super.notion.overrideAttrs (old: {
    patches = [ ./notion-timer.patch ];
  });
}

Now that package is always built with that patch, anywhere on my system. Only the patch is added, nothing else is changed. So if the package or dependencies are upgraded, or build flags changed, my patch will still be applied on top. (And if the patch fails to apply, it will break my next system rebuild until I fix it.)

Imagine trying to do that on Ubuntu: you could… grab the source package, install all the dependencies, apply your patch, and build it into a .deb and manually install it on a system. But then what happens if the package is upgraded in Ubuntu? An apt upgrade will replace your version. The “right way” to do this would involve setting up a local apt repository, building a patched version after every upstream release (with some kind of CI? I’m not sure), and ensuring your version has a higher priority so your system will pick that one. It’s possible, but the Nix version is far far simpler and easier.

However, this illustrates one of the downsides of the Nix model: note that I said the package is built with my patch, not downloaded from the main Nix binary cache. Of course it has to be: the Nix CI system can’t know about my patch. But remember that a change in a dependency causes all the dependents to be rebuilt. If you were to add a patch to glibc or systemd, well, then you would have to locally build just about every package in the system.2 So this works great for leaves in the dependency tree, but can be impractical for changing core libraries.


  1. The wiki says it does if it’s a directory, but that doesn’t seem to be true. If you trace through the code, it doesn’t look it should work: nixos/modules/misc/nixpkgs.nix explicitly passes a value for overlays, so all the logic in pkgs/top-level/impure.nix to load overlays as a default value is bypassed. That seems like the right choice for nixos-rebuild in my opinion, but it does require a little code when you want to sync things up. ↩︎

  2. If you wanted to do this for real, you could set up your own instance of the Nix CI system (hydra) that builds everything with your overlays on a large build cluster and pushes them to a private binary cache. ↩︎