NixOS
Table of Contents
NixOS is a new kind of GNU/Linux distribution, borrowing the ideas of functional programming languages to bring about a revolution of how we think about operating systems and software development.
NixOS
NixOS is a "functional" GNU/Linux distribution extending the ideas of functional programming languages to operating systems.
To illustrate, most operating systems are more analogous to imperative languages where the programmer instructs, via the various incantations of the language, the computer to perform some operation. Many of these instructions modify state– variables, files, network packets. Functional programming languages can be instructive, but many of them feel more declarative in nature. That is, instead of telling what the computer to do, the programmer defines the result of the computation. Side-effects, as they are known, are either disallowed completely or require a certain amount of "ceremony" to be performed.
Following this imperfect analogy, GNU/Linux distributions typically follow the imperative paradigm: the operator issues instructions that modify the state of the system. Installing and configuring some software package, for example, requires the operator to run a series of commands that first install the package, another series of commands to configure the software to the desired state, and finally, the system and the resulting software is ready for use.
In contrast, NixOS follows the functional paradigm. The entire
system, from the boot loader to the available software and its configuration
can all be traced to a single file: /etc/nixos/configuration.nix
.
In the system configuration file for NixOS, we declare the various end states of the system:
- system packages
- system services
- users and groups
- kernel modules loaded during boot
- boot loader configuration
- mount points
The vast majority of the system can be codified into this configuration file. Currently missing is disk partitioning and allocation and user data.
To understand better the concepts behind NixOS, we will take a brief detour through Nix, the package manager.
Nix (the package manager)
Beyond the comparisons of language paradigms, there are some other really neat ideas that come from the Nix family. Specifically, I want to discuss the dependency management of Nix and, therefore, NixOS.
Recall the description of the "imperative" operating systems above: many
software packages have dependencies on various other components and packages.
Typically the distribution managers resolve the dependencies into a coherent
tree such that all packages resolve to the correct (read available) version of
glibc
. However, maintainers are human and maintainership is
non-trivial. Packages slip through, and core breakages happen. Worse, the
possible matrix of configurations and various packages is mind-numbingly
large.
Concretely, what this may look like can be demonstrated by a simple example.
Let's say an operator installs package A
which depends on package C
.
Later, our example operator installs another package B
, which also depends on
package C
. For now, we will say that package C
is the same version. So
far, all is good. Nothing is broken, and the system is stable. However, some
time later, package B
needs to be updated and causes a resulting update to
package C
. Now package A
may not work. Its version of package C
is now
replaced by the version pulled in by package B
. This may be fine, but by a
similar token, it may also be plainly broken. Worse, it may be only subtly
broken, the breakage is not noticed by cursory testing.
Using Nix, this situation is impossible. Package A
has its own
complete dependency graph, including package C
's dependency graph. The same
holds for package B
, Nix stores the entire dependency graph of each
package separate from each other package.
Therefore, given our above example of package management mishaps, when package
B
updates and pulls in a new version of package C
. The version of C
used
by A
is left untouched and package A
works just the same as before.
Nix Store
After examining the above example, how does Nix accomplish this?
If we think about the Filesystem Hierarchy Standard, an executable
package is installed into some bin
directory, its dynamically linked objects
are in a lib
directory, its configuration may be in a etc
directory.
With a Nix package, the package has all same directories, however,
they are isolated in the "store". The Nix store is typically a
directory /nix/store/
that contains each package in its hash-name-version
folder. For example, let's look at Firefox:
% ls -al $(which firefox) lrwxrwxrwx 1 root root 68 Dec 31 1969 /run/current-system/sw/bin/firefox -> /nix/store/78gl44fjjira5jsgyj8vdwsnw8wdwngs-firefox-68.0/bin/firefox % ls -l /nix/store/78gl44fjjira5jsgyj8vdwsnw8wdwngs-firefox-68.0/ total 0 dr-xr-xr-x 2 root root 21 Dec 31 1969 bin dr-xr-xr-x 3 root root 21 Dec 31 1969 lib dr-xr-xr-x 2 root root 42 Dec 31 1969 nix-support dr-xr-xr-x 4 root root 39 Dec 31 1969 share
There's a few things to notice here. One, the directories are all read-only; two, the modified time is set to UNIX Epoch; third, the entire folder structure necessary for Firefox to run is in this store.
Let's examine a package that requires dynamic linking to correctly execute, the Python interpreter:
% ls -l $(which python) lrwxrwxrwx 1 root root 68 Dec 31 1969 /run/current-system/sw/bin/python -> /nix/store/dmh36s38dcpc91grfsh6wqrm65rz5hfh-pythonOverlay/bin/python % ls -l /nix/store/dmh36s38dcpc91grfsh6wqrm65rz5hfh-pythonOverlay total 4 dr-xr-xr-x 2 root root 4096 Dec 31 1969 bin lrwxrwxrwx 1 root root 65 Dec 31 1969 include -> /nix/store/10rqw9cx8x2knwdaxhlyb4drla8v8zzk-python3-3.7.4/include dr-xr-xr-x 3 root root 113 Dec 31 1969 lib dr-xr-xr-x 3 root root 17 Dec 31 1969 share
Now, let's examine the linked objects:
% ldd $(which python) linux-vdso.so.1 (0x00007ffd3c3d9000) libpython3.7m.so.1.0 => /nix/store/10rqw9cx8x2knwdaxhlyb4drla8v8zzk-python3-3.7.4/lib/libpython3.7m.so.1.0 (0x00007f6ccf964000) libpthread.so.0 => /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libpthread.so.0 (0x00007f6ccf943000) libdl.so.2 => /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libdl.so.2 (0x00007f6ccf93e000) libcrypt.so.1 => /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libcrypt.so.1 (0x00007f6ccf904000) libncursesw.so.6 => /nix/store/adc71v5apk4dzcxg7cjqgszjg1a6pd0z-ncurses-6.1-20190112/lib/libncursesw.so.6 (0x00007f6ccf892000) libutil.so.1 => /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libutil.so.1 (0x00007f6ccf88b000) libm.so.6 => /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libm.so.6 (0x00007f6ccf6f5000) libgcc_s.so.1 => /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libgcc_s.so.1 (0x00007f6ccf4df000) libc.so.6 => /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libc.so.6 (0x00007f6ccf329000) /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/ld-linux-x86-64.so.2 => /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib64/ld-linux-x86-64.so.2 (0x00007f6ccfccf000)
Each of the linked objects, sans the kernel object, are found in the Nix store.
Furthermore, if we examine the Python lib
folder:
% ls -l /nix/store/dmh36s38dcpc91grfsh6wqrm65rz5hfh-pythonOverlay/lib total 12 lrwxrwxrwx 1 root root 78 Dec 31 1969 libpython3.7m.so -> /nix/store/10rqw9cx8x2knwdaxhlyb4drla8v8zzk-python3-3.7.4/lib/libpython3.7m.so lrwxrwxrwx 1 root root 82 Dec 31 1969 libpython3.7m.so.1.0 -> /nix/store/10rqw9cx8x2knwdaxhlyb4drla8v8zzk-python3-3.7.4/lib/libpython3.7m.so.1.0 lrwxrwxrwx 1 root root 75 Dec 31 1969 libpython3.so -> /nix/store/10rqw9cx8x2knwdaxhlyb4drla8v8zzk-python3-3.7.4/lib/libpython3.so lrwxrwxrwx 1 root root 71 Dec 31 1969 pkgconfig -> /nix/store/10rqw9cx8x2knwdaxhlyb4drla8v8zzk-python3-3.7.4/lib/pkgconfig dr-xr-xr-x 3 root root 8192 Dec 31 1969 python3.7
The shared objects and folders are also symbolic links to other packages and folders in the Nix store.
This leads us to the following observation: packages in the Nix store are comprised of the outputs of the package and associated symbolic links to the package's inputs.
Nix Profiles
Packages in Nix are directory trees found in the Nix store, what are profiles? Perhaps, more appropriately, what are user environments?
When setting up Nix as a package manager either in NixOS or a different GNU/Linux distribution, there's typically a symbolic link in the user's home directory:
% ls -l ~/.nix-profile lrwxrwxrwx 1 kb users 41 May 15 2017 /home/kb/.nix-profile -> /nix/var/nix/profiles/per-user/kb/profile
However, this link is to another link. Let's follow the rabbit:
% ls -l /nix/var/nix/profiles/per-user/kb/profile lrwxrwxrwx 1 kb users 14 Jul 30 15:15 /nix/var/nix/profiles/per-user/kb/profile -> profile-3-link
This indirect symbolic link points to a symbolic link in the same directory. Let's keep following:
% ls -l /nix/var/nix/profiles/per-user/kb/profile-3-link lrwxrwxrwx 1 kb users 60 Jul 30 15:15 /nix/var/nix/profiles/per-user/kb/profile-3-link -> /nix/store/d7d6hcv8v2crb98nhh00nrr2bkh034kc-user-environment % ls -l /nix/store/d7d6hcv8v2crb98nhh00nrr2bkh034kc-user-environment dr-xr-xr-x 2 root root 156 Dec 31 1969 bin lrwxrwxrwx 1 root root 63 Dec 31 1969 etc -> /nix/store/k0hyks88khah4hvb19i0d6swsawyzz5a-awscli-1.16.170/etc dr-xr-xr-x 2 root root 35 Dec 31 1969 lib lrwxrwxrwx 1 root root 60 Dec 31 1969 manifest.nix -> /nix/store/lqc7v4cb77fzgnrqn0miz1fpkzb3dxc2-env-manifest.nix lrwxrwxrwx 1 root root 65 Dec 31 1969 share -> /nix/store/k0hyks88khah4hvb19i0d6swsawyzz5a-awscli-1.16.170/share
After following three links, we are referred to a directory tree in the Nix store.
Let's examine the bin
directory quickly:
% ls -l /nix/store/d7d6hcv8v2crb98nhh00nrr2bkh034kc-user-environment/bin total 0 lrwxrwxrwx 1 root root 67 Dec 31 1969 aws -> /nix/store/k0hyks88khah4hvb19i0d6swsawyzz5a-awscli-1.16.170/bin/aws lrwxrwxrwx 1 root root 82 Dec 31 1969 aws_bash_completer -> /nix/store/k0hyks88khah4hvb19i0d6swsawyzz5a-awscli-1.16.170/bin/aws_bash_completer lrwxrwxrwx 1 root root 77 Dec 31 1969 aws_completer -> /nix/store/k0hyks88khah4hvb19i0d6swsawyzz5a-awscli-1.16.170/bin/aws_completer lrwxrwxrwx 1 root root 89 Dec 31 1969 cargo-generate-nixfile -> /nix/store/s11zqp9r8h4r65iqv272b477igb9a9mw-rust_carnix-0.10.0/bin/cargo-generate-nixfile lrwxrwxrwx 1 root root 91 Dec 31 1969 cargo_generate_nixfile.d -> /nix/store/s11zqp9r8h4r65iqv272b477igb9a9mw-rust_carnix-0.10.0/bin/cargo_generate_nixfile.d lrwxrwxrwx 1 root root 73 Dec 31 1969 carnix -> /nix/store/s11zqp9r8h4r65iqv272b477igb9a9mw-rust_carnix-0.10.0/bin/carnix lrwxrwxrwx 1 root root 75 Dec 31 1969 carnix.d -> /nix/store/s11zqp9r8h4r65iqv272b477igb9a9mw-rust_carnix-0.10.0/bin/carnix.d
Currently, this example profile only has two packages installed:
% nix-env -q awscli-1.16.170 rust_carnix-0.10.0
But from this example so far, we see that user environments are comprised of symbolic link "forests" of the packages that make up the current profile.
Let's follow this example again, however, we going to modify the user environment by adding a package:
% nix-env -i autogen
Starting with second link:
% ls -l /nix/var/nix/profiles/per-user/kb/profile lrwxrwxrwx 1 kb users 14 Aug 13 05:52 /nix/var/nix/profiles/per-user/kb/profile -> profile-4-link
The profile link now points to a different link. Let's keep going:
% ls -l /nix/var/nix/profiles/per-user/kb/profile-4-link lrwxrwxrwx 1 kb users 60 Aug 13 05:52 /nix/var/nix/profiles/per-user/kb/profile-4-link -> /nix/store/xslnn9gs5gkgdvzgb0w3b0iggbsszag5-user-environment
The user-profile now points to a completely different symlink forest in the Nix store.
The old profile still exists. Let's switch (rollback) to it:
% nix-env --rollback (1) switching from generation 4 to 3 % ls -l /nix/var/nix/profiles/per-user/kb/profile lrwxrwxrwx 1 kb users 14 Aug 13 05:56 /nix/var/nix/profiles/per-user/kb/profile -> profile-3-link % ls -l /nix/var/nix/profiles/per-user/kb/profile-3-link lrwxrwxrwx 1 kb users 60 Jul 30 15:15 /nix/var/nix/profiles/per-user/kb/profile-3-link -> /nix/store/d7d6hcv8v2crb98nhh00nrr2bkh034kc-user-environment
Rolling back to a previous profile was effortless and we went back to exactly the same store path that we had previously.
Since the symlink(2)
operation is atomic, changing profile
generations is atomic. Adding a package to the profile is atomic: that is,
once the package is downloaded, built, added to the store, and the set of links
are compiled into a new profile, the switch to this new profile is entirely
atomic. If the any of the previous steps fail, the user profile is not
adversely affected.
System Profiles
After user profiles, we are left with system profiles. What exactly is NixOS? If any of the above provides any foreshadowing, the answer may seem obvious: a system profile is a forest of symbolic links to the packages, services, and other system configuration that comprise a GNU/Linux system.
However, that may not be obvious.
Let's start by first reiterating that NixOS is different than
traditional GNU/Linux distributions, very different. One
of the most notable differences that is important to this discussion is the
lack of adherence to the Filesystem Hierarchy Standard. Chiefly, in
the root of the filesystem of a NixOS system, there is almost no
need for /bin
, /usr
, and /lib
.
% ls -l / total 57 drwxr-xr-x 2 root root 4096 Aug 2 09:04 bin drwxr-xr-x 5 root root 1024 Jun 5 17:01 boot drwxr-xr-x 21 root root 4140 Aug 13 05:37 dev drwxr-xr-x 26 root root 4096 Aug 2 09:04 etc drwxr-xr-x 3 root root 19 May 16 10:54 gnu drwxr-xr-x 3 root root 29 May 8 07:24 home drwx------ 2 root root 16384 Jun 5 16:02 lost+found drwxr-xr-x 4 root root 30 May 16 09:49 nix drwxr-xr-x 5 root root 4096 Jun 5 20:01 opt dr-xr-xr-x 257 root root 0 Aug 2 09:03 proc drwx------ 6 root root 4096 Aug 12 23:04 root drwxr-xr-x 20 root root 640 Aug 14 20:56 run dr-xr-xr-x 13 root root 0 Aug 2 09:03 sys drwxrwxrwt 55 root root 16384 Aug 14 21:00 tmp drwxr-xr-x 3 root root 4096 Jun 5 17:01 usr drwxr-xr-x 9 root root 4096 Aug 2 09:04 var
In the above output, there is both /bin
and /usr
, but no /lib
. What is
in /bin
and /usr
? Two things: one, /bin/sh
and two, /usr/bin/env
.
These are kept around as ways to resolve issues with porting packages into the
Nix environment.
% ls -l /bin/ total 4 lrwxrwxrwx 1 root root 75 Aug 2 09:04 sh -> /nix/store/93h01q6yg13xdrabvqbddzbk11w6a928-bash-interactive-4.4-p23/bin/sh % ls -lR /usr /usr: total 4 drwxr-xr-x 2 root root 4096 Aug 2 09:04 bin /usr/bin: total 4 lrwxrwxrwx 1 root root 66 Aug 2 09:04 env -> /nix/store/d9s1kq1bnwqgxwcvv4zrc36ysnxg8gv7-coreutils-8.30/bin/env
Notice, however, that these files are in fact symbolic links into the Nix store.
If there is nothing in /bin
and nothing in /usr
, where does the system find
all of the install programs?
The answer: /run/current-system
:
% ls -l /run/current-system lrwxrwxrwx 1 root root 88 Aug 2 09:04 /run/current-system -> /nix/store/9c3k3ky5lg3x937984902v1d7148m7c5-nixos-system-phenex-19.03.173147.77295b0bd26 % ls -l /nix/store/9c3k3ky5lg3x937984902v1d7148m7c5-nixos-system-phenex-19.03.173147.77295b0bd26 total 48 -r-xr-xr-x 1 root root 16455 Dec 31 1969 activate lrwxrwxrwx 1 root root 91 Dec 31 1969 append-initrd-secrets -> /nix/store/vynm9pvxlzd8rracmmkhpj2a3g79whbw-append-initrd-secrets/bin/append-initrd-secrets dr-xr-xr-x 2 root root 37 Dec 31 1969 bin -r--r--r-- 1 root root 0 Dec 31 1969 configuration-name lrwxrwxrwx 1 root root 51 Dec 31 1969 etc -> /nix/store/a04f5cdfinc8p4n6x0hw9a0jn5l2mi9i-etc/etc -r--r--r-- 1 root root 57 Dec 31 1969 extra-dependencies dr-xr-xr-x 2 root root 6 Dec 31 1969 fine-tune lrwxrwxrwx 1 root root 65 Dec 31 1969 firmware -> /nix/store/ak22608y0db7m4bzwmps23gi4f0s13dc-firmware/lib/firmware -r-xr-xr-x 1 root root 5568 Dec 31 1969 init -r--r--r-- 1 root root 9 Dec 31 1969 init-interface-version lrwxrwxrwx 1 root root 57 Dec 31 1969 initrd -> /nix/store/n7x32hhg41mflx9xvmmw61piwjdr81m1-initrd/initrd lrwxrwxrwx 1 root root 65 Dec 31 1969 kernel -> /nix/store/sgkk7pqh7jqvy6rvgnkk367amrpknw91-linux-4.19.59/bzImage lrwxrwxrwx 1 root root 58 Dec 31 1969 kernel-modules -> /nix/store/nrmlrwxyqmp4dbcldrlvibv0h61356bf-kernel-modules -r--r--r-- 1 root root 10 Dec 31 1969 kernel-params -r--r--r-- 1 root root 24 Dec 31 1969 nixos-version lrwxrwxrwx 1 root root 55 Dec 31 1969 sw -> /nix/store/11pfbzzamqvnbfxis4pbnzhrvarn3pj1-system-path -r--r--r-- 1 root root 12 Dec 31 1969 system lrwxrwxrwx 1 root root 64 Dec 31 1969 systemd -> /nix/store/9zkhhvix7rlqlj8pf8s2kbw8b88rky75-systemd-239.20190219
The various files and directories in this derivation are what is necessary for the current generation of the system. Similar to user environments, upgrades and rollbacks are atomic. Installing a package into the system packages, for example, will happen in isolation. Only after the build process is complete and successful does the forest of links get changed.
However, because there is some necessary artifacts of state when running a system, this isolation is certainly not perfect. Particularly so with the interaction of services and their underlying configuration.
Alternatives
Aside from NixOS, there is also Guix, a GNU alternative to Nix and NixOS. Many of the ideas of Guix are derived from Nix. In fact, early on in the life of the Guix project, Guix interacted with the Nix daemon directly. This is no longer the case as Guix has its own daemon now, however, the design is very similar.
The configuration language of Guix is, in proper GNU style, Guile Scheme instead of a DSL– nix.
Guix, as a GNU project, also takes a hard-line stance on software freedom and therefore does not and will not include any non-free code in the package repositories. Furthermore, this also means that Guix the package manager will not be supported on other operating systems such as Apple MacOS and Microsoft Windows.
Another alternative is using any number of Configuration Management solutions on a typical GNU/Linux distribution. However, solutions such as Ansible, Puppet, and Salt fail in a very similar way that tradition distrubtions fail: they are dependent on ordering and distrubtion managers and software maintainers to create appropriate software dependency graphs. Failure in dependency management yields failures in the system. Furthermore, once a package is no longer available in the official repositories, it is no longer trivially available to be installed via the software configuration tools.
Impressions and Thoughts
Nix and by extension NixOS (hopefully) solve some really annoying problems I tend to keep running into when developing software and immutable, reproducible infrastructure. I'm really excited about the possibilities Nix can bring. However, until I can really sit down and use Nix for development, I can't say anything with certainty.
In time, I will provide more detail on my impressions and review Nix and NixOS in more depth. However, if nothing else, being able to codify systems and environments in a reproducible manner is already a huge win.
I highly recommend reading NixOS: A Purely Functional Linux Distribution and Nix: A Safe and Policy-Free System for Software Development by Dolstra et al.. Both papers are very good explanations and breakdowns of the motivation and ideas behind Nix and NixOS.