I am no longer using flakes


⇠ back | date: 2023-06-18 | tags: nixos, tech | duration: 12:51 minutes

Before I really get started on what bothered me about flakes, I would like to very clearly point out that this is my opinion, and also that I am a big dumb-ass.

I use my computer for lots of different creative and professional (sometimes both) endeavour, and while I like Nix in a lot of ways (I have even really come to love the syntax of the language), having "managing my computer" as a hobby is not one of them.

I deliberated whether I should write and publish this blog post. In the end, please don't put too much weight onto my opinion. But I still wanted to share my experience and frustrations. Maybe the way that I have come to use Nix on my computers is not in line with "how you're supposed to do it", but maybe that's exactly the reason why I should write something about this topic.

Anyway...

What are flakes

Back in 2019 RFC 0049 was proposed, which introduced the Nix community to the new concept of "flakes", a way to manage Nix build inputs and outputs in a pure and reproducible manner. This made a lot of people very angry and after months of reasonable online discourse, the RFC was withdrawn and implementation work began in the new Nix CLIs behind an "experimental" feature flag. This was done in order to work on the design of flakes, and being able to explore problems that came up more organically. I can see however how people might feel that the design was rejected, and was then snuck in the backdoor by the RFC authors. In the meantime a number of smaller RFCs have come up by community members as a way to voice feature requests for flakes.

Responses to flakes are still varied to this day. Some are really into the features they enable your builds to have, and I have to admit that being able to have a single flake.nix file in a project repository, which pins a version of nixpkgs and produces a build of the given application is very nice.

Disdain for even the idea of flakes can still be found in some corners of the Internet however, and I think everyone involved in the development of flakes is rightfully on-edge, given the amount of hostility and abuse that they have been getting for checks notes implementing a new feature in a package manager.

I am aware of this history and I don't want this blog post to become a weapon wielded by jack-asses to harass a bunch of people online. Don't be a dick and go look at some ducks.

What I did before

When I got started using NixOS I began by maintaining a nixpkgs subtree. That's what all my friends were doing, and it seemed like a neat idea at the time. I was occasionally working on upstream nixpkgs, so having all the code right there seemed like a sensible idea. In hindsight, it really wasn't, and it turned into a frequent source of frustration (see the disclaimer above about how I don't want "managing my computer" to be a hobby of mine).

I stuck with this system for close to 4 years anyway (see the disclaimer about being a dumb-ass), because the cost of migrating always seemed to high. I knew I wanted to try out flakes, but the barrier to entry also seemed too high. The fact that the flakes RFC was withdrawn and development continued behind an experimental feature flag signalled, at least to me, a state of flux and uncertainty about the design of flakes. The fact that the only documentation available was one short page on the (generally excellent, please contribute more to it!) NixOS wiki, and a blog post series by tweag didn't help this.

The two big problems with the nixpkgs-subtree approach are: nixpkgs is huge and is going to make your computer unbearably slow, and unmanageable merge conflicts in case I ever updated a computer and didn't immediately push the update to the repository remote.

And while it would seem like something that is easy to avoid, I ran into this problem again and again and again, to the point where by the end my servers and desktops were running completely disjointed configuration repositories, that only occasionally were updated by sending patches around. Fucking yikes. Yes, this is my fault, but the system I had designed myself into encouraged these sorts of human errors to accumulate.

Finally, late last year (2022) I started a job where I came into much closer contact with flakes, and so, I pulled off the several-year-old band-aid and dove right into it.

Migrating to flakes

This was the state that I inherited and moved over to flakes. It took me a few weeks to get things building again on my NixOS desktop and Pop_OS (with home-manager) laptop. Right off the bat I didn't like a few things. I was using the NIX_PATH as a path lookup for modules in my configuration, so I didn't have to rely on relative paths between modules (I would frequently expand <configuration/foo> and similar lookups). This isn't allowed with flakes, unless you opt into impure evaluation mode, which is what I did. In hindsight, the way that my flakes configuration ended up looking was much more linear than the kludge of horror I had built before, and so this limitation was less of an issue, although it still bothered me in some places.

I also really wasn't, and still am not a fan of the build syntax:

  • Before: nix-build '<nixpkgs/nixos>' system -I nixos-config="$HOST"
  • After: nix build .#nixosConfiguration."$HOST".config.system.build.toplevel

This is the commit that ported my desktop over to flakes, if you wanna have a look. In my build harness I removed a bunch of features like being able to quickly build vm and iso targets. This might still be possible with flakes, but I didn't find an obvious way to do things in the moment, and so I opted for removing the features in the meantime.

The NixOS documentation (and SEO!) situation is bad enough, and restricting yourself to results that talk about a subset of the language and ecosystem made it unbearable in some cases to understand what I was supposed to do. I ended up reading several people's configurations to copy things from them.

I ended up with this custom harness function to create a NixOS system because I hate code duplication (and a similar one for the homeManagerConfiguration function):

nixosSystem = root: nixpkgs.lib.nixosSystem (root // {
    inherit system;
    modules = root.modules ++ [
      ({ ... }: {
        nix.nixPath = [
          "nixpkgs=${nixpkgs.outPath}"
          "home-manager=${home-manager.outPath}"
        ];
        nixpkgs.overlays = defaultOverlays;
      })

      ## Include the home-manager NixOS module
      home-manager.nixosModules.home-manager
    ];
  });

