This blog post is be about Nix, "The Purely Functional Package Manager", a software project as mind-blowingly useful as it is difficult to succinctly define. What makes Nix so peculiar is that it doesn't seem to aim at implementing specific functionality, instead implementing primitives that make this functionality trivial. This cuts both ways: on the one hand, it results in a radical simplification of a whole slew of build system and package management tooling, on the other hand it can be quite puzzling for beginners.

While there are many resources praising Nix as a package manager, it is rarely talked about as a build system, which it most certainly is. In this post I am going to lay out the problem with traditional build systems and the way Nix solves it, as well as demonstrate its benefits using a real-world example, my color space project HSLuv.

This problem is generally confronted at the level of a language ecosystem. For example, in Node.js, you use package.json to specify your build tools. In Python, you might use a Pipfile or requirements.txt . And likewise in every other language, which introduces new problems:

Looks simple, but will make hello.o work as expected? This is not certain, because it depends on the presence of GCC and on its version, and this is precisely where traditional build systems throw in the towel. They place the burden of reproducing the build environment on the programmer, often arming him with little more than a list of software to install manually.

The Nix solution

Look again at the sample Makefile from the previous section. Imagine yourself as its original developer. How do you make sure that when the build is run by another developer, the output is the same? By making sure the inputs are identical and the build operation is deterministic . In other words, by making sure the build operation is a pure function.

It is not hard to see that a Makefile rule is essentially a function whose arguments and return value are declared in the first line separated by a colon. How would this function be written in a purely functional programming language? Keep reading to find out.

What is Nix? Nix is defined by its creators as "The Purely Functional Package Manager", but it is helpful to see Nix as part of a set of tools that, like all good software, is structured in layers: Nix, the purely functional programming language Nix, the build system (think of GNU Make) Nix, the package manager NixOS, the Linux distribution NixOps, the NixOS deployment/configuration/provisioning tool The project is hosted on nixos.org and appears to advertise its Linux distribution as the most important layer, but I believe for most developers, most of the benefits can be reaped at the level of the package manager, which can be installed on MacOS or a Linux distro of your choosing.

Nix, the programming language When Nix is discussed online, one of the top comments is always a criticism of its syntax as if was a fatal flaw of the language. Having actually used Nix, I was puzzled to see this phenomenon. Then I realized where it was coming from and now I happily collapse these threads. The syntax is fine, trust me. For the purposes of this post it is enough to know that the Nix expression language is really small, covered entirely in a short section of the Nix manual. Alternatively, you can learn it as part of Luca Bruno's excellent Nix Pills tutorial, which in my opinion is the best way to learn Nix.

Nix, the build system What turns a purely functional programming language into a purely functional build system is the concept of a derivation, a first-class equivalent to a Makefile rule, i.e. a script that produces a set of files from a set of inputs: other files or configuration values. After installing Nix, create a file called default.nix with the following contents: rec { pkgs = import <nixpkgs> {}; hello = pkgs . stdenv . mkDerivation rec { name = "hello" ; builder = builtins . toFile "builder.sh" '' source $stdenv/setup echo "hello world" > $out '' ; }; } Here we have defined a derivation named hello that creates a text file. The output of a derivation is a file that gets created at the path provided by the environment variable $out . You can do anything inside the builder script, as long as you create a file or directory at $out . What about that pkgs object? Doesn't it look like useless boilerplate? It sure does, but fear not, we will be making use of it shortly. In the meantime, let's build the derivation by running nix-build -A hello from the same directory as the file above: $ nix-build -A hello these derivations will be built: /nix/store/yhca2sy0z8ilkgymsyyj2l02xxbqk7i8-hello.drv building path ( s ) ‘/nix/store/nydwxxqnxigsqslvri4hm4yd71al8dxy-hello’ /nix/store/nydwxxqnxigsqslvri4hm4yd71al8dxy-hello Nix stores the output in /nix/store but creates a link in the current directory so you can inspect it: $ cat result hello world You can input files into a derivation by referencing them relative to the .nix file directory. Inputs are defined as arbitary name/value pairs in the mkDerivation parameter and get passed into the builder script as identically named environment variables. This means in the example below, the path to the file ./foo.txt will be passed in as the environment variable $foo : rec { pkgs = import <nixpkgs> {}; demo = pkgs . stdenv . mkDerivation rec { name = "demo" ; foo = . /foo.txt ; builder = builtins . toFile "builder.sh" '' source $stdenv/setup mkdir $out install $foo $out/foo1.txt install $foo $out/foo2.txt '' ; }; } Similarly you can reference other derivations: rec { pkgs = import <nixpkgs> {}; hello = pkgs . stdenv . mkDerivation rec { name = "hello" ; builder = builtins . toFile "builder.sh" '' source $stdenv/setup echo "hello world" > $out '' ; }; demo = pkgs . stdenv . mkDerivation rec { inherit hello ; name = "demo" ; foo = . /foo.txt ; builder = builtins . toFile "builder.sh" '' source $stdenv/setup mkdir $out install $foo $out/foo.txt install $hello $out/hello.txt '' ; }; } When you build the derivation demo , you automatically build its dependent derivation hello : $ nix-build -A demo these derivations will be built: /nix/store/q2nmhdn3r1nn50rybgihw59irz40z1gp-hello.drv /nix/store/bsa1bnp05dbdls1hhfv9hbsfr8sp06aj-demo.drv building path ( s ) ‘/nix/store/gybvj34iq35rcwm2pggrrx5jpdf7rnz6-hello’ building path ( s ) ‘/nix/store/nwkjm3i41v30zq6k9jb3nq2m4rw8p524-demo’ /nix/store/nwkjm3i41v30zq6k9jb3nq2m4rw8p524-demo $ ls result foo.txt hello.txt This is all fine and good, but where are the promised build tools? The compilers, the libraries, the minifiers and uglifiers? Read on.