Skip to main content

Overriding for Fun and Profit

·2883 words·14 mins
Nix Kepler Unrefined
Author
Jonas A. Hultén
Opinionated engineer who rambles semi-coherently about DevOps, type systems, math, music, keyboard layouts, security, and misc.

A lot has happened in the last… [checks notes]… five months. Nothing too alarming happened in the intervening time, right? Good. Good…

State of Kepler
#

Kepler has been coming along nicely. It’s still a far cry from the be-all, end-all configuration system I want, but it is way more than what it was when I wrote my last post.

For the sake of comparison, this was the state of the Kepler codebase then…

nix
├── home
│   ├── pkgs
│   │   └── vscodium
│   └── users
│       └── sldr.nix
├── sldr-meta
└── system
    ├── general.nix
    ├── legacy
    │   ├── default.nix
    │   └── <SYSTEMS REDACTED>
    ├── pkgs
    │   └── general.nix
    ├── services
    │   └── ssh.nix
    └── users
        ├── general.nix
        └── sldr.nix

… and this is now:

nix
├── home
│   ├── cfg
│   │   ├── editorconfig.nix
│   │   └── xfce.nix
│   ├── default.nix
│   ├── pkgs
│   │   ├── audio
│   │   │   └── default.nix
│   │   ├── default.nix
│   │   ├── dev
│   │   │   ├── default.nix
│   │   │   └── vscodium
│   │   │       └── default.nix
│   │   ├── fonts
│   │   │   └── default.nix
│   │   ├── git
│   │   │   └── default.nix
│   │   ├── graphics
│   │   │   └── default.nix
│   │   ├── localbin
│   │   │   ├── bond.sh
│   │   │   ├── default.nix
│   │   │   └── xfix.sh
│   │   ├── net
│   │   │   ├── default.nix
│   │   │   ├── firefox
│   │   │   │   └── default.nix
│   │   │   └── tor
│   │   │       └── default.nix
│   │   ├── office
│   │   │   └── default.nix
│   │   ├── pass
│   │   │   └── default.nix
│   │   ├── ranger
│   │   │   └── default.nix
│   │   ├── shell
│   │   │   ├── bash
│   │   │   │   ├── ansi.nix
│   │   │   │   ├── bash.nix
│   │   │   │   ├── default.nix
│   │   │   │   ├── ssh-agent-connect.sh
│   │   │   │   └── util.sh
│   │   │   └── default.nix
│   │   ├── social
│   │   │   └── default.nix
│   │   ├── ssh
│   │   │   └── default.nix
│   │   ├── taskwarrior
│   │   │   └── default.nix
│   │   └── wezterm
│   │       ├── config.lua
│   │       └── default.nix
│   └── users
│       └── sldr.nix
├── sldr-meta
├── system
│   ├── default.nix
│   ├── general.nix
│   ├── gui.nix
│   ├── legacy
│   │   ├── base.nix
│   │   └── <SYSTEMS REDACTED>
│   ├── pkgs
│   │   └── default.nix
│   ├── services
│   │   ├── db
│   │   │   ├── default.nix
│   │   │   └── postgres.nix
│   │   ├── default.nix
│   │   ├── firewall.nix
│   │   ├── gitolite.nix
│   │   ├── nginx.nix
│   │   ├── printing
│   │   │   ├── default.nix
│   │   │   └── printing.nix
│   │   ├── purge.nix
│   │   ├── qmk.nix
│   │   ├── resolved.nix
│   │   ├── roundcube
│   │   │   ├── default.nix
│   │   │   └── plugins.nix
│   │   ├── ssh.nix
│   │   └── vpn.nix
│   ├── sops.nix
│   ├── sound.nix
│   ├── sudo.nix
│   └── users
│       ├── default.nix
│       ├── <OTHER USERS REDACTED>
│       └── sldr.nix
└── util
    ├── default.nix
    ├── ports.nix
    └── profile.nix

Put another way, it’s 2.4k vs. 8.1k lines of Nix.

The fact that I’m still using this, and haven’t given up, goes to show that NixOS is hella powerful and, once you wrap your head around its intricacies, it’s actually quite fun.

