Last week, I had to read a paper on a DSL and give a presentation on it as part of my programming language implementation class. Jingwen and I decided to present on NixOS. NixOS provides a functional, declarative way for users to configure and build an operating system deterministically. I thought this would be interesting to read about since it seems promising and after taking operating systems last semester, I was very interested in how the authors went about doing this. Of course, the word functional in the paper’s abstract also got me immediately excited. This article focuses on the parts of the paper that I’m presenting on—the design decisions behind Nix. If I’m already going to present on this, why not write about it?
NixOS is configured using Nix expressions, a pure and lazy functional language, specially defined for the package manager of the same name. The two key features about Nix are attribute sets and derivations. Attribute sets are essentially records that are passed into functions or derivations. Derivations are functions that take an attribute set and interpret it as build action for a package. It returns the original attribute set, extended with additional attributes and this newly returned attribute set would be passed into another derivation that depends on it. Each installed package is stored in the Nix store—immutable and analogous to a heap in a purely functional language—at
/nix/store with a filename containing a cryptographic hash of all inputs used to build the package. Nix derivations perform pure and atomic build actions.
Nix is pure, i.e. evaluating Nix expressions do not result in side-effects and an expression will always return the same result when evaluated with the same value. This “pureness” results in a number of neat properties.
- A specific Nix expression has one value and it does not depend on the state of the system.
- Since each value corresponds to a single Nix expression, it’s possible to identify a package based on the Nix expression that built it.
- Results from evaluating Nix expressions can be reused and the system can download pre-computed results.
- Builds not depending on each other can be done in parallel.
Since evaluating a Nix expression would mean building something (an expensive operation), evaluating expressions lazily allows Nix expressions to be freely passed around without having to evaluate them first.
3. Attribute sets
These are for convenience’s sake. Most functions in Nix are first-order functions taking in a large number of dependencies. With attribute sets, these dependencies don’t have to be specified in order and can be referred to by name instead of position. Additionally, attribute sets allows subtyping and hence checking that the right set of packages are passed into a function.
NixOS thus consists of a set of Nix expressions that return derivations that build the various parts that constitute a Linux system: static configuration files, boot scripts, and so on. These build upon the software packages already provided by Nixpkgs.
NixOS needs only a single configuration file,
/etc/nixos/configuration.nix. This contains configurations for the user’s system. Since Nix expressions are evaluated atomically, NixOS upgrades can be tried out and then rolled back if necessary. The cool part about NixOS is that build results are (almost always) deterministic and a single config file should result in the same system being built. There are some builds that might be non-deterministic, for example, if these builds use information such as system time.
I’ve only touched on the surface of NixOS and Nix here, so I’d highly recommend reading the original paper if you’re interested to know more about the design decisions behind Nix and more details on how NixOS works (such as the workarounds for the non-deterministic builds mentioned above). It’s a really neat paper!