It was important to me to have the nixpkgs and home-manager keys set correctly in my NIX_PATH for the "legacy" CLIs to be able to pick them up and use. Nonetheless, this caused countless issues that I still don't fully understand. But more on that in a little bit.

Another rift I saw in the community of flakes users was whether or not to use flake-utils. The two philosophies seem to be:

  • "use flake-utils because it makes your life easier"
  • "don't use flake-utils because it makes flakes seem too complex"

Which one is it? Are they both true? I ended up not including another dependency for my configuration, instead writing the wrapper functions myself (as I said, I really quite like writing Nix code!).

Especially because all my computers are x86_64-linux, and flake-utils is often used to work around some issues with flakes regarding cross compilation. Depending on what systems you use with Nix, flake-utils may give you a better bang for your buck because you need to write less obscure loop code that includes your configuration root for multiple system targets.

I imagine this will confuse potential adopters of flakes similarly to how it confused me, and it certainly didn't leave me feeling rosy about this technology I had just built my sand castle atop of. If you're interested in the custom wrapper code I wrote: here are my main utilities and user utilities!

Using flakes

And so, that was my life for about 6 months. I would occasionally run nix flake update, which was easy to complete, almost never caused any merge conflicts due to my dumbassery (and even the one or two times it did, was easy enough to fix by just deleting the lockfile), and life was good.

I also started using flakes for some one-off projects, where I was collaborating with other people (for example jackctl, which you should check out if you make music on Linux), where using flakes made it easy to depend on multiple sources of nix code, and build a single package with a particular postFixup phase or whatever. I will still use flakes for these use-cases, because I think they're the ideal situation to map a simple set of inputs to a simple set of outputs.

Importantly, this is not my development environment, but rather something that would be invoked in CI.

But, I slowly noticed that the development environments littered around my system stopped getting current rustc packages. The way I build those is with shell.nix files, which include <nixpkgs> from my path, which are built by the lorri service. The idea of having different environments, that all pull from the same local package set is just to reduce the space requirements on my computer. I have a big SSD, but might as well not pull in 50 slightly different versions of rustc, cargo, etc.

At first I didn't really care, but with a lot of Rust projects recently dropping down their supported Rust compiler period, I started running into compilation issues. I even briefly installed rustup on my laptop because I was blocked from working on a project and it was the easiest way to get out of my pickle. shudders.

I knew something had to change. I couldn't go back to the way things were before, and the way flakes made me re-evaluate my configuration was good. At the same time, my computer started accumulating strange behaviours I didn't really understand. And maybe it was a matter of not using the new CLIs exclusively, or wanting to rely on lorri to asynchronously build environments for me and injecting them with direnv (something that works very well with emacs too).

The last straw was a build failure as a result of an incompatibility between home-manager and nixpkgs, that had allegedly been fixed, but updating both my nixpkgs and home-manager inputs over the span of 2 weeks didn't allow me to update my computer. Maybe I got extremely unlucky with the unstable channel progressing, but in the end it didn't really matter. I wanted to make a change.

What now