There are plenty of teaching moments on this journey which I should document at some point, but I wanted to start with the one I had to cobble together today: monkeypatching a package via overriding and dirty tricks.

Aside: Whatever happened to the rabbit trail?
#

In my last post, I got entirely sidelined by trying to enumerate the entire system config to find everywhere “kepler” showed up. It didn’t take long to realize that the problem I landed on — infinite recursion — isn’t something that can be solved. Nix is a lazily evaluated language. For those not in the know, that means that Nix won’t evaluate anything until you force it to — usually by trying to use the result of an evaluation somehow.

This feature allows you to do some truly cursed things. Let me give you an example.

x = 1 : x
This particular snippet is in Haskell, one of the “more popular” functional programming languages, which sounds like it’d be a tiny niche, but it’s somehow not. We’ll get back to Nix in a bit.

This little snippet defines a list, which we call x, of two elements (that’s what the : operator does). The first element is 1. The second element is x. Which is also the name of the list. Wait…

Yes, dear reader, this is a self-referential definition; the list x is defined in terms of itself. Most (non-lazy) programming languages would look at this and go “x isn’t defined, what are you on about” then halt and catch fire. But not Haskell (or other lazy languages) — there, this is completely legal, and a really useful way to define infinite lists.

Think about it: what’s the first element of x? 1, that’s easy enough to see. What’s the second element? Well, x, but that’s a list, so we need the first element of that, which is 1, as we just said. And so on — it’s an infinite list of all ones. However, it’s defined succinctly as x = 1 : x so it won’t cause a memory explosion by just being defined. Trying to ask what the last element of x is, though, will certainly crash.

Fun anecdote: when I was a baby 1st year computer science student first learning Haskell, we were tasked with writing an efficient exponentiation function (i.e. to solve \(x^y\) type equations). The goal was to solve \(2^{64}\) faster than the example solution — which, admittedly, was pretty dumb.

When I finally found a workable solution, I was amazed by how fast it was, and thought “I’ll see how it can handle bigger numbers” and fed it \(2^{2^{64}}\) — that’s two to the power of about \(1.8 \times 10^{19}\), or about 18 quintillion. My computer tried its utmost, for a little bit, then seized and became unresponsive. I had to hard-reboot it to be able to use it again.

Some back-of-the-envelope math later told me why. Storing \(2^{64}\) will require 64 bits, or 8 bytes — virtually nothing, even in the halcyon days of 2013. Storing \(2^{2^{64}}\), however, would require those 18 quintillion bits of storage or, via some more quick math, about 2.1 million terabytes of RAM. Haskell took all it could, then ran out, and took my system out along with itself.

Bringing it back to Nix, consider this:

a = rec {
    b = {
        c = a;
    };
};

The rec keyword says we’re defining a recursive set — spoiler, the NixOS config is one giant recursive set — where a contains b which contains c which is a which contains b… and so on, forever.

You see the problem? NixOS is littered with this kind of recursive definitions which work just fine because normally the configuration is evaluated lazily and only the parts we really care about. My idea of “just enumerate all the keys” will never work because there are infinitely many keys.

It’s turtles all the way down.

Okay, but, now what?
#

In part, I’ve gotten far better at navigating the Nix REPL, which in turn is due to me finally starting to understand how this system is put together. But also, I ran across nix-inspect, which is a helpful TUI tool written in Rust for navigating arbitrary Nix expressions. Having installed that has helped a whole bunch.

Let’s learn overriding
#

When I write tooling, I usually think in three modes: normal operation, force, and override. Override is usually the “all safeties off, best of luck, my dude” last-resort option that’s needed by either something internal or very specific, very dangerous administrative tasks. I was, as a result, a little hesitant to use Nix’s override functions — it felt like actively inviting danger.

Now, I wasn’t wrong, but it’s also turned out to be an incredibly powerful way to tweak packages without having to reproduce the whole package file, which is needlessly excessive when all you want to do is change a few lines.

