/run/current-system: NixOS. A beginner's look.
About two months ago, when switching to a different computer, I have decided to finally install NixOS and give it a try. I have been doing all my usual work on it since then, and I think I can start sharing what I have learned so far.
This was a natural move for me, since I was already using the nix package manager on my machine, then running Fedora, for about two years — I installed all audio software via nix1, from SuperCollider to jack to Ardour. Nixpkgs, the main package collection of nix, offered a wider and more recent selection of audio software than Fedora (with Planet CCRMA).
NixOS is a complete linux distribution based on nix, currently offering about 60000 packages.
Disclaimer: I’m still a NixOS newbie, so my understanding of it may be naive-ish; yet that is in part why I have decided to start writing now - before I forget how it was using the system fresh, adapting to the new environment and studying it deeper as I write.
Much of what I describe below also applies to Gnu Guix, a distribution built on the same architecture as nix and NixOS, but using a lisp language as its main fabric instead of nix. (In fact, I anticipate making a switch to Guix some day.)
Both NixOS and Guix are oriented towards using the command line and a text editor to manage your system; and you probably need to have a basic understanding of the main building blocks of a unix-like system to feel comfortable.
So what is NixOS, after all?
The core ideas
Append, not overwrite
Ever since I have started using git, I fell in love with the idea of
immutability: the beauty of creating new versions of things instead of
modifying things in-place. After you did a git commit
, you know any
edits you make will not ruin what you already have. I now recognize it
in many places - functional programming languages, copy-on-write
filesystems, backup and file
synchronization tools, package managers, or
Wikipedia. And I tend to gravitate towards tools and
systems that leverage this idea.
When you install, upgrade or remove a package with nix, it is somewhat similar to making a commit in git - it creates a new version of the environment with the required packages added or removed. Then it just switches to the new environment by updating a symlink.
The previous version of the environment stays there should you want to rollback. Each version of the environment is called a generation. Unlike git, you can ask nix to remove some or all of the previous generations to save disk space (sure you wouldn’t want to keep past versions of software installed forever.) But it won’t delete anything by itself, only when you explicitly ask it to.
An environment is basically just a directory tree resembling the
traditional /usr
but with symlinks instead of actual binaries; the
binaries themselves are stored in a special directory called the nix
store:
$ ls ~/.nix-profile/
bin etc include lib lib64 libexec manifest.nix sbin share
$ which kdenlive
/home/me/.nix-profile/bin/kdenlive # < this is a symlink to /nix/store:
$ realpath $(which kdenlive)
/nix/store/hn2pv0hci1d9c9b5rd0b9g0css0ydg74-kdenlive-20.04.3/bin/kdenlive
A succession of environment generations is called a profile. A profile is, in a way, like a git branch - it is a pointer to a specific version of an environment:
$ readlink ~/.nix-profile # my current user profile
/nix/var/nix/profiles/per-user/me/profile
$ readlink /nix/var/nix/profiles/per-user/me/profile # ...points to generation 95
profile-95-link
$ readlink /nix/var/nix/profiles/per-user/me/profile-95-link # ...of the environment
/nix/store/8y9k19xhs6v4v97k8wpqc0xjklfqrvq8-user-environment
There are two kinds of profiles on NixOS - system and per-user. System profile provides common packages for all users; each user may additionally install packages into their own profile without affecting other users.
$ which kdenlive # installed my user profile:
/home/me/.nix-profile/bin/kdenlive
$ which bash # installed system-wide:
/run/current-system/sw/bin/bash
Also, user A might decide to install a different version a package than user B, or even a different build of the exact same version of the package. Both packages will peacefully coexist, because:
Different build, different hash, different path
The actual files of each package are stored in a separate directory per package:
/nix/store/<long-hash>-package-version/
…and symlinked into your environment when you install a package.
Each build of a package produces a hash based on all the parameters of
the build (including the hash of the downloaded source code archive)
as well as all of its dependencies. So if I ask nix to rebuild
ffmpeg
with a different configure flag, or with a different version
of any dependency, or if it is a different version of ffmpeg itself,
it will get a different hash, and will appear at a different path in
/nix/store/
.
Also, that particular build of ffmpeg will in turn depend on the
particular build of glibc in /nix/store/
it was compiled
against. That means one can have different versions of ffmpeg (and
glibc) available on a system simultaneously and switch between them on
demand.
Which version of a package you have installed depends on which
channel you installed it from. For instance, there is a stable
channel that corresponds to the latest NixOS release (nixos-20.03
at
the time of writing), and an unstable channel providing the most
recent versions of software.
Most of the packages on my system come from the stable channel, but occassionally I would install a package from the unstable channel in my user profile. This allows me to easily access the most bleeding-edge packages should I need to, while also being able to revert to a stable version at any time.2
$ nix-env --query --available --attr-path kdenlive
nixos.kdeApplications.kdenlive kdenlive-19.12.3
unstable.kdeApplications.kdenlive kdenlive-20.04.3
$ kdenlive --version # the system-wide kdenlive
kdenlive 19.12.3
$ nix-env --install kdenlive-20.04.3 # builds a new user environment with kdenlive 20
installing 'kdenlive-20.04.3'
building '/nix/store/7xd3gw6yqk7c41na9b8w87ql71l732s9-user-environment.drv'...
created 1533 symlinks in user environment
$ kdenlive --version # affects my user only
kdenlive 20.04.3
$ nix-env --rollback
switching from generation 97 to 96
$ kdenlive --version # back to system-wide kdenlive
kdenlive 19.12.3
Get to the source
Nix is a source-based package manager, meaning that a package is not a bunch of binaries compressed in an archive, but a precise description of how to derive the binary (or bytecode, or whatever) from source code.
When you install a package, nix asks a server whether a prebuilt binary substitute of a package is available. If there is one, it will be used instead of building from source on your machine. Otherwise, nix will compile the package from start to finish.
Nix strives to be fully reproducible (meaning that the resulting binaries should be bit-to-bit identical between machines), but not all packages are there yet.
Nevertheless, this ability to easily rebuild a package from source is very handy when you want to customize a package, for instance by changing the configure flags or enabling an optional build dependency. A build happens in an isolated environment, so that none of the development dependencies will pollute your working environment.
In effect, the user experience of installing a package from source is the same as when installing a prebuilt binary package. You just ask nix to install your customized definition of a package and it automatically builds and installs it for you.
A generated, versioned system
I have been managing my Fedora install (as well as remote systems) with Ansible for some time - I like the idea of having a concise description of the configuration of a system in one place (as opposed to having to remember it), and I hate doing things twice. Ansible automates boring repetitive tasks (like disabling password logins on a server) well for me.
I don’t need to use Ansible to manage NixOS though, since the system itself is based on declarative configuration. This is how you install NixOS - you edit a configuration file describing the to-be system (the file is conveniently pregenerated by the installer to reflect the hardware configuration of your machine) and then the installer uses nix to build the system accordingly.
Later, when you want to change something (add or remove a user, a
package, a service, flip a setting), you edit the file again and run
sudo nixos rebuild switch
. This builds a new generation of the
system profile (just like with user package profiles) and switches the
system to it on the fly.
The system profile includes much more than just the list of installed
packages. It also defines the contents of files like /etc/passwd
,
/etc/sudoers
, /etc/profile
or even /etc/fstab
; it contains the
configuration of systemd services, firewall, runtime kernel parameters,
bootloader configuration, locales, etc.
While Ansible is about modifying a system, NixOS generates one instead. What this means is that if you remove something from the configuration (like a package or service), it disappears from (current generation of) the system too, or is reset to its default clean state.
If anything goes wrong after you have made a change, you can do sudo
nixos rebuild switch --rollback
to go back to the previous
generation; additonally, the GRUB boot menu provides a list of past
configurations that you can boot into just in case.
While this might not be for everyone, I find this a very natural
upgrade from using Ansible on my own system. And if I need to manually
put something into /etc
, I just add the relevant section to
my configuration.nix
to do that for me.
Caveats
NixOS prioritizes certain properties of the system over others; controlling and isolating software compilation, minimizing undeclared modification to a system is certainly an important aspect of its design.
Depending on your use case, this might or might not be what you want.
For instance, this will most likely make curl
http://example.com/some-random-script.sh | sudo sh
fail, as the system
environment is quite different from what such a script would probably
expect (for instance, /usr
is not in $PATH
and is mostly empty).
This also immobilizes most binary software not specifically compiled for
NixOS, as that would expect shared libraries in common locations,
whereas on NixOS they are stored in /nix/store/
to make multiple
versions of shared libraries coexist.
This is preferable for me, as I avoid running third-party binaries (or
piping curl
into sudo
) for security reasons anyway. (I only do
things like sudo npm -g install some-popular-package
in temporary
isolated containers if I need to.) Nor do I run closed-source software,
although there are various ways of making
precompiled binaries run on NixOS, if you must.
Compiling software from source also needs to be done in an environment prepared by nix. Again, that may or may not be for you. I find this to be a clean way to manage build dependencies, but it needs a bit of getting used to.
Overall, NixOS is for use cases where you want to have a managed system with a declarative configuration and the possibility to easily alternate between different versions2 and/or builds of packages (including custom builds).
While in this post I allowed myself to speak in a fairly general way, I hope this it was helpful as a birds-eye view. I’m looking forward to writing more about specific, practical aspects of nix and NixOS in my next posts.
-
By the way, software installed via nix doesn’t touch
/usr
at all, so it won’t interefere with your distribution. ↩ -
Note that while nix provides the means to have as many different versions of packages (including their differing dependencies) on a system as one likes, the main package collection (nixpkgs) is not aimed at providing arbitrary versions of all packages to install; most packages have a single version available in a given channel at a given moment in time. There are ways of installing older versions of packages that don’t match what’s currently in a channel - and thanks to nix, that won’t mess up your system (you can always
--rollback
or usenix-shell
). ↩ ↩2