[src] 4 May 2022 My experience with Nix

At the beginning of this year, I had read a lot about Nix and felt pretty confident that this wasn’t something for me. That was, until I did a voicecall with my good friend Victor. He was showing me these really neat tricks on how I could use Nix and Nix Flakes to handle configuration files, build software reproducibly and define my own development shell for each project I’m doing - I was blown away.

After that call, I immediately started with the (admittedly stupid) thing that every developer does when learning about a new technology. I started moving all my shit over to it: System configuration, package management, package configuration, etc. - I wanted it all reproducible. In this blog post I will document my experience so far. This is very far from a guide to using Nix, but you might find some useful hints from my mistakes.

Installing Nix

Okay, the first step was to aquire Nix. I use a Macbook Air M1, and I quickly discovered that Apple have made this a hastle. After Catalina, you are not able to write to the root directory of Macs, which meant getting the /nix directory up and running required hacky workarounds. Luckily, these are now intergrated into the installer, which made the install very simple.

sh <(curl -L https://nixos.org/nix/install)

did the trick for me. A reboot was needed, but then everything was up and running smoothly. Nice!

Setting up nix-darwin

The next step was to get my system built through Nix. This is a pretty wacky concept, but the advantages are incredible. Think of all the times you’ve bought a new computer or needed to do a factory reset. All those preferences you’ve tinkered with to perfection over the years: They do not need to be set up again! A git clone, one simple command, most likely a few minutes of building and you have all your apps and software up and running.

I started with this guide and tinkered a bit according to my system:

{
  description = "kmaasrud's system";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    darwin.url = "github:lnl7/nix-darwin/master";
    darwin.url.nixpkgs.follows = "nixpkgs";
  };

  outputs = { self, darwin, nixpkgs }: {
    darwinConfigurations = rec {
      mba = darwin.lib.darwinSystem {
        system = "aarch64-darwin";
        modules = [ ./configuration.nix ];
      };
    }
  };
}

I put some simple system configuration in configuration.nix, built the flake with

nix build .#darwinConfigurations.mba.system

and was joyful to see everything working. This gave me a symlinked directory named result, containing a binary sw/bin/darwin-rebuild which I could use to bootstrap the system from the same flake. To my dismay, I was met with an error that said I was missing the /run directory. The error message said I needed to include the line run\tprivate/var/run in /etc/synthetic.conf, which would create the firmlink I needed. I did this without luck and ended up giving up on the whole debacle after a long night of googling. That following morning - when I could think sensibly again - I had an idea only a genius like me could find: I did a reboot

It worked.

Okay, 1-0 to Nix, but I had no reason to give up now. darwin-rebuild switch --flake . was working, and I was off to the races! The following days, weeks, months, I have been tinkering and my Nix system configuration to the core, and I am nowhere near considering myself done. If you are curious, you can explore my config yourself on GitHub.

Nix in projects

The whole system configuration thing is very cool, but I’ve found that the real magic of Nix comes when you start using it in your projects.

Nix serves as a very flexible build tool for your software. The builds are done inside a siloed environment, which means you need to declare all the dependencies of your build. This is immensely useful! Software is built the same on all computers, which especially makes CI a lot easier.

In my project MDoc, which depends on Tectonic (and subsequently a bunch of C/C++ libraries), I am able to do the build in a shell that has all the required dependencies without actually having to manually install said dependencies. They are listed in the following way:

buildInputs = with pkgs; [
  fontconfig
  graphite2
  harfbuzz
  icu
  libpng
  perl
  openssl
  zlib
] ++ lib.optionals stdenv.isDarwin [
  ApplicationServices
  Cocoa
];

Nix uses this list and makes sure these are linkable when building - so smooth! As you can see, “software is built the same on all computers” wasn’t an entirely true statement after all, since I need some specific dependencies for Darwin systems. However, when they can be conditionally defined this way, who am I to judge.

Conclusion

Allright, that is a misleading header; My Nix journey has only just begun. What I can say, is that Nix has changed the way I look at software. It would be very hard to go back to the old ways of a mutable system and non-reproducible development environments. I am convinced that Nix is the future - in some way or the other. It just needs a facelift, because there is no way I am accepting nix-* type commands.