For this explanation, I will use aome510’s spotify-player as my example, as it’s what I was banging away at earlier today when I first took a serious stab at overriding. This program is available in nixpkgs as pkgs.spotify-player but it has a nasty habit of trailing the latest release, which has proved problematic since Spotify seems hell-bent on gutting their API.

The way I used to deal with this was by cloning the entire package.nix file into Kepler, updating the fetchFromGithub call to get the latest version, and then calling my modified file specifically. All in all I had to modify a handful — six, or so — lines out of the whole file, which is silly inefficient, but it was the best idea I had until today.

Problem one: out-of-date Rust
#

This is actually another problem, which I ran into when updating to 0.20.1, but whatever version of Rust Nix was using via the rustPlatform.buildRustPackage call was too old to build the package. This is in part due to me refusing to run Kepler on the bleeding edge nixos-unstable branch, and in part because I probably hadn’t updated my flake inputs in a while.

Remember flakes? I’ve learned a whole bunch about those too since last time — topic for a future post.

Fortunately, since I recently decided I wanted to do Rust development again, I’d installed an overlay on Nixpkgs which let me get the latest and greatest Rust, all the time.

Overlays is another topic for a future post. Stay tuned.

Using the tooling provided by this overlay, I could construct a new rustPlatform via

rustPlatform = makeRustPlatform {
    cargo = rust-bin.stable.latest.minimal;
    rustc = rust-bin.stable.latest.minimal;
};

Originally, as said, I just jammed this into the package.nix file and called it a day, but it turns out this is one of the easiest things to override.

The documentation discusses a few ways one can override packages, but the first one — <pkg>.override — allows us to override the arguments passed to the package. That’s what we want, since we want to feed in a custom rustPlatform.

And sure enough, that’s as easy as defining a new package then installing that one.

let
    overridenSP = pkgs.spotify-player.override {
        rustPlatform = makeRustPlatform {
            cargo = rust-bin.stable.latest.minimal;
            rustc = rust-bin.stable.latest.minimal;
        };
    };
in
{
    environment.systemPackages = [ overridenSP ];
}
This isn’t how my audio.nix file looks, but it serves as a good explanation.

Having overridden the rustPlatform we can now be sure that we’re building with the latest Rust I have available on my system. But we still haven’t made sure that we’re building the latest version of the package itself.

Problem two: updating the package
#

This turned out to be a way hairier problem. The documentation helpfully points out that <pkg>.overrideAttrs is available if you want to override something that goes into making the derivation — Nix fancy talk which is way out of scope, just think of it as “package” — but that won’t cut it here.

The overrideAttrs function works by overriding the arguments passed to mkDerivation, but that isn’t what we’re doing in our package. We’re using buildRustPackage which wraps mkDerivation, but it does a bunch of stuff first.

This forum post discusses off and on how one can override a Rust package in this way. It specifically points out that if we want to use overrideAttrs we have to look at what buildRustPackage does with its inputs before handing them to mkDerivation. That seemed like a promising, if disgustingly complicated method of doing it but — cutting to the chase — I couldn’t get it to work.

The problem is the cargoHash argument, which locks the Cargo dependencies of the package. It’s unique to buildRustPackage, and while it is passed to mkDerivation in a convoluted fashion, I couldn’t override it in a way that actually worked — my guess is that it does some processing on the value before it’s handed to mkDerivation and this processing was failing.

The blog post then takes a hard turn and starts suggesting writing overlays for replacing the package — I tried that too, but it was more effort than one single package was worth.

So, what do? Override harder.

Yo dawg, I heard you like overriding
#

Here’s the solution I eventually put together:

let
  spotifyPlayer =
    with pkgs;
    spotify-player.override (
      let
        rustPlatform' = makeRustPlatform {
          cargo = rust-bin.stable.latest.minimal;
          rustc = rust-bin.stable.latest.minimal;
        };
      in
      {
        rustPlatform = rustPlatform' // {
          buildRustPackage =
            args:
            rustPlatform'.buildRustPackage (
              args
              // rec {
                version = "0.20.3";
                src = fetchFromGitHub {
                  owner = "aome510";
                  repo = args.pname;
                  rev = "refs/tags/v${version}";
                  hash = "sha256-9iXsZod1aLdCQYUKBjdRayQfRUz770Xw3/M85Rp/OCw=";
                };
                cargoHash = "sha256-e9MAq31FTmukHjP5VoAuHRGf28vX9aPUWjOFfH9uY9g=";
              }
            );
        };
      }
    );
