Landing on planet NixOS
If you’ve ever taken a look at the Linux universe, you will have found numerous flavors - more correctly known as distributions - of operating systems. Each one slightly different from the other, almost like planets within a solar system, or perhaps akin to the thousands of delicious flavors of ice cream (this analogy sounds more appealing). The one thing keeping them all in orbit? The singular, common star at the center that is the Linux kernel.
Every distribution will have made a variety of design choices that brought them into their respective orbits, each possessing different properties that make them particularly desirable to different space travelers. Planet Ubuntu for the plain Jane, Kali for the security conscious, or Elementary for the design savvy. Some of the distributions lie light years away from our familiar planet Ubuntu (read Arch), and require the more foolhardy, renegade space cowboys to tame the lay of the land, and build a self-sustaining civilization before they can start beaming more spacefarers over.
In my case, I feel as though I was beamed right into NixOS’ warm and welcoming atmosphere, stumbling onto a medium-sized planet whose surface is entirely composed of a beautiful-yet-functional botanical garden of software and configuration. Beautiful, because it doesn’t take shape to the user as a single, clunky desktop environment, but instead hosts a whole floral arrangement of options. Functional, because each part of the garden is organized into modular, declarative components, allowing newcomers to grow the “garden of their dreams”.
The NixOS ecosystem
Enough with the analogies, what I really want to hone in on, are the reasons why I jumped from Ubuntu to NixOS. In Part 1 of the series, we looked at how I set-up my new laptop and made an initial installation of NixOS. Now, we take a deep dive into configuring the OS, looking briefly at why it is advantageous to have declarative control, before highlighting some of the available options and the decisions I took to build my current setup - which I’ve been using happily for about a half year at the time of writing.
As there will be snippets of configuration, it will probably be helpful to gloss over the Nix language syntax.
Why is NixOS’ declarative approach so special?
To understand the significance of declarative configuration, it is helpful to gain some
insight into the limitations of dealing with non-declarative, or “ad-hoc”, systems. With other
distributions, such as Ubuntu and Debian, or for the tinkerers - Arch Linux, you would typically
use a combination of package managers (
pacman) to manage installed
software and their dependencies, alongside innumerable configuration scripts - some of which
are managed by
root, and others which are confusingly overridden by the regular user.
Consider the following scenario. You spend a painstaking amount of blood, sweat, and tears to create the “perfect” Arch setup. As time passes, you make modifications to your configurations, tuning the OS to your liking. You will occasionally find yourself unhappy with the changes you’ve made and want to revert them. Unfortunately, this means you will have to recall the five different files to which you made your changes, not to mention what the changes themselves were. Depending on your mood you ultimately end up either: spending another hour or so recovering your StackOverflow answers and undoing your changes bit-by-bit, or giving up and living with your undesired changes, leaving your system just that little bit less “perfect”.
Now consider another scenario. One in which you have a C/C++ project that, for whatever reason, must be tested against multiple versions of GCC (I’ve encountered similar requirements at work). This is near impossible, if not a hassle, with standard package managers alone. A common solution would be to add a layer of virtualization, typically in the form of Docker or Vagrant, both of which are excellent tools for specific problems but add a layer of memory and computational overhead.
Let’s not even begin to consider the possibility of building and installing third-party software
directly to the root directory
/ - this works fine, at least until you want to uninstall it!
Might I dare to detail the process of migrating the entire shebang to a fresh install of the
OS on your new rig?
You probably get the picture.
NixOS avoids all of the aforementioned situations and introduces some added benefits. The Nix package manager is at the heart of the OS’ success. Similar to other package managers, Nix also tracks dependencies between different pieces of software. It’s most glowing innovation, however, is in how it manages the installed software under-the-hood and hands you a usable software stack.
Every software package in Nix is declared publicly on GitHub. They are each defined in Nix’s own expression language, which in summary tells Nix how to build the package from source (“should it run CMake, Autoconf, plain Make?”), and what other software it depends on.
From a definition, Nix will build the software into its own directory (it expects the child
directory structure to match the Filesystem Hierarchy Standard, FHS) named after a
computed hash based-on the inputs to the build definition together with the package name and
version. Here’s an example of how the
tree command has been packaged on from my
machine (Nix definition).
And the directory contents (
tree-ing tree, how meta).
$ tree /nix/store/x50x926i805qz046qbhssj5r6w2w05a6-tree-1.7.0/ /nix/store/x50x926i805qz046qbhssj5r6w2w05a6-tree-1.7.0/ ├── bin │ └── tree └── share └── man └── man1 └── tree.1.gz 4 directories, 2 files
Notably, once a package has been built, the contents of its store directory are immutable. Coupled with the hashes, each version of is guaranteed to be atomic. It should be obvious from this, that managing multiple versions of a software package alongside one another becomes trivial.
Our question then becomes: how is the software then made available to the user if each version of each software lives independently of each other? The answer is surprisingly simple.
$ ls -al /run/current-system/sw/bin | tail -n5 lrwxrwxrwx 1 root root 70 Jan 1 1970 zipdetails -> /nix/store/7yf3fh95ljf90nnw6cv70dry5jvqin0l-perl-5.28.1/bin/zipdetails lrwxrwxrwx 1 root root 62 Jan 1 1970 zless -> /nix/store/zrzqgdm6jxihsban195vrlcskmx9m4zc-gzip-1.9/bin/zless lrwxrwxrwx 1 root root 62 Jan 1 1970 zmore -> /nix/store/zrzqgdm6jxihsban195vrlcskmx9m4zc-gzip-1.9/bin/zmore lrwxrwxrwx 1 root root 61 Jan 1 1970 znew -> /nix/store/zrzqgdm6jxihsban195vrlcskmx9m4zc-gzip-1.9/bin/znew lrwxrwxrwx 1 root root 77 Jan 1 1970 zramctl -> /nix/store/hlk44cpp9nn7isb1jycxcj5f9lz0qa1v-util-linux-2.32.1-bin/bin/zramctl
Everything is symlinked! Nix knows how and what to symlink because either the
built package follows the FHS or the package definition prescribes the information accordingly.
So for each of the packages the user requires in their environment, the contents of the package
/nix/store/ are linked to common directories like
lib/. Better yet,
using symlinks grants additional flexibility - changing the version of a package means pointing
the link to another target! Note that this also applies to rolling back version changes - just
point it back to the previous target.
This paradigm can be taken a step further and applied to the management of the software and
configuration for a whole OS, let alone per-user packages. In the previous post,
we took a quick look at the
configuration.nix file. The OS bases its current state off this
file, written in Nix’s functional expression syntax. Every new state of your machine that occurs
as a result of changes made to the file is versioned. Given the guarantees made by Nix, you can
be sure your whole setup is deterministic and hence reproducible. Rolling back undesired changes
to the OS works the same as rolling back software versions - swapping a bunch of symlinks.
Migrating your setup to a new machine is as simple as copying the
over and running
When I began writing this post I didn’t expect it to end-up quite so long. Consequently, I’ve split this topic out into two posts. This one acts as a primer Nix’s design, whilst the next is a more practical case study of my xinit configuration.
Turns out there is quite a lot to say about the OS, despite its simplicity. I implore you to read more about Nix/NixOS here and here. Whoever took this concept and applied it at OS level raised its potential to a whole new level. NixOS has even been taken a step further and is being leveraged to provision infrastructure via NixOps. I’m a huge fan of Hashicorp, but I’m skeptical Terraform can match NixOS’s simple-yet-functional power, although I’ll finish by stealing one of their marketing taglines which I feel also applies to NixOS/Ops.
Infrastructure As Code
NB I’ve intentionally avoided mentioning
nix-shell at this stage as we see more of it in