I had previously evaluated different strategies for managing my nixpkgs and home-manager dependencies. I briefly thought about subtrees, where I squash committed everything but decided that I didn't really get any benefit out of that arrangement. I tried a submodule, which was somehow even worse, and then I tried [niv] and flakes. In the end, flakes briefly won out because I wanted to try them. So moving to niv was the obvious choice.

niv was born out of some of the ideas of the initial flakes RFC, taking basically only its input management, encoded in a json configuration, which gets parsed by some magic nix code (which I have to admit, I do not fully understand, nor have I had to look at it because something was broken or unclear as to how it worked).

My build harness script looks like this now (the relevant bit):

NIXPKGS_ALLOW_UNFREE=1 \
    home-manager build -f "$ROOT" \
      -I "nixpkgs=$DIR" \
      -I "klib=$DIR/lib" \
      -I "nixpkgs-overlays=$DIR/overlays" \
      -I "home-manager=$($DIR/lib/find-component.sh home-manager)" \
      "$@"

and the way I import nixpkgs looks like this:

{ overlays ? [], ... } @ args:
let
  sources = import nix/sources.nix;
in
import sources.nixpkgs (args // {
  overlays = (import lib/overlays.nix) ++ overlays;
})

I had to write a small utility for setting the appropriate NIX_PATH for home-manager:

# De-reference current directory and grab the component parameter
# This will break if none was provided so... don't do that :)
DIR=$(realpath "$(dirname "$0")/..")
COMPONENT=$1

# Run nix eval and just return the result
nix eval --expr "(import $DIR/nix/sources.nix).${COMPONENT}.outPath" --impure | tr -d '"'

The nice thing about grabbing the component paths from the niv sources is that the NIX_PATH environment variable gets populated with absolute /nix/store paths. This means that changing something in my configuration won't apply to things on my system until I next build and switch to a new generation:

 ❤  (tempest) ~/sys> echo $NIX_PATH
nixpkgs=/nix/store/7znqbsc1vqyjhcmdvfjrfp73hhvazpwq-nixfiles 
home-manager=/nix/store/0haa6si3qmp8r80irj4a692qvxp51rfh-zb3bip08c8w6jxbhbmb4i852lp4a3d95-home-manager-src
nixpkgs-overlays=/nix/store/q015fg370v3qjwrz840pl5f38km4j5sh-overlays

In conclusion

In the end... is this better? I really don't know.

Flakes felt like magic in a lot of places, even though their functionality should be able to be encoded into a simple input -> output relationship. While using flakes I didn't feel like I understood what my computer was doing. And while my current solution technically allowed me to reduce the amount of code needed for my system to build, my solution is bespoke, and I can understand that someone who hasn't thought about their Nix setup as long and hard as I have, might feel overwhelmed by it. This is especially a problem if everyone comes up with slightly different variations on how they would like to initialise their nixpkgs builds. And maybe I should just have globally installed rustc and cargo and called it a day.

Shit... maybe managing my computer is my hobby.

I can certainly see how flakes can work for many, many people, and I am probably in the minority of users who will bump into problems.

At the same time, I'm not a fan of the way flakes have been developed.

There seem to be a sentiment that flakes are "done", even though they've technically never left the experimental stage. I do not condone the abuse directed towards the creators of this technology! But at the same time, I can see why some people might feel annoyed at the idea that an RFC was de-facto rejected (withdrawing an RFC is not generally done because its initial design was perfect), and then having its feature set snuck into the Nix codebase without going through the same RFC process again. At this point, enough users depend on the experimental flakes feature as it exists, it could be argued that any breaking change would be too disruptive, and so the current state must be stabilised as is.

Since 2019 the documentation situation for flakes has only improved marginally. There are a few more resources out there now, but none are "official" or built by the community, that go beyond the basics of what a flake is and does. There is a kind of chicken & egg problem where documentation must refer to things that are stable, and flakes need documentation to get more people into the headspace to try to use them.

Ultimately, I'm open to the idea of using flakes again. Updating my horrible hacks from before flakes to flakes was hard, changing from flakes to niv was two afternoons. And who knows, maybe I will try them again at some point. But for the time being, I decided it's more trouble to me than it's worth.

Thank you for reading this rambly post until the end. Maybe you learned something, or were able to contextualise some of your own experiences or problems. Please don't be a dick. In the end, it doesn't really matter. Computers don't matter, and you shouldn't get too upset about them.

Cheers.