in

Don’t worry, we’re going to walk through it.

First of all, take a deep breath. Remember that we are developers. The power is ours to wield.

Now, look at the top. We’re “just” using the override function, not overrideArgs. So we’re technically just fiddling with the package inputs, but we’ll see that that is plenty enough to achieve what we want.

We then construct rustPlatform' just as we did above, but we’re not feeding it directly into the override arguments. Instead, we’re constructing it in a let-in block, since we’ll be using it a lot in what happens next.

Nix does allow nested let-in blocks like this which is both incredibly useful and powerful and can be incredibly confusing when one’s trying to decipher someone else’s code. Remember that I’m writing these posts in equal part so that I will remember, six months from now, what the 💣 I was thinking.

Then we open the set that we’re going to pass as the argument to the override function. Initially, it looks good.

rustPlatform = rustPlatform'

So far, we’re just overriding the rustPlatform with the one we just constructed. It’s after that it gets weird.

All sets, all the time
#

The // operator is for attribute set merging, i.e.

{ foo = 1; } // { bar = 2; } == { foo = 1; bar = 2; }

It’s great power is that if the right side set defines an attribute that’s already in the left side set, it takes precedence.

{ foo = 1; } // { foo = 2; } == { foo = 2; }

That’s the level of shenanigans we get up to here. rustPlatform is just an attribute set which contains a bunch of cool things, including buildRustPackage. Would be a shame if something were to happen to it…

rustPlatform = rustPlatform' // {
    buildRustPackage = ...

Oops. Yes, we’re making our own buildRustPackage. Now, I won’t even pretend to know what that function normally does, but the nice thing is I don’t have to — I just want to intercept the arguments that get passed to it.

rustPlatform = rustPlatform' // {
    buildRustPackage =
    args:
    rustPlatform'.buildRustPackage (
        args
        // rec {

I do know that buildRustPackage is a function that takes some attribute set, so here we make our own function — recall that args: is the syntax for declaring a function which takes an argument, here named args. We then take the arguments and pass them to rustPlatform'.buildRustPackage — the very function we’re overriding. But, before that, // shows back up, because it’s time to fiddle with the arguments.

Pause and consider
#

At this point we have essentially re-constructed overrideAttrs, but for buildRustPackage. I feel confident in saying that a similar approach is bound to work for any other build platform in Nix, and that knowledge is incredibly powerful.

Like, I want to stress this point — we’ve found a pattern which could reliably be used to override the arguments to any build system in Nix, current and future. And, not for nothing, it’s something I cobbled together on my own — the key parts of it are taken from that forum post I linked, but this method — without using an overlay — wasn’t described there.

Enough pausing
#

The remainder of the code…

// rec {
    version = "0.20.3";
    src = fetchFromGitHub {
        owner = "aome510";
        repo = args.pname;
        rev = "refs/tags/v${version}";
        hash = "sha256-9iXsZod1aLdCQYUKBjdRayQfRUz770Xw3/M85Rp/OCw=";
    };
    cargoHash = "sha256-e9MAq31FTmukHjP5VoAuHRGf28vX9aPUWjOFfH9uY9g=";
}

are really just the arguments we want to override. We’re setting a new version, we’re tweaking src to make sure we’re actually getting that version of the code, and we update that pesky cargoHash which was the reason for this whole adventure in the first place.

After that, it’s just the usual soup of closing brackets and semicolons until the whole overriding business is done. And, to my great shock, it works.


These posts are usually a multi-day ordeal, but I was just so excited about this one find that, not only did I spend half my day tweaking Kepler to declaratively set up hosting for this blog and bring it live, I also wrote this post in more-or-less one marathon sitting.

Consequently, I’m tired, and will be making my exit now. Have a Shaman to level.