Scrive Nix Workshop

Welcome to Scrive Nix Workshop! This book contains the workshop materials used for training Haskell developers at Scrive. (We are hiring Haskell developers!)

We are open sourcing this content so that the Nix community can also benefit from the content and make contribution to improve it.

The content in this workshop is currently organized based on the workshop sessions we have held at Scrive. With the materials open sourced, we welcome to changes to make this more catered for the general audience in the Nix community. We hope that the current materials are still useful for more people to learn about Nix.

Nix is a rather difficult subject with a steep learning curve, even for us! So this is also our chance to learn from the rest of the Nix community. If there is any mistake or misconception in this book, do let us know!

Build Instructions

A HTML version of this workshop is published at https://scrive.github.io/nix-workshop/. The source code of this workshop is available on GitHub. This workshop material is built using mdbook.

To view this book, first install Nix, and then run:

make serve

You can then browse to http://localhost:3000 to view the book webpage.

License

Copyright (c) 2020-2021 Scrive AB. This work is licensed under both the Creative Commons Attribution-ShareAlike 4.0 International License and the MIT license.

Introduction

What is Nix?

Programming Language

  • Dynamically typed - Similar semantics to JavaScript and Lisp.

  • Functional programming - Higher order functions, immutability, etc.

  • Lazy - Values are not evaluated until needed.

Package Manager

  • Packages as special Nix objects that produce derivations and build artifacts.

  • One package can serve as build input of another package.

  • Multiple versions of the "same" package can be present on the same system.

Build System

  • Packages are built from source code.

  • Build artifacts of packages are cached based on content address (SHA256 checksum).

  • Multi language / multi repository build system.

    • Language agnostic.
    • Construct your own build system pipeline.

Operating System

  • Nix itself is a pseudo operating system.

    • Rich set of Nix packages that can typically be found in OS packages.
  • Nix packages can co-exist non-destructively with native OS packages.

    • All Nix artifacts are stored in /nix.

    • Global "installation" is merely a set of symlinks to Nix artifacts in /nix/store.

  • Lightweight activation of global Nix packages.

    • Add ~/.nix-profile/bin/ to $PATH.

    • Call source ~/.nix-profile/etc/profile.d/nix.sh to activate Nix.

    • Otherwise Nix is almost invisible to users if it is not activated.

  • NixOS is a full Linux operating system.

Reproducibility

  • Key differentiation of Nix as compared to other solutions.

  • Nix packages are built inside a lightweight sandbox.

    • No containerization.

    • Sanitize all environment variables.

    • Special $HOME directory at /homeless-shelter.

    • Reset date to Unix time 0.

    • Very difficult to accidentally escape the sandbox.

  • Content-addressable storage.

    • Addresses of Nix packages are based on a checksum of the source code, plus other factors such as CPU architecture and operating system.

    • If the checksum of the source code changes, the addresses of the derivation and any build artifacts also change.

    • If the address of a dependency changes, the addresses of the derivation and build artifact also change.

Installation

Download available at https://nixos.org/download.html.

Simplest way is to run:

$ curl -L https://nixos.org/nix/install | sh

After installation, you might need to relogin to your shell to reload the environment. Otherwise, run the following to use Nix immediately:

source ~/.nix-profile/etc/profile.d/nix.sh

You may want to configure to load this automatically in ~/.bashrc or similar file.

Update

If you have installed Nix before but have not updated it for a while, you should update it with:

nix-channel --update

This helps ensure we are installing the latest version of packages in global installation and global imports.

Learning Resources for Nix

  • Official Nix Manuals:

    • Nix Manual - Information about nix-the-command and the Nix Language.
    • Nixpkgs Manual - Information about the Nix Package Set (nixpkgs). How to extend and customise packages, how each language ecosystem is packaged, etc.
    • NixOS Manual - Information about the NixOS operating system. How to install/configure/update NixOS, the DSL for describing configuration options, etc.
  • nix.dev - Pragmatic guide on how to use Nix productively.

  • Awesome Nix - Curated list of Nix resources.

  • Nix Pills - Alternative Nix tutorial. Takes a bottom-up approach, explaining Nix and Nixpkgs design patterns along the way.

  • Gabriel439's Nix and Haskell tutorial

Install Global Packages

For newcomers, we can think of Nix as a supercharged package manager. Similar to traditional package managers, we can use Nix to install packages globally. Nix "installs" a global package by adding symlinkgs to ~/.nix-profile/bin, which should be automatically included in your shell's $PATH.

$ nix-env -i hello
installing 'hello-2.10'
building '/nix/store/mlfrpy1ahv3arh2n23p45vdpm0p4nl1x-user-environment.drv'...
created 39 symlinks in user environment
$ hello
Hello, world!

$ which hello
/home/user/.nix-profile/bin/hello

$ readlink $(which hello)
/nix/store/ylhzcjbchfihsrpsg0dxx9niwzp35y63-hello-2.10/bin/hello

Uninstall

$ nix-env --uninstall hello
uninstalling 'hello-2.10'

While convenient, global packages pollute the global environment of our system. Next we will look at how Nix shell can provide a local shell environment that provide the same dependencies that we need.

Nix Shell

You can use Nix packages without installing them globally on your machine. This is a good way to bring in the tools you need for individual projects.

$ nix-shell -p hello

[nix-shell:nix-workshop]$ hello
Hello, world!

Using Multiple Packages

$ nix-shell -p nodejs ghc cabal-install

[nix-shell:nix-workshop]$ which node
/nix/store/ndkzg5kpyp92mlzh5h66l4j393x6b256-nodejs-12.19.0/bin/node

[nix-shell:nix-workshop]$ which ghc
/nix/store/sbqnpfnx4w8jb7jq2yb71pifihwqy2a5-ghc-8.8.4/bin/ghc

[nix-shell:nix-workshop]$ which cabal
/nix/store/060x141b9fz2pm6yz4zn3i0ncavbdbf7-cabal-install-3.2.0.0/bin/cabal

Using shell.nix

It is common practice for Nix-using projects to provide a shell.nix file that specifies the shell environment. The nix-shell command reads this file, allowing us to create reproducible shell environments without using -p. These environments can provide access to any tool written in any language, without polluting the global environment. We will cover the use of shell.nix in a later chapter.

Nix Repl

nix repl
Welcome to Nix version 2.3.8. Type :? for help.

nix-repl> "Hello World!"
"Hello World!"

Nix Primitives

Strings

nix-repl> "hello"
"hello"

Booleans

nix-repl> true
true

nix-repl> false
false

nix-repl> true && false
false

nix-repl> true || false
true

Null

nix-repl> null
null

nix-repl> true && null
error: value is null while a Boolean was expected, at (string):1:1

Numbers

nix-repl> 1
1

nix-repl> 2
2

nix-repl> 1 + 2
3

String Interpolation

nix-repl> name = "John"

nix-repl> name
"John"

nix-repl> "Hello, ${name}!"
"Hello, John!"

Multiline Strings

nix-repl> ''
            Lorem ipsum dolor sit amet, consectetur adipiscing elit.
              Nullam augue ligula, pharetra quis mi porta.

            - ${name}
          ''
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n  Nullam augue ligula, pharetra quis mi porta.\n\n- John\n"

String Concatenation

nix-repl> "Hello " + "World"
"Hello World"

nix-repl> "Hello " + 123
error: cannot coerce an integer to a string, at (string):1:1

Set / Object

nix-repl> object = { foo = "foo val"; bar = "bar val"; }

nix-repl> object
{ bar = "bar val"; foo = "foo val"; }

nix-repl> object.foo
"foo val"

nix-repl> object.bar
"bar val"

Merge Objects

nix-repl> a = { foo = "foo val"; bar = "bar val"; }

nix-repl> b = { foo = "override"; baz = "baz val"; }

nix-repl> a // b
{ bar = "bar val"; baz = "baz val"; foo = "override"; }

Inherit

nix-repl> foo = "foo val"

nix-repl> bar = "bar val"

nix-repl> { foo = foo; bar = bar; }
{ bar = "bar val"; foo = "foo val"; }

nix-repl> { inherit foo bar; }
{ bar = "bar val"; foo = "foo val"; }

Inherit From Object

nix-repl> let
            object = {
              foo = "foo val";
              bar = "bar val";
            };
          in
          {
            foo = object.foo;

            baz = "baz val";
          }
{ baz = "baz val"; foo = "foo val"; }
nix-repl> let
            object = {
              foo = "foo val";
              bar = "bar val";
            };
          in
          {
            inherit (object) foo;

            baz = "baz val";
          }
{ baz = "baz val"; foo = "foo val"; }

List

nix-repl> list = [ "hello" 123 { foo = "foo"; } ]

nix-repl> list
[ "hello" 123 { ... } ]

nix-repl> builtins.elemAt list 2
{ foo = "foo"; }

List Concatenation

nix-repl> [ 1 2 ] ++ [ "foo" "bar" ]
[ 1 2 "foo" "bar" ]

Nix Expressions

If Expression

nix-repl> if true then 0 else 1
0

nix-repl> if false then 0 else "foo"
"foo"

nix-repl> if null then 1 else 2
error: value is null while a Boolean was expected

Let Expression

nix-repl> let
            foo = "foo val";
            bar = "bar val, ${foo}";
          in
          { inherit foo bar; }
{ bar = "bar val, foo val"; foo = "foo val"; }

With Expression

nix-repl> let
            object = {
              foo = "foo val";
              bar = "bar val";
            };
          in
          [
            object.foo
            object.bar
          ]
[ "foo val" "bar val" ]
nix-repl> let
            object = {
              foo = "foo val";
              bar = "bar val";
            };
          in
          with object; [
            foo
            bar
          ]
[ "foo val" "bar val" ]

Function

nix-repl> greet = name: "Hello, ${name}!"

nix-repl> greet "Alice"
"Hello, Alice!"

nix-repl> greet "Bob"
"Hello, Bob!"

Curried Function

nix-repl> secret-greet = code: name:
            if code == "secret"
            then "Hello, ${name}!"
            else "Nothing here"

nix-repl> secret-greet "secret" "John"
"Hello, John!"

nix-repl> nothing = secret-greet "wrong"

nix-repl> nothing "Alice"
"Nothing here"

nix-repl> nothing "Bob"
"Nothing here"

Named Arguments

nix-repl> greet = { name, title }: "Hello, ${title} ${name}"

nix-repl> greet { title = "Ms."; name = "Alice"; }
"Hello, Ms. Alice"

nix-repl> greet { name = "Alice"; }
error: anonymous function at (string):1:2 called without required argument 'title', at (string):1:1

Default Arguments

nix-repl> greet = { name ? "Anonymous", title ? "Ind." }: "Hello, ${title} ${name}"

nix-repl> greet {}
"Hello, Ind. Anonymous"

nix-repl> greet { name = "Bob"; }
"Hello, Ind. Bob"

nix-repl> greet { title = "Mr."; }
"Hello, Mr. Anonymous"

Lazy Evaluation

nix-repl> err = throw "something went wrong"

nix-repl> err
error: something went wrong

nix-repl> if true then 1 else err
1

nix-repl> if false then 1 else err
error: something went wrong

nix-repl> object = { foo = err; bar = "bar val"; }

nix-repl> object.bar
"bar val"

nix-repl> object.foo
error: something went wrong

Sequencing

nix-repl> builtins.seq err true
error: something went wrong
nix-repl> builtins.seq object true
true

nix-repl> builtins.deepSeq object true
error: something went wrong

File Management in Nix

String to File

nix-repl> builtins.toFile "hello.txt" "Hello World!"
"/nix/store/r4mvpxzh7rgrm4j831b2yi90zq64grqm-hello.txt"
$ cat /nix/store/r4mvpxzh7rgrm4j831b2yi90zq64grqm-hello.txt
Hello World!

Path

nix-repl> ./.
/path/to/nix-workshop

nix-repl> ./code/01-getting-started
/path/to/nix-workshop/code/01-getting-started

nix-repl> ./not-found
/path/to/nix-workshop/not-found

Path Concatenation

nix-repl> ./. + "code/01-getting-started"
/path/to/nix-workshop/code/01-getting-started

Read File

nix-repl> builtins.readFile ./code/03-nix-basics/03-files/hello.txt
"Hello World!"

nix-repl> builtins.readFile /nix/store/r4mvpxzh7rgrm4j831b2yi90zq64grqm-hello.txt
"Hello World!"

nix-repl> builtins.readFile (builtins.toFile "hello" "Hello World!")
"Hello World!"

Path

nix-repl> builtins.path { path = ./.; }
"/nix/store/s0c3cc8k6dy51zx9xicfprsl9r35zvf6-nix-workshop"
nix-repl> "${./.}"
"/nix/store/s0c3cc8k6dy51zx9xicfprsl9r35zvf6-nix-workshop"
$ ls /nix/store/s0c3cc8k6dy51zx9xicfprsl9r35zvf6-nix-workshop
01-getting-started  02-nix-commands ...

The exact address changes every time the directory is updated.

Named Path

nix-repl> workshop = builtins.path { path = ./.; name = "first-scrive-workshop"; }

nix-repl> workshop
"/nix/store/fp0lw035xhxqwgfqifxlb430lyw48r7m-first-scrive-workshop"

nix-repl> builtins.readFile (workshop + "/code/03-nix-basics/03-files/hello.txt")
"Hello World!"

Content-Addressable Path

The files hello.txt and hello-2.txt both have the same content "Hello World!", but they produce different artifacts in the Nix store, i.e. the name of a Nix artifact depends on the name of the original file / directory.

nix-repl> builtins.path { path = ./code/03-nix-basics/03-files/hello.txt; }
"/nix/store/925f1jb1ajrypjbyq7rylwryqwizvhp0-hello.txt"

nix-repl> builtins.path { path = ./code/03-nix-basics/03-files/hello-2.txt; }
"/nix/store/bghk1lsjcylfm05j00zj5j42lv09i79z-hello-2.txt"

Solution: give a fixed name to path artifacts:

nix-repl> builtins.path {
            name = "hello.txt";
            path = ./code/03-nix-basics/03-files/hello-2.txt;
          }
"/nix/store/925f1jb1ajrypjbyq7rylwryqwizvhp0-hello.txt"

Fetch URL

nix-repl> example = builtins.fetchurl "https://scrive.com/robots.txt"

nix-repl> example
[0.0 MiB DL] downloading 'https://scrive.com/robots.txt'"/nix/store/r98i29hkzwyykm984fpr4ldbai2r8lhj-robots.txt"

nix-repl> example
"/nix/store/r98i29hkzwyykm984fpr4ldbai2r8lhj-robots.txt"
$ cat /nix/store/r98i29hkzwyykm984fpr4ldbai2r8lhj-robots.txt
User-agent: *
Sitemap: https://scrive.com/sitemap.xml
Disallow: /amnesia/
Disallow: /api/

URLs are only fetched once locally!

Fetch Tarball

nix-repl> nodejs-src = builtins.fetchTarball
            "https://nodejs.org/dist/v14.15.0/node-v14.15.0-linux-x64.tar.xz"
nix-repl> nodejs-src
"/nix/store/6wkj0blipzdqbsvwv03qy57n4l33scpw-source"
$ ls /nix/store/6wkj0blipzdqbsvwv03qy57n4l33scpw-source
bin  CHANGELOG.md  include  lib  LICENSE  README.md  share

SHA256 Checksum

Make sure that the content retrieved is the same for all users.

nix-repl> nodejs-src = builtins.fetchTarball {
            name = "nodejs-src";
            url = "https://nodejs.org/dist/v14.15.0/node-v14.15.0-linux-x64.tar.xz";
            sha256 = "14jmakaxmlllyyprydc6826s7yk50ipvmwwrkzf6pdqis04g7a9v";
          }
nix-repl> nodejs-src
"/nix/store/6wkj0blipzdqbsvwv03qy57n4l33scpw-source"

Importing Modules

Importing Nix Modules

We have the following files in 03-nix-basics/04-import:

"foo val"
[ "bar val 1" "bar val 2" ]
let
  foo = import ./foo.nix;
  bar = import ./bar.nix;
in
{ inherit foo bar; }
nix-repl> import ./code/03-nix-basics/04-import/foo.nix
"foo val"

nix-repl> import ./code/03-nix-basics/04-import/bar.nix
[ "bar val 1" "bar val 2" ]

nix-repl> import ./code/03-nix-basics/04-import
{ bar = [ ... ]; foo = "foo val"; }

Importing Global Modules

nix-repl> nixpkgs = import <nixpkgs> {}

nix-repl> nixpkgs.lib.stringLength "hello"
5

Importing a Tarball

Let's say we have the same foo.nix now available as a gist. We can import that file by asking Nix to fetch the tarball generated by GitHub:

nix-repl> gist = builtins.fetchTarball "https://gist.github.com/soareschen/d41e9b764018da4d2336644329c915e4/archive/476c45eaba13e23316cdca781bda7ec68676397b.tar.gz"

nix-repl> foo = import (gist + "/foo.nix")

nix-repl> foo
"foo val"

Pinning a Tarball Version

We have tested our remote foo.nix hosted on GitHub Gist, expecting it to have the same content as the local foo.nix we have. But the gist can be updated, yet we want to ensure that the content remains the same. This can be done by pinning the SHA256 checksum of foo.nix to what we expect using nix-prefetch-url:

$ nix-prefetch-url --type sha256 --unpack "https://gist.github.com/soareschen/d41e9b764018da4d2336644329c915e4/archive/476c45eaba13e23316cdca781bda7ec68676397b.tar.gz"
unpacking...
[0.0 MiB DL]
path is '/nix/store/vik2vk9ifbyps9pvhqa89px0c76cvaxz-476c45eaba13e23316cdca781bda7ec68676397b.tar.gz'
1ig5g6gvys26ka11z0wx08l72h8g5rr7p4fywk905sabdknf92yx

We can then copy the SHA256 checksum to make sure that the file content at the URL never unexpectedly changes:

nix-repl> gist =  builtins.fetchTarball {
            url = "https://gist.github.com/soareschen/d41e9b764018da4d2336644329c915e4/archive/476c45eaba13e23316cdca781bda7ec68676397b.tar.gz";
            sha256 = "1ig5g6gvys26ka11z0wx08l72h8g5rr7p4fywk905sabdknf92yx";
          }

Pinning Nixpkgs

We can use the same approach to pin nixpkgs itself. By pinning nixpkgs to a specific commit ID and SHA256 checksum, we can be sure that everyone that uses our Nix module are using the exact version of nixpkgs that we have specified.

For instance, we can pin the nixpkgs version we use to commit c1e5f8723ceb684c8d501d4d4ae738fef704747e:

$ nix-prefetch-url --type sha256 --unpack \
>     "https://github.com/NixOS/nixpkgs/archive/c1e5f8723ceb684c8d501d4d4ae738fef704747e.tar.gz"
unpacking...
[19.6 MiB DL]
path is '/nix/store/7ik3kdki828cnva46vnis87ha6axjk7n-c1e5f8723ceb684c8d501d4d4ae738fef704747e.tar.gz'
02k3l9wnwpmq68xmmfy4wb2panqa1rs04p1mzh2kiwn0449hl86j

Now we can import our pinned nixpkgs:

nix-repl> nixpkgs-src =  builtins.fetchTarball {
            url = "https://github.com/NixOS/nixpkgs/archive/c1e5f8723ceb684c8d501d4d4ae738fef704747e.tar.gz";
            sha256 = "02k3l9wnwpmq68xmmfy4wb2panqa1rs04p1mzh2kiwn0449hl86j";
          }

nix-repl> nixpkgs = import nixpkgs-src {}

nix-repl> nixpkgs.lib.stringLength "hello"
5

Pinning nixpkgs is highly encouraged when developing Nix modules. Without pinning, your users may run your modules on a version of nixpkgs that is several months old.

We will go through in a later chapter on how to use niv to automate the management of pinned remote Nix packages.

Nix Derivation Basics

First import a pinned version of nixpkgs so that we all get the same result:

nix-repl> nixpkgs-src = builtins.fetchTarball {
            url = "https://github.com/NixOS/nixpkgs/archive/c1e5f8723ceb684c8d501d4d4ae738fef704747e.tar.gz";
            sha256 = "02k3l9wnwpmq68xmmfy4wb2panqa1rs04p1mzh2kiwn0449hl86j";
          }

nix-repl> nixpkgs = import nixpkgs-src {}

We use the pinned version of nixpkgs so that everyone following the tutorial will get the exact same derivation.

Standard Derivation

nix-repl> hello-drv = nixpkgs.stdenv.mkDerivation {
            name = "hello.txt";
            unpackPhase = "true";
            installPhase = ''
              echo -n "Hello World!" > $out
            '';
          }

nix-repl> hello-drv
«derivation /nix/store/ad6c51ia15p9arjmvvqkn9fys9sf1kdw-hello.txt.drv»
$ cat /nix/store/ad6c51ia15p9arjmvvqkn9fys9sf1kdw-hello.txt.drv
Derive([("out","/nix/store/f6qq9bwv0lxw5glzjmin1y1r1s3kangv-hello.txt","","")],...)

$ nix show-derivation /nix/store/ad6c51ia15p9arjmvvqkn9fys9sf1kdw-hello.txt.drv
{
  "/nix/store/ad6c51ia15p9arjmvvqkn9fys9sf1kdw-hello.txt.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/z449wrqvwncs8clk7bsliabv1g1ci3n3-hello.txt"
      }
    },
    "inputSrcs": [
      "/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"
    ],
    ...
  }
}

Building Derivation

We can now build our derivation:

$ nix-build /nix/store/ad6c51ia15p9arjmvvqkn9fys9sf1kdw-hello.txt.drv
/nix/store/z449wrqvwncs8clk7bsliabv1g1ci3n3-hello.txt

$ cat /nix/store/z449wrqvwncs8clk7bsliabv1g1ci3n3-hello.txt
Hello World!

This may take some time to load on your computer, as Nix fetches the essential build tools that are commonly needed to build Nix packages.

We can also build the derivation within Nix repl using the :b command:

nix-repl> :b hello-drv
[1 built, 0.0 MiB DL]

this derivation produced the following outputs:
  out -> /nix/store/z449wrqvwncs8clk7bsliabv1g1ci3n3-hello.txt

Tracing Derivation

Our hello-drv produce the same output as hello.txt in previous chapter, but produce different output in the Nix store. (previously we had /nix/store/925f1jb1ajrypjbyq7rylwryqwizvhp0-hello.txt)

We can trace the dependencies of the derivation back to its source:

$ nix-store --query --deriver /nix/store/z449wrqvwncs8clk7bsliabv1g1ci3n3-hello.txt
/nix/store/ad6c51ia15p9arjmvvqkn9fys9sf1kdw-hello.txt.drv

$ nix-store --query --deriver /nix/store/925f1jb1ajrypjbyq7rylwryqwizvhp0-hello.txt
unknown-deriver

Our hello.txt built from stdenv.mkDerivation is built from a derivation artifact hello.txt.drv, but our hello.txt created from builtins.path has no deriver. In other words, the Nix artifacts are different because they are produced from different derivations.

We can further trace the dependencies of hello.txt.drv:

$ nix-store -qR /nix/store/ad6c51ia15p9arjmvvqkn9fys9sf1kdw-hello.txt.drv
/nix/store/01n3wxxw29wj2pkjqimmmjzv7pihzmd7-which-2.21.tar.gz.drv
/nix/store/03f77phmfdmsbfpcc6mspjfff3yc9fdj-setup-hook.sh
...

That's a lot of dependencies! Where are they being used? We will learn about it in the next chapter.

Derivation in a Nix File

We save the same earlier derivation we defined inside a Nix file named hello.nix. Now we can build our derivation directly:

$ nix-build 04-derivations/01-derivation-basics/hello.nix
/nix/store/z449wrqvwncs8clk7bsliabv1g1ci3n3-hello.txt

We can also get the derivation without building it using nix-instantiate:

$ nix-instantiate 04-derivations/01-derivation-basics/hello.nix
warning: you did not specify '--add-root'; the result might be removed by the garbage collector
/nix/store/ad6c51ia15p9arjmvvqkn9fys9sf1kdw-hello.txt.drv

Ignore the warning from nix-instantiate, as we don't care whether the derivation is deleted during Nix garbage collection.

Notice that both the derivation and the build output have the same hash as the earlier result we had in nix repl.

Caching Nix Build Artifacts

We create hello-sleep.nix as a variant of hello.nix which sleeps for 10 seconds in its buildPhase. (We will go through how each phase works in the next chapter) The 10 seconds sleep simulates the time taken to compile a program. We can see what happens when we try to build the same Nix derivation multiple times.

First, instantiating a derivation is not affected by the build time:

$ time nix-instantiate 04-derivations/01-derivation-basics/hello-sleep.nix
/nix/store/58ngrpwgv6hl633a1iyjbmjqlbdqjw92-hello.txt.drv

real    0m0,217s
user    0m0,179s
sys     0m0,032s

The first time we build hello-sleep.nix, it is going to take about 10 seconds. We can also see the logs we printed during the build phase is shown:

$ time nix-build 04-derivations/01-derivation-basics/hello-sleep.nix
these derivations will be built:
  /nix/store/58ngrpwgv6hl633a1iyjbmjqlbdqjw92-hello.txt.drv
building '/nix/store/58ngrpwgv6hl633a1iyjbmjqlbdqjw92-hello.txt.drv'...
unpacking sources
patching sources
configuring
no configure script, doing nothing
building
Building hello world...
Finished building hello world!
installing
post-installation fixup
shrinking RPATHs of ELF executables and libraries in /nix/store/lm801yriwjj4298ry74hdv5j0rpkpacq-hello.txt
strip is /nix/store/bnjps68g8ax6abzvys2xpx12imrx8949-binutils-2.31.1/bin/strip
patching script interpreter paths in /nix/store/lm801yriwjj4298ry74hdv5j0rpkpacq-hello.txt
checking for references to /build/ in /nix/store/lm801yriwjj4298ry74hdv5j0rpkpacq-hello.txt...
/nix/store/lm801yriwjj4298ry74hdv5j0rpkpacq-hello.txt

real    0m12,202s
user    0m0,371s
sys     0m0,084s

But the next time we build hello-sleep.nix, it will take no time to build, and there is no build output:

$ time nix-build 03-nix-basics/05-derivation/hello-sleep.nix
/nix/store/lm801yriwjj4298ry74hdv5j0rpkpacq-hello.txt

real    0m0,310s
user    0m0,256s
sys     0m0,047s

Nix determines whether a derivation needs to be rebuilt based on the input derivation. For our case, in both calls to hello-sleep.nix, nix-build instantiates the derivation behind the scene: i.e. /nix/store/58ngrpwgv6hl633a1iyjbmjqlbdqjw92-hello.txt.drv. So it determines that the result has previously already been built, and reuses the same Nix artifact.

Derivation as File

With the duck-typing nature of Nix, derivations act just like files in Nix. We can actually treat the hello-drv we defined earlier as a file and read from it:

nix-repl> builtins.readFile hello-drv
querying info about missing paths"Hello World!"

How does that work? Internally Nix lazily builds a derivation when it is evaluated, and turns it into a file path. We can verify that by using builtins.toPath:

nix-repl> builtins.toPath hello-drv
"/nix/store/z449wrqvwncs8clk7bsliabv1g1ci3n3-hello.txt"

With this property, we can also import derivations from a Nix file, and then use it as if the derivation has been built:

nix-repl> hello = import ./code/04-derivations/01-derivation-basics/hello.nix

nix-repl> builtins.readFile hello
querying info about missing paths"Hello World!"

We can even use a derivation as a string. Nix automatically builds the derivation when it is evaluated as a string:

nix-repl> "path of hello: ${hello}"
"path of hello: /nix/store/z449wrqvwncs8clk7bsliabv1g1ci3n3-hello.txt"

Dependencies

Previously we have built toy derivations with dummy output. In practice, Nix derivations are used for building programs, with build artifacts such as compiled binaries being the derivation output.

We can demonstrate this property by "building" a greet program. First we have to import a pinned version of nixpkgs as before. To simplify the process we abstract it out into a nixpkgs.nix at the root directory.

Now we build a greet program in greet.nix:

let
  nixpkgs = import ../../nixpkgs.nix;

  greet = nixpkgs.stdenv.mkDerivation {
    name = "greet";
    unpackPhase = "true";

    buildPhase = ''
      echo "building greet..."
      sleep 3
    '';

    installPhase = ''
      mkdir -p $out/bin

      cat <<'EOF' > $out/bin/greet
      #!/usr/bin/env bash
      echo "Hello, $1!"
      EOF

      chmod +x $out/bin/greet
    '';
  };
in
greet
$ nix-build code/04-derivations/02-dependencies/greet.nix
these derivations will be built:
  /nix/store/97lmyym0isl0ism7pfnv1b0ls4cahpi8-greet.drv
building '/nix/store/97lmyym0isl0ism7pfnv1b0ls4cahpi8-greet.drv'...
unpacking sources
patching sources
configuring
no configure script, doing nothing
building
building greet...
installing
post-installation fixup
shrinking RPATHs of ELF executables and libraries in /nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet
strip is /nix/store/bnjps68g8ax6abzvys2xpx12imrx8949-binutils-2.31.1/bin/strip
stripping (with command strip and flags -S) in /nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet/bin
patching script interpreter paths in /nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet
/nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet/bin/greet: interpreter directive changed from "/usr/bin/env bash" to "/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash"
checking for references to /build/ in /nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet...
/nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet

Now we can run greet:

$ result/bin/greet John
Hello, John!

Let's try to see what's inside the produced greet script:

$ cat result/bin/greet
#!/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash
echo "Hello, $1!"

The shebang to the bash shell has been modified to pin to the Nix version of Bash.

Upper Greet

Our greet program can now be used as a dependency to other derivations. Let's create an upper-greet derivation that convert any greet result to upper case.

upper-greet.nix:

let
  nixpkgs = import ../../nixpkgs.nix;

  inherit (nixpkgs) coreutils;

  greet = import ./greet.nix;
in
nixpkgs.stdenv.mkDerivation {
  name = "upper-greet";

  unpackPhase = "true";

  buildPhase = ''
    echo "building upper-greet..."
    sleep 3
  '';

  installPhase = ''
    mkdir -p $out/bin

    cat <<'EOF' > $out/bin/upper-greet
    #!/usr/bin/env bash
    ${greet}/bin/greet "$@" | ${coreutils}/bin/tr [a-z] [A-Z]
    EOF

    chmod +x $out/bin/upper-greet
  '';
}

Show Derivation

First we instantiate upper-greet.drv without building it yet:

drv=$(nix-instantiate 04-derivations/02-dependencies/upper-greet.nix)

We can use nix show-derivation to find out the dependency graph of the derivation of upper-greet:

$ nix show-derivation $drv
{
  "/nix/store/n61g8616l7g7zv32q52yrzmzr850mjp0-upper-greet.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/dj2vp64gbja0bp65lngrw9q4lrm1a8r3-upper-greet"
      }
    },
    "inputSrcs": [
      "/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"
    ],
    "inputDrvs": {
      "/nix/store/7gby8zic1p851ap63q1vdpwy7z1db85c-coreutils-8.32.drv": [
        "out"
      ],
      "/nix/store/97lmyym0isl0ism7pfnv1b0ls4cahpi8-greet.drv": [
        "out"
      ],
      "/nix/store/l54djrh1n7d8zdfn26w7v6zjh5wp7faa-bash-4.4-p23.drv": [
        "out"
      ],
      "/nix/store/x9why09hwx2pcnmw0fw7hhh1511hyskl-stdenv-linux.drv": [
        "out"
      ]
    },
    ...
    "env": {
      "buildInputs": "",
      "buildPhase": "echo \"building upper-greet...\"\nsleep 3\n",
      "builder": "/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash",
      ...
      "installPhase": "mkdir -p $out/bin\n\ncat <<'EOF' > $out/bin/upper-greet\n#!/usr/bin/env bash\n/nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet/bin/greet \"$@\" | /nix/store/2shqhfsyzz4rnfyysbzgyp5kbfk29750-coreutils-8.32/bin/tr [a-z] [A-Z]\nEOF\n\nchmod +x $out/bin/upper-greet\n",
      "name": "upper-greet",
...

We can see that greet.drv is included as one of inputDrvs. This means that when upper-greet.drv is being built, greet.drv will have to be built first.

The output path of upper-greet.drv is listed in outputs. This shows that the output hash of a derivation is fixed, regardless of the content of the build result.

This is also why the output path of greet.drv is used directly in env.installPhase of upper-greet.drv, even for the case when greet.drv has not been built.

Build Derivation

$ nix-build 04-derivations/02-dependencies/upper-greet.nix
these derivations will be built:
  /nix/store/n61g8616l7g7zv32q52yrzmzr850mjp0-upper-greet.drv
building '/nix/store/n61g8616l7g7zv32q52yrzmzr850mjp0-upper-greet.drv'...
unpacking sources
patching sources
configuring
no configure script, doing nothing
building
building upper-greet...
installing
post-installation fixup
shrinking RPATHs of ELF executables and libraries in /nix/store/dj2vp64gbja0bp65lngrw9q4lrm1a8r3-upper-greet
strip is /nix/store/bnjps68g8ax6abzvys2xpx12imrx8949-binutils-2.31.1/bin/strip
stripping (with command strip and flags -S) in /nix/store/dj2vp64gbja0bp65lngrw9q4lrm1a8r3-upper-greet/bin
patching script interpreter paths in /nix/store/dj2vp64gbja0bp65lngrw9q4lrm1a8r3-upper-greet
/nix/store/dj2vp64gbja0bp65lngrw9q4lrm1a8r3-upper-greet/bin/upper-greet: interpreter directive changed from "/usr/bin/env bash" to "/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash"
checking for references to /build/ in /nix/store/dj2vp64gbja0bp65lngrw9q4lrm1a8r3-upper-greet...
/nix/store/dj2vp64gbja0bp65lngrw9q4lrm1a8r3-upper-greet

As expected, the greet results are turned into upper case.

$ result/bin/upper-greet John
HELLO, JOHN!

The absolute paths to greet and coreutils are extended:

$ cat result/bin/upper-greet
#!/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash
/nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet/bin/greet "$@" | /nix/store/2shqhfsyzz4rnfyysbzgyp5kbfk29750-coreutils-8.32/bin/tr [a-z] [A-Z]

Runtime Dependency

If we query the references of the upper-greet output (not the derivation), we can see that greet is still a runtime dependency of upper-greet.

$ nix-store --query --references /nix/store/dj2vp64gbja0bp65lngrw9q4lrm1a8r3-upper-greet
/nix/store/2shqhfsyzz4rnfyysbzgyp5kbfk29750-coreutils-8.32
/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23
/nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet

We can use nix why-depends to find out why Nix thinks greet is a runtime dependency to upper-greet:

$ nix why-depends /nix/store/dj2vp64gbja0bp65lngrw9q4lrm1a8r3-upper-greet /nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet
/nix/store/dj2vp64gbja0bp65lngrw9q4lrm1a8r3-upper-greet
╚═══bin/upper-greet: …ash-4.4-p23/bin/bash./nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet/bin/greet "$@" | /nix/sto…
    => /nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet

Fibonacci

Nix dependencies can be nested arbitrarily deep. We can demonstrate that by building a fibonacci Nix derivation with the following behavior:

  • Answers are produced in $out/answer.
  • Each build takes 3 seconds to produce the answer.
  • fib(0) is 0, and fib(1) is 1.
  • fib(n) depends on the answers from fib(n-1) and fib(n-2).
  • The builds are prefixed with a name, so that we can force Nix to re-evaluate the whole sequence by changing the name.

fib.nix:

let
  nixpkgs = import ../../nixpkgs.nix;

  inherit (nixpkgs) stdenv;

  prefixed-fib = prefix:
    let fib = n:
      assert builtins.isInt n;
      assert n >= 0;
      let
        n-str = builtins.toString n;
      in
        if n == 0 || n == 1
        then
          stdenv.mkDerivation {
            name = "${prefix}-fib-${n-str}";
            unpackPhase = "true";

            buildPhase = ''
              echo "Producing base case fib(${n-str})..."
              sleep 3
              echo "The answer to fib(${n-str}) is ${n-str}"
            '';

            installPhase = ''
              mkdir -p $out
              echo "${n-str}" > $out/answer
            '';
          }
        else
          let
            fib-1 = fib (n - 1);
            fib-2 = fib (n - 2);

            n-1-str = builtins.toString (n - 1);
            n-2-str = builtins.toString (n - 2);
          in
          stdenv.mkDerivation {
            name = "${prefix}-fib-${n-str}";
            unpackPhase = "true";

            buildPhase = ''
              fib_1=$(cat ${fib-1}/answer)
              fib_2=$(cat ${fib-2}/answer)

              echo "Calculating the answer of fib(${n-str}).."
              echo "Given fib(${n-1-str}) = $fib_1,"
              echo "and given fib(${n-2-str}) = $fib_2.."

              sleep 3

              answer=$(( $fib_1 + $fib_2 ))
              echo "The answer to fib(${n-str}) is $answer"
            '';

            installPhase = ''
              mkdir -p $out
              echo "$answer" > $out/answer
            '';
          }
    ;
  in fib;
in
prefixed-fib

To make sure we build the fibonacci sequence from scratch each time, we can use $(date +%s) with the Unix timestamp as the prefix to our builds.

Let's try fib(0) and fib(1):

$ time nix-build -E "import ./code/04-derivations/03-fibonacci/fib.nix \"$(date +%s)\" 0"
these derivations will be built:
  /nix/store/yyy5fz5rsws6a812c9xc5ps1hwh9lm98-1605561354-fib-0.drv
building '/nix/store/yyy5fz5rsws6a812c9xc5ps1hwh9lm98-1605561354-fib-0.drv'...
...
no configure script, doing nothing
building
Producing base case fib(0)...
The answer to fib(0) is 0
...
checking for references to /build/ in /nix/store/9kvw6x88l9nx42mvrgzif60a72h66fqz-1605561354-fib-0...
/nix/store/9kvw6x88l9nx42mvrgzif60a72h66fqz-1605561354-fib-0

real    0m4,763s
user    0m0,476s
sys     0m0,130s
$ time nix-build -E "import ./code/04-derivations/03-fibonacci/fib.nix \"$(date +%s)\" 1"
these derivations will be built:
  /nix/store/qs2pc54dmd21xlhlqgzwmgfj98y1kr8n-1605561412-fib-1.drv
building '/nix/store/qs2pc54dmd21xlhlqgzwmgfj98y1kr8n-1605561412-fib-1.drv'...
...
Producing base case fib(1)...
The answer to fib(1) is 1
...
checking for references to /build/ in /nix/store/426cpqvvr7lbg92hywmbw7552vggpway-1605561412-fib-1...
/nix/store/426cpqvvr7lbg92hywmbw7552vggpway-1605561412-fib-1

real    0m5,279s
user    0m0,415s
sys     0m0,102s

So both fib(0) and fib(1) takes roughly 4~5 seconds to build.

Let's try fib(2):

$ time nix-build -E "import ./code/04-derivations/03-fibonacci/fib.nix \"$(date +%s)\" 2"
these derivations will be built:
  /nix/store/r8n1v9ifk0q6mf75j35scn3s2dwa03j7-1605561535-fib-1.drv
  /nix/store/xxrzkbsnq1jpp4s5fdkpklxr3fbc0aq6-1605561535-fib-0.drv
  /nix/store/1bgy17jbakr9yn1yz0bnwhnnz1y1xsdc-1605561535-fib-2.drv
building '/nix/store/xxrzkbsnq1jpp4s5fdkpklxr3fbc0aq6-1605561535-fib-0.drv'...
...
Producing base case fib(0)...
The answer to fib(0) is 0
...
checking for references to /build/ in /nix/store/8k74irn8w8rzpccd30wdn8589nq0wdv5-1605561535-fib-0...
building '/nix/store/r8n1v9ifk0q6mf75j35scn3s2dwa03j7-1605561535-fib-1.drv'...
...
Producing base case fib(1)...
The answer to fib(1) is 1
...
checking for references to /build/ in /nix/store/p716qrpmyi4649k2njfy7gb97ksyi5y3-1605561535-fib-1...
building '/nix/store/1bgy17jbakr9yn1yz0bnwhnnz1y1xsdc-1605561535-fib-2.drv'...
...
Calculating the answer of fib(2)..
Given fib(1) = 1,
and given fib(0) = 0..
The answer to fib(2) is 1
...
checking for references to /build/ in /nix/store/qnaad56wgknlgki9r3kpmr4fhc7x8vxv-1605561535-fib-2...
/nix/store/qnaad56wgknlgki9r3kpmr4fhc7x8vxv-1605561535-fib-2

real    0m12,599s
user    0m0,658s
sys     0m0,198s

So building fib(2) causes fib(1) and fib(0) to also be built.

With this going on, if we are going to build fib(5), then in total it is going to take a lot of time! but if we have built fib(4) already, then building fib(5) will be very fast.

Let's fix our prefix to see Nix cache in effect:

$ prefix=$(date +%s)
$ time nix-build -E "import ./code/04-derivations/03-fibonacci/fib.nix \"$prefix\" 4"
these derivations will be built:
  /nix/store/gz9bgzmna8v7pw5giclfhrk81dp1z0rw-1605561962-fib-1.drv
  /nix/store/y2waqw60jqawallz4q3r64iwrrihnd1p-1605561962-fib-0.drv
  /nix/store/nwj4bmrqgfv1fkvhh00bl2v03c3zqpy1-1605561962-fib-2.drv
  /nix/store/17d2r2dd52qvmfa5k3dm9gkl1k09wdb8-1605561962-fib-3.drv
  /nix/store/w4w4la01p9a2i8mlg6fm15il6vmgcqzl-1605561962-fib-4.drv
building '/nix/store/y2waqw60jqawallz4q3r64iwrrihnd1p-1605561962-fib-0.drv'...
...
Calculating the answer of fib(4)..
Given fib(3) = 2,
and given fib(2) = 1..
The answer to fib(4) is 3
...
checking for references to /build/ in /nix/store/c26q5vs8vdfr268nqgamjk8bcypf8b7r-1605561962-fib-4...
/nix/store/c26q5vs8vdfr268nqgamjk8bcypf8b7r-1605561962-fib-4

real    0m19,486s
user    0m0,910s
sys     0m0,253s

Now run fib(5):

$ time nix-build -E "import ./code/04-derivations/03-fibonacci/fib.nix \"$prefix\" 5"
these derivations will be built:
  /nix/store/nyb25403l4m5n69y3djlffsyzvpwyv6g-1605561962-fib-5.drv
building '/nix/store/nyb25403l4m5n69y3djlffsyzvpwyv6g-1605561962-fib-5.drv'...
unpacking sources
patching sources
configuring
no configure script, doing nothing
building
Calculating the answer of fib(5)..
Given fib(4) = 3,
and given fib(3) = 2..
The answer to fib(5) is 5
installing
post-installation fixup
shrinking RPATHs of ELF executables and libraries in /nix/store/p4iq9jaiid469ycgjm5v3ks3w0v35spi-1605561962-fib-5
strip is /nix/store/bnjps68g8ax6abzvys2xpx12imrx8949-binutils-2.31.1/bin/strip
patching script interpreter paths in /nix/store/p4iq9jaiid469ycgjm5v3ks3w0v35spi-1605561962-fib-5
checking for references to /build/ in /nix/store/p4iq9jaiid469ycgjm5v3ks3w0v35spi-1605561962-fib-5...
/nix/store/p4iq9jaiid469ycgjm5v3ks3w0v35spi-1605561962-fib-5

real    0m7,916s
user    0m0,461s
sys     0m0,151s

Now only fib-5.drv needs to be built.

Lazy Evaluation

A derivation like fib(10) is going to take a long time to build. So we don't really want to build it unless we actually need it. In fact, we also wouldn't want to build fib(0) through fib(10) unless they are actually needed.

With Nix's lazy evaluation strategy, we in fact get the laziness property that none of the fibonacci derivations are going to be built unless they are needed:

$ time nix-instantiate -E "import ./code/04-derivations/03-fibonacci/fib.nix \"$(date +%s)\" 10"
warning: you did not specify '--add-root'; the result might be removed by the garbage collector
/nix/store/dwjxm9rqxfbhf4m8nbg5wzddx1j4rcpl-1605562192-fib-10.drv

real    0m0,235s
user    0m0,182s
sys     0m0,038s

We can see that while fib(10) has fib(0) through fib(9) as its dependencies, but they are not being built just yet.

$ nix-store -qR /nix/store/dwjxm9rqxfbhf4m8nbg5wzddx1j4rcpl-1605562192-fib-10.drv | grep fib
/nix/store/31xdxhxynqndaiym043bjjky0l229vlg-1605562192-fib-1.drv
/nix/store/r4wkf6774ahva1zchk77kpvjf14xigrl-1605562192-fib-0.drv
/nix/store/8agah8vidgxi6yp9ki066yimfr16kigg-1605562192-fib-2.drv
/nix/store/ka3glx20pgzkvgan88xl87xki51y5pmi-1605562192-fib-3.drv
/nix/store/v5zra6kayssdg04n7xpjljqa1q5jjyqn-1605562192-fib-4.drv
/nix/store/1cqwdhnk8f55lxlajmjw6rzq2lq12x5l-1605562192-fib-5.drv
/nix/store/qrbrgdv16f7mc2xfalrgmypfz6c7yljq-1605562192-fib-6.drv
/nix/store/p5chc4l9mjqw5871lkd6har4hyjp55fj-1605562192-fib-7.drv
/nix/store/gn9hp9jcsfclrsdx6qlvjd051w4rsx8b-1605562192-fib-8.drv
/nix/store/6av3vlibm4knm4m3djcrfrhhb6jck3zx-1605562192-fib-9.drv
/nix/store/dwjxm9rqxfbhf4m8nbg5wzddx1j4rcpl-1605562192-fib-10.drv

Input Derivations

If we inspect the derivation, the derivations fib-9.drv and fib-8.drv are listed as one of the input derivations:

$ nix show-derivation /nix/store/dwjxm9rqxfbhf4m8nbg5wzddx1j4rcpl-1605562192-fib-10.drv
{
  "/nix/store/dwjxm9rqxfbhf4m8nbg5wzddx1j4rcpl-1605562192-fib-10.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/g7415lrzl6b43vnw58dgkxg5nzbjplp0-1605562192-fib-10"
      }
    },
    "inputSrcs": [
      "/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"
    ],
    "inputDrvs": {
      "/nix/store/6av3vlibm4knm4m3djcrfrhhb6jck3zx-1605562192-fib-9.drv": [
        "out"
      ],
      "/nix/store/gn9hp9jcsfclrsdx6qlvjd051w4rsx8b-1605562192-fib-8.drv": [
        "out"
      ],
      "/nix/store/l54djrh1n7d8zdfn26w7v6zjh5wp7faa-bash-4.4-p23.drv": [
        "out"
      ],
      "/nix/store/x9why09hwx2pcnmw0fw7hhh1511hyskl-stdenv-linux.drv": [
        "out"
      ]
    },
    ...
    "env": {
      "buildInputs": "",
      "buildPhase": "fib_1=$(cat /nix/store/zr1ziy94ig8isdg0gdliz0sm59abh2l1-1605562192-fib-9/answer)\nfib_2=$(cat /nix/store/zvivby98nbjlb6pszp6qla4v1r6zwj82
-1605562192-fib-8/answer)\n\necho \"Calculating the answer of fib(10)..\"\necho \"Given fib(9) = $fib_1,\"\necho \"and given fib(8) = $fib_2..\"\n\nsleep 3\n\
nanswer=$(( $fib_1 + $fib_2 ))\necho \"The answer to fib(10) is $answer\"\n",
...

Building Actual Derivation

We can build them later on when nix-build is actually called, or we can get the cached result from else where before building them.

$ time nix-build /nix/store/dwjxm9rqxfbhf4m8nbg5wzddx1j4rcpl-1605562192-fib-10.drv
these derivations will be built:
  /nix/store/31xdxhxynqndaiym043bjjky0l229vlg-1605562192-fib-1.drv
  /nix/store/r4wkf6774ahva1zchk77kpvjf14xigrl-1605562192-fib-0.drv
  /nix/store/8agah8vidgxi6yp9ki066yimfr16kigg-1605562192-fib-2.drv
  /nix/store/ka3glx20pgzkvgan88xl87xki51y5pmi-1605562192-fib-3.drv
  /nix/store/v5zra6kayssdg04n7xpjljqa1q5jjyqn-1605562192-fib-4.drv
  /nix/store/1cqwdhnk8f55lxlajmjw6rzq2lq12x5l-1605562192-fib-5.drv
  /nix/store/qrbrgdv16f7mc2xfalrgmypfz6c7yljq-1605562192-fib-6.drv
  /nix/store/p5chc4l9mjqw5871lkd6har4hyjp55fj-1605562192-fib-7.drv
  /nix/store/gn9hp9jcsfclrsdx6qlvjd051w4rsx8b-1605562192-fib-8.drv
  /nix/store/6av3vlibm4knm4m3djcrfrhhb6jck3zx-1605562192-fib-9.drv
  /nix/store/dwjxm9rqxfbhf4m8nbg5wzddx1j4rcpl-1605562192-fib-10.drv
building '/nix/store/r4wkf6774ahva1zchk77kpvjf14xigrl-1605562192-fib-0.drv'...
...
Calculating the answer of fib(10)..
Given fib(9) = 34,
and given fib(8) = 21..
The answer to fib(10) is 55
...
checking for references to /build/ in /nix/store/g7415lrzl6b43vnw58dgkxg5nzbjplp0-1605562192-fib-10...
/nix/store/g7415lrzl6b43vnw58dgkxg5nzbjplp0-1605562192-fib-10

real    0m39,818s
user    0m1,260s
sys     0m0,413s

Evaluation-Time Dependencies

In our original fib.nix, the build output of earlier fibonacci numbers are used during the build phase of the current derivation. But if we somehow uses the earlier fibonacci numbers to build the derviation itself, Nix would behave quite differently.

fib-serialized.nix:

let
  fib-1 = fib (n - 1);
  fib-2 = fib (n - 2);

  n-1-str = builtins.toString (n - 1);
  n-2-str = builtins.toString (n - 2);

  fib-1-answer = nixpkgs.lib.removeSuffix "\n"
    (builtins.readFile "${fib-1}/answer");
  fib-2-answer = nixpkgs.lib.removeSuffix "\n"
    (builtins.readFile "${fib-2}/answer");
in
stdenv.mkDerivation {
  name = "${prefix}-fib-${n-str}";
  unpackPhase = "true";

  buildPhase = ''
    echo "Calculating the answer of fib(${n-str}).."
    echo "Given fib(${n-1-str}) = ${fib-1-answer},"
    echo "and given fib(${n-2-str}) = ${fib-2-answer}.."

    sleep 3

    answer=$(( ${fib-1-answer} + ${fib-2-answer} ))
    echo "The answer to fib(${n-str}) is $answer"
  '';
  ...
}

Let's try to instantiate the serialized version of fib(4):

$ time nix-instantiate -E "import ./code/04-derivations/03-fibonacci/fib-serialized.nix \"$(date +%s)\" 4"
building '/nix/store/97h18adc1358s8ri9mjzmnbvbbsj7p0a-1606145205-fib-1.drv'...
...
Producing base case fib(1)...
The answer to fib(1) is 1
...
building '/nix/store/fyizpk51qr2k6pm6v2pbqynfqf8ws68p-1606145205-fib-0.drv'...
...
Producing base case fib(0)...
The answer to fib(0) is 0
...
building '/nix/store/8qglzk0vq984mx65f3993rqjpipc3q9j-1606145205-fib-2.drv'...
...
Calculating the answer of fib(2)..
Given fib(1) = 1,
and given fib(0) = 0..
The answer to fib(2) is 1
...
building '/nix/store/4p8xy858gwc348576h6rz39a3l7wk64l-1606145205-fib-3.drv'...
...
Calculating the answer of fib(3)..
Given fib(2) = 1,
and given fib(1) = 1..
The answer to fib(3) is 2
...
/nix/store/c29ap9ljazs7k0jx687hnm3s0rgsz2vm-1606145205-fib-4.drv

real    0m20,016s
user    0m1,221s
sys     0m0,261s

What happened here? fib(0) to fib(3) are built even though we are just instantiating fib(4).

Inspecting Input Derivation

Showing the derivation of fib-4.drv gives us a better idea:

$ nix show-derivation /nix/store/c29ap9ljazs7k0jx687hnm3s0rgsz2vm-1606145205-fib-4.drv
{
  "/nix/store/c29ap9ljazs7k0jx687hnm3s0rgsz2vm-1606145205-fib-4.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/fgq0j5mlqpy99mfdfc3v4bvbd6wr2slg-1606145205-fib-4"
      }
    },
    ...
    "inputDrvs": {
      "/nix/store/l54djrh1n7d8zdfn26w7v6zjh5wp7faa-bash-4.4-p23.drv": [
        "out"
      ],
      "/nix/store/x9why09hwx2pcnmw0fw7hhh1511hyskl-stdenv-linux.drv": [
        "out"
      ]
    },
    "env": {
      "buildInputs": "",
      "buildPhase": "echo \"Calculating the answer of fib(4)..\"\necho \"Given fib(3) = 2,\"\necho \"and given fib(2) = 1..\"\n\nsleep 3\n\nanswer=$(( 2 + 1 ))\necho \"The answer to fib(4) is $answer\"\n",
  ...

Thanks to builtins.readFile, the results for fib(3) and fib(2) have in fact been calculated and inlined inside the the derivation itself. They are no longer listed in the input derivation.

Caching Problem of Evaluation-Time Dependencies

In fact, fib(3) and fib(2) are not even shown as dependencies in fib(4) anymore:

$ nix-store -qR --include-outputs /nix/store/c29ap9ljazs7k0jx687hnm3s0rgsz2vm-1606145205-fib-4.drv | grep fib
/nix/store/c29ap9ljazs7k0jx687hnm3s0rgsz2vm-1606145205-fib-4.drv
/nix/store/fgq0j5mlqpy99mfdfc3v4bvbd6wr2slg-1606145205-fib-4

This has a consequence in caching Nix dependencies. Not knowing fib(0) to fib(3) are actually input to fib(4), it would be difficult to properly cache these dependencies to Cachix.

Import From Derivation

Evaluation-time dependencies can also occur if we import a .nix file from a derivation. Since Nix has to read the file content to be able to import the .nix file, the derivation has to be built at evaluation time. This is more commonly known as Import From Derivation, or IFD in short.

IFD should be avoided if possible. However as we will see in later chapters, there are valid use cases that can only be solved using IFD.

Non-Lazy Build

If we build fib(4) now, indeed only fib(4) itself is being built.

$ nix-build /nix/store/c29ap9ljazs7k0jx687hnm3s0rgsz2vm-1606145205-fib-4.drv
these derivations will be built:
  /nix/store/c29ap9ljazs7k0jx687hnm3s0rgsz2vm-1606145205-fib-4.drv
building '/nix/store/c29ap9ljazs7k0jx687hnm3s0rgsz2vm-1606145205-fib-4.drv'...
...
Calculating the answer of fib(4)..
Given fib(3) = 2,
and given fib(2) = 1..
The answer to fib(4) is 3
...
/nix/store/fgq0j5mlqpy99mfdfc3v4bvbd6wr2slg-1606145205-fib-4

The same effect can also happen if we import .nix files from derivation outputs.

Lesson learnt: parallelization in Nix can still be tricky. Try your best to include dependencies as input derivations, and lazily refer to the built output of dependencies only during build time.

Raw Derivation

We have previously used stdenv.mkDerivation to define toy derivations without looking into how derivations work. Here we will go deeper into Nix derivations, starting with the most basic derivation construct, builtins.derivation.

From the repl, we can see that builtins.derivation is a function:

nix-repl> builtins.derivation
«lambda @ /nix/store/qxayqjmlpqnmwg5yfsjjayw220ls8i2r-nix-2.3.8/share/nix/corepkgs/derivation.nix:4:1»

Since builtins.derviation is more primitive as compared to stdenv.mkDerivation, the way we can build a derivation is also more involved:

nix-repl> builtins.derivation {
            name = "hello";
            system = builtins.currentSystem;
            builder = "${nixpkgs.bash}/bin/bash";
            args = [
              "-c"
              ''
              echo "Hello World!" > $out
              ''
            ];
          }
«derivation /nix/store/hbsv13kn5imfri16f6g2l5c2jy6dfmxl-hello.drv»

System

First we have to supply a system attribute, which we set it to the current OS we are running on. It is most common to have the system values as "x86_64-linux" or "x86_64-darwin".

nix-repl> builtins.currentSystem
"x86_64-linux"

The system attribute is required because Nix supports cross compilation. So we can also define derivations that are built on different platforms than the one we are on.

Builder

The builder attribute expects a file path to an executable script that is called when the derivation is built. To keep things simple, we use the bash shell from nixpkgs.bash as the builder program.

The args attribute is used to specify the command line line arguments passed to the builder program. Since bash itself do not know how to build the program we want, we pass the command string using -c to execute the bash script echo "Hello World!" > $out

Now we can try to build the derivation and see that it works:

$ nix-build /nix/store/hbsv13kn5imfri16f6g2l5c2jy6dfmxl-hello.drv
these derivations will be built:
  /nix/store/hbsv13kn5imfri16f6g2l5c2jy6dfmxl-hello.drv
building '/nix/store/hbsv13kn5imfri16f6g2l5c2jy6dfmxl-hello.drv'...
/nix/store/dsgf85gxzw167v320sy08as72c0hk8wd-hello

$ cat /nix/store/dsgf85gxzw167v320sy08as72c0hk8wd-hello
Hello World!

Explicit Dependencies

Inside builtins.derivation, almost all dependencies have to be provided explicitly, even the bash shell that we are running on. Since we specify bash as the builder program, it is also shown in the list of inputDrvs of our derivation.

$ nix show-derivation /nix/store/hbsv13kn5imfri16f6g2l5c2jy6dfmxl-hello.drv
{
  "/nix/store/hbsv13kn5imfri16f6g2l5c2jy6dfmxl-hello.drv": {
    "outputs": {
      "out": {
        "path": "/nix/store/dsgf85gxzw167v320sy08as72c0hk8wd-hello"
      }
    },
    "inputSrcs": [],
    "inputDrvs": {
      "/nix/store/l54djrh1n7d8zdfn26w7v6zjh5wp7faa-bash-4.4-p23.drv": [
        "out"
      ]
    },
    "platform": "x86_64-linux",
    "builder": "/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash",
    "args": [
      "-c",
      "echo \"Hello World!\" > $out\n"
    ],
    "env": {
      "builder": "/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash",
      "name": "hello",
      "out": "/nix/store/dsgf85gxzw167v320sy08as72c0hk8wd-hello",
      "system": "x86_64-linux"
    }
  }
}

Inspecting the Build Environment

We can use the env command to inspect the environment variables inside our build script. Let's try and build a derivation that prints the environment to the terminal.

nix-repl> builtins.derivation {
            name = "env";
            system = builtins.currentSystem;
            builder = "${nixpkgs.bash}/bin/bash";
            args = [
              "-c"
              ''
                set -x
                ls -la .
                ls -la /
                env
                touch $out
              ''
            ];
          }
«derivation /nix/store/4nq2kgcmryhwjh5sg05jgwsd4ixh81ia-env.drv»
$ nix-build /nix/store/4nq2kgcmryhwjh5sg05jgwsd4ixh81ia-env.drv
+ nix-build /nix/store/4nq2kgcmryhwjh5sg05jgwsd4ixh81ia-env.drv
these derivations will be built:
  /nix/store/4nq2kgcmryhwjh5sg05jgwsd4ixh81ia-env.drv
building '/nix/store/4nq2kgcmryhwjh5sg05jgwsd4ixh81ia-env.drv'...
+ ls -la .
bash: line 1: ls: command not found
+ ls -la /
bash: line 2: ls: command not found
+ env
bash: line 3: env: command not found
+ touch /nix/store/blcl4m2vgga6i86kh13nqlvx1l2ha7v5-env
bash: line 4: touch: command not found
builder for '/nix/store/4nq2kgcmryhwjh5sg05jgwsd4ixh81ia-env.drv' failed with exit code 127
error: build of '/nix/store/4nq2kgcmryhwjh5sg05jgwsd4ixh81ia-env.drv' failed

Not good, with builtins.derivation, not even basic commands like ls, env, and touch are provided. (As seen previously, echo is provided though)

Instead, we also have to specify our build dependencies explicitly with nixpkgs.coreutils providing the basic shell commands:

nix-repl> builtins.derivation {
            name = "env";
            system = builtins.currentSystem;
            builder = "${nixpkgs.bash}/bin/bash";
            args = [
              "-c"
              ''
                set -x
                ${nixpkgs.coreutils}/bin/ls -la .
                ${nixpkgs.coreutils}/bin/ls -la /
                ${nixpkgs.coreutils}/bin/env
                ${nixpkgs.coreutils}/bin/touch $out
              ''
            ];
          }
«derivation /nix/store/c4bp5bvx73fz9jf1si64i00as30k9fga-env.drv»
$ nix-build /nix/store/c4bp5bvx73fz9jf1si64i00as30k9fga-env.drv
these derivations will be built:
  /nix/store/c4bp5bvx73fz9jf1si64i00as30k9fga-env.drv
building '/nix/store/c4bp5bvx73fz9jf1si64i00as30k9fga-env.drv'...
+ /nix/store/2shqhfsyzz4rnfyysbzgyp5kbfk29750-coreutils-8.32/bin/ls -la .
total 8
drwx------ 2 nixbld nixbld 4096 Nov 30 19:09 .
drwxr-x--- 9 nixbld nixbld 4096 Nov 30 19:09 ..
+ /nix/store/2shqhfsyzz4rnfyysbzgyp5kbfk29750-coreutils-8.32/bin/ls -la /
total 32
drwxr-x---   9 nixbld nixbld  4096 Nov 30 19:09 .
drwxr-x---   9 nixbld nixbld  4096 Nov 30 19:09 ..
drwxr-xr-x   2 nixbld nixbld  4096 Nov 30 19:09 bin
drwx------   2 nixbld nixbld  4096 Nov 30 19:09 build
drwxr-xr-x   4 nixbld nixbld  4096 Nov 30 19:09 dev
drwxr-xr-x   2 nixbld nixbld  4096 Nov 30 19:09 etc
drwxr-xr-x   3 nixbld nixbld  4096 Nov 30 19:09 nix
dr-xr-xr-x 410 nobody nogroup    0 Nov 30 19:09 proc
drwxrwxrwt   2 nixbld nixbld  4096 Nov 30 19:09 tmp
+ /nix/store/2shqhfsyzz4rnfyysbzgyp5kbfk29750-coreutils-8.32/bin/env
out=/nix/store/3rv14i75j4wyp6n9fila5rll4f99yksi-env
builder=/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash
NIX_LOG_FD=2
system=x86_64-linux
PWD=/build
HOME=/homeless-shelter
TMP=/build
NIX_STORE=/nix/store
TMPDIR=/build
name=env
TERM=xterm-256color
TEMPDIR=/build
SHLVL=1
NIX_BUILD_CORES=8
TEMP=/build
PATH=/path-not-set
NIX_BUILD_TOP=/build
_=/nix/store/2shqhfsyzz4rnfyysbzgyp5kbfk29750-coreutils-8.32/bin/env
+ /nix/store/2shqhfsyzz4rnfyysbzgyp5kbfk29750-coreutils-8.32/bin/touch /nix/store/3rv14i75j4wyp6n9fila5rll4f99yksi-env
/nix/store/3rv14i75j4wyp6n9fila5rll4f99yksi-env

Nix Sandbox

From above we can see that the build environment inside a Nix build script is sandboxed.

According to Nix manual:

If set to true, builds will be performed in a sandboxed environment, i.e., they’re isolated from the normal file system hierarchy and will only see their dependencies in the Nix store, the temporary build directory, private versions of /proc, /dev, /dev/shm and /dev/pts (on Linux), and the paths configured with the sandbox-paths option. This is useful to prevent undeclared dependencies on files in directories such as /usr/bin. In addition, on Linux, builds run in private PID, mount, network, IPC and UTS namespaces to isolate them from other processes in the system (except that fixed-output derivations do not run in private network namespace to ensure they can access the network).

Nix sandbox should be enabled by default. You can check your sandbox configuration with:

$ nix show-config | grep sandbox
extra-sandbox-paths =
sandbox = true
sandbox-build-dir = /build
sandbox-dev-shm-size = 50%
sandbox-fallback = true
sandbox-paths = /bin/sh=/nix/store/w0xp1k96c1dvmx6m4wl1569cdzy47w5r-busybox-1.31.1-x86_64-unknown-linux-musl/bin/busybox

Capturing Build Environment

We can capture the build environment as a file by saving the output of env to $out.

nix-repl> builtins.derivation {
            name = "env";
            system = builtins.currentSystem;
            builder = "${nixpkgs.bash}/bin/bash";
            args = [
              "-c"
              "${nixpkgs.coreutils}/bin/env > $out"
            ];
          }
«derivation /nix/store/39ah25v6iwlka3jl2angxrlx00mk2ijd-env.drv»
$ nix-build /nix/store/39ah25v6iwlka3jl2angxrlx00mk2ijd-env.drv
these derivations will be built:
  /nix/store/39ah25v6iwlka3jl2angxrlx00mk2ijd-env.drv
building '/nix/store/39ah25v6iwlka3jl2angxrlx00mk2ijd-env.drv'...
/nix/store/6kjgg8j3y44g1ja95swqdd1v8xp6mwi1-env

$ cat /nix/store/6kjgg8j3y44g1ja95swqdd1v8xp6mwi1-env
out=/nix/store/6kjgg8j3y44g1ja95swqdd1v8xp6mwi1-env
builder=/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash
NIX_LOG_FD=2
system=x86_64-linux
PWD=/build
HOME=/homeless-shelter
TMP=/build
NIX_STORE=/nix/store
TMPDIR=/build
name=env
TERM=xterm-256color
TEMPDIR=/build
SHLVL=1
NIX_BUILD_CORES=8
TEMP=/build
PATH=/path-not-set
NIX_BUILD_TOP=/build
_=/nix/store/2shqhfsyzz4rnfyysbzgyp5kbfk29750-coreutils-8.32/bin/env

Nix Shell

Nix achieves reproducible build by carefully setting/unsetting the appropriate environment variables, so that our derivations are always built with the same environment regardless of where it is being built.

However since the derivation is built in a sandboxed environment, it may be difficult to debug when there are build errors, or rapid prototyping with the source code changed frequently.

We can get almost the same environment as inside nix build by entering a Nix shell.

$ nix-shell --pure --run env /nix/store/39ah25v6iwlka3jl2angxrlx00mk2ijd-env.drv
__ETC_PROFILE_SOURCED=1
DISPLAY=:1
out=/nix/store/6kjgg8j3y44g1ja95swqdd1v8xp6mwi1-env
builder=/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash
USER=user
system=x86_64-linux
PWD=/path/to/nix-workshop
HOME=/home/user
TMP=/run/user/1000
NIX_STORE=/nix/store
TMPDIR=/run/user/1000
name=env
IN_NIX_SHELL=pure
TERM=xterm-256color
TEMPDIR=/run/user/1000
SHLVL=3
NIX_BUILD_CORES=8
TEMP=/run/user/1000
LOGNAME=user
PATH=/nix/store/lf467z8nr5y50q1vqnlbhpv2jachx3cs-bash-interactive-4.4-p23/bin:/home/user/.nix-profile/bin:...
NIX_BUILD_TOP=/run/user/1000
_=/usr/bin/env

Our pure Nix environment look pretty similar to the environment we captured in nix-build. There are however a few differences, in particular with $PATH.

According the manual for the --pure option in nix-shell:

If this flag is specified, the environment is almost entirely cleared before the interactive shell is started, so you get an environment that more closely corresponds to the “real” Nix build. A few variables, in particular HOME, USER and DISPLAY, are retained. Note that (depending on your Bash installation) /etc/bashrc is still sourced, so any variables set there will affect the interactive shell.

We can compare the differences by diffing the output of both environments:

$ drv=/nix/store/39ah25v6iwlka3jl2angxrlx00mk2ijd-env.drv
$ diff --color <(cat $(nix-build $drv)) <(nix-shell $drv --pure --run env)

In contrast, the default impure Nix shell keeps all existing environment variables, and only add or override variables that are introduced by the derivation.

$ nix-shell $drv --run env

Environment Variables

If we observe the captured build environment, almost all attributes we passed to builtins.derivation are converted into environment variables.

In fact, we can define any number of attributes to be used as environment variables inside our build script.

nix-repl> builtins.derivation {
            name = "foo";
            foo = "foo val";
            system = builtins.currentSystem;
            builder = "${nixpkgs.bash}/bin/bash";
            args = [
              "-c"
              "echo $foo > $out"
            ];
          }
«derivation /nix/store/v1i0khcvxy5bkyv2iq0kqzhcbfcfml8m-foo.drv»

We can see from the build output that the value of $foo is in fact captured.

$ nix-build /nix/store/v1i0khcvxy5bkyv2iq0kqzhcbfcfml8m-foo.drv
these derivations will be built:
  /nix/store/v1i0khcvxy5bkyv2iq0kqzhcbfcfml8m-foo.drv
building '/nix/store/v1i0khcvxy5bkyv2iq0kqzhcbfcfml8m-foo.drv'...
/nix/store/zmgp33rl2sh3l32syhq4h8gph3f4s1k9-foo

$ cat /nix/store/zmgp33rl2sh3l32syhq4h8gph3f4s1k9-foo
foo val

We can also get the same $foo variable set when entering Nix shell:

$ nix-shell --pure --run 'echo $foo' /nix/store/v1i0khcvxy5bkyv2iq0kqzhcbfcfml8m-foo.drv
foo val

Setting Dependencies as Variables

We can set out dependencies as custom attributes in a derivation and then refer to them as environment variables during the build.

For example, we can add the greet package we defined earlier and set it as $greet in the shell.

nix-repl> greet = import ./04-derivations/02-dependencies/greet.nix

nix-repl> builtins.derivation {
            inherit greet;
            name = "greet-alice";
            system = builtins.currentSystem;
            builder = "${nixpkgs.bash}/bin/bash";
            args = [
              "-c"
              "$greet/bin/greet Alice > $out"
            ];
          }
«derivation /nix/store/68gdf6z0rjcyl8xcwix3gfafndsa50jj-greet-alice.drv»
$ nix-build /nix/store/68gdf6z0rjcyl8xcwix3gfafndsa50jj-greet-alice.drv
these derivations will be built:
  /nix/store/68gdf6z0rjcyl8xcwix3gfafndsa50jj-greet-alice.drv
building '/nix/store/68gdf6z0rjcyl8xcwix3gfafndsa50jj-greet-alice.drv'...
/nix/store/dd290zmn983fs1w33nnq9gyh3cnj2jif-greet-alice

$ cat /nix/store/dd290zmn983fs1w33nnq9gyh3cnj2jif-greet-alice
Hello, Alice!

Standard Derivation

builtins.derivation provides the minimal functionality to define a Nix derivation. However all dependencies have to be manually managed, which can be pretty cumbersome. In practice, most Nix derivations are built on top of stdenv.mkDerivation, which provide many battery-included functionalities that helps make defining derivations easy.

The tradeoff is that stdenv.mkDerivation is much more complex than builtins.derivation. With the detour to understand builtins.derivation first, we can hopefully have an easier time understanding stdenv.mkDerivation

Inspecting Build Environment

We can inspect the standard environment in similar way.

nix-repl> nixpkgs.stdenv.mkDerivation {
            name = "inspect";
            unpackPhase = "true";

            buildPhase = ''
              set -x
              ls -la .
              ls -la /
              env
              set +x
            '';

            installPhase = "touch $out";
          }
«derivation /nix/store/vdyp9cxs0li87app03vm8zbxmq0lhw5l-inspect.drv»
$ nix-build /nix/store/vdyp9cxs0li87app03vm8zbxmq0lhw5l-inspect.drv
these derivations will be built:
  /nix/store/vdyp9cxs0li87app03vm8zbxmq0lhw5l-inspect.drv
building '/nix/store/vdyp9cxs0li87app03vm8zbxmq0lhw5l-inspect.drv'...
unpacking sources
patching sources
configuring
no configure script, doing nothing
building
++ ls -la .
total 16
drwx------ 2 nixbld nixbld 4096 Nov 30 19:42 .
drwxr-x--- 9 nixbld nixbld 4096 Nov 30 19:42 ..
-rw-r--r-- 1 nixbld nixbld 5013 Nov 30 19:42 env-vars
++ ls -la /
total 32
drwxr-x---   9 nixbld nixbld  4096 Nov 30 19:42 .
drwxr-x---   9 nixbld nixbld  4096 Nov 30 19:42 ..
drwxr-xr-x   2 nixbld nixbld  4096 Nov 30 19:42 bin
drwx------   2 nixbld nixbld  4096 Nov 30 19:42 build
drwxr-xr-x   4 nixbld nixbld  4096 Nov 30 19:42 dev
drwxr-xr-x   2 nixbld nixbld  4096 Nov 30 19:42 etc
drwxr-xr-x   3 nixbld nixbld  4096 Nov 30 19:42 nix
dr-xr-xr-x 405 nobody nogroup    0 Nov 30 19:42 proc
drwxrwxrwt   2 nixbld nixbld  4096 Nov 30 19:42 tmp
++ env
...
unpackPhase=true
propagatedBuildInputs=
stdenv=/nix/store/ajq5dfwn4hzlx1qf2xxwb6rj8a7s65nm-stdenv-linux
TZ=UTC
OLDPWD=/build
out=/nix/store/a226brzfy71vr6vkfy4m188qs9f7k7g7-inspect
CONFIG_SHELL=/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash
buildInputs=
builder=/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash
...
buildPhase=set -x
ls -la .
ls -la /
env
set +x

PATH=/nix/store/cr86kfhzfwa558mzav4rnfkbz00hw27w-patchelf-0.12/bin:/nix/store/ppfvi0cfcpdr83klw5kx6si2l260n1gh-gcc-wrapper-9.3.0/bin:...
NIX_BUILD_TOP=/build
depsBuildTargetPropagated=
NIX_ENFORCE_PURITY=1
SIZE=size
nativeBuildInputs=
LD=ld
patches=
depsTargetTargetPropagated=
_=/nix/store/2shqhfsyzz4rnfyysbzgyp5kbfk29750-coreutils-8.32/bin/env
++ set +x
installing
post-installation fixup
shrinking RPATHs of ELF executables and libraries in /nix/store/a226brzfy71vr6vkfy4m188qs9f7k7g7-inspect
strip is /nix/store/bnjps68g8ax6abzvys2xpx12imrx8949-binutils-2.31.1/bin/strip
patching script interpreter paths in /nix/store/a226brzfy71vr6vkfy4m188qs9f7k7g7-inspect
checking for references to /build/ in /nix/store/a226brzfy71vr6vkfy4m188qs9f7k7g7-inspect...
/nix/store/a226brzfy71vr6vkfy4m188qs9f7k7g7-inspect

As we can see, our standard environment is quite more complicated than the minimal environment provided by builtins.derivation. We also have a number of executables added to $PATH, which we can use without specifying them as dependencies.

Capturing the Build Environment

nix-repl> nixpkgs.stdenv.mkDerivation {
            name = "env";
            unpackPhase = "true";
            installPhase = "env > $out";
          }
«derivation /nix/store/5rgcvwndbc4525ypbb0r1vgqpbxgcy2g-env.drv»
$ cat $(nix-build /nix/store/5rgcvwndbc4525ypbb0r1vgqpbxgcy2g-env.drv)
...
unpackPhase=true
propagatedBuildInputs=
stdenv=/nix/store/ajq5dfwn4hzlx1qf2xxwb6rj8a7s65nm-stdenv-linux
TZ=UTC
OLDPWD=/build
out=/nix/store/rkjhcjhdj6ba7r7n7fasq8gmzxi5hk72-env
CONFIG_SHELL=/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash
buildInputs=
builder=/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash
...

Build Inputs

stdenv.mkDerivation also provides a convenient way of adding dependencies to appropriate environment variables with the buildInputs attribute.

nix-repl> nixpkgs.stdenv.mkDerivation {
            name = "greet-alice";
            buildInputs = [ greet ];

            unpackPhase = "true";
            installPhase = "greet Alice > $out";
          }
«derivation /nix/store/in40c5fl13ziqzds3wfg2ag7ax2xmq5l-greet-alice.drv»
$ nix-build /nix/store/in40c5fl13ziqzds3wfg2ag7ax2xmq5l-greet-alice.drv
these derivations will be built:
  /nix/store/in40c5fl13ziqzds3wfg2ag7ax2xmq5l-greet-alice.drv
building '/nix/store/in40c5fl13ziqzds3wfg2ag7ax2xmq5l-greet-alice.drv'...
...
/nix/store/kp32rzq63barqa55q3mf761gsggi2bq6-greet-alice

$ cat /nix/store/kp32rzq63barqa55q3mf761gsggi2bq6-greet-alice
Hello, Alice!

We can check that greet is added to $PATH using Nix shell:

$ drv=/nix/store/in40c5fl13ziqzds3wfg2ag7ax2xmq5l-greet-alice.drv

$ nix-shell $drv --pure --run "command -v greet"
/nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet/bin/greet

$ nix-shell $drv --pure --run 'echo $PATH' | tr ':' '\n'
...
/nix/store/2shqhfsyzz4rnfyysbzgyp5kbfk29750-coreutils-8.32/bin
/nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet/bin
...

stdenv also adds the build inputs to other variables.

$ nix-shell $drv --pure --run 'echo $NIX_LDFLAGS'
-rpath /nix/store/kp32rzq63barqa55q3mf761gsggi2bq6-greet-alice/lib64 -rpath /nix/store/kp32rzq63barqa55q3mf761gsggi2bq6-greet-alice/lib

Note that the paths /nix/store/kp32rzq63barqa55q3mf761gsggi2bq6-greet-alice/lib does not exist, but stdenv still sets the variables anyway.

Stdenv Script

How do stdenv.mkDerivation do the magic compared to builtins.derivation? We can find out by first inspecting the derivation:

$ nix show-derivation $drv
{
  "/nix/store/in40c5fl13ziqzds3wfg2ag7ax2xmq5l-greet-alice.drv": {
    ...
    "builder": "/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash",
    "args": [
      "-e",
      "/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"
    ],
    "env": {
      "buildInputs": "/nix/store/l6xy4qjr8x3ni16skfilw0fvnda13szq-greet",
      "builder": "/nix/store/qdp56fi357fgxxnkjrwx1g67hrk775im-bash-4.4-p23/bin/bash",
      ...
      "installPhase": "greet Alice > $out",
      "name": "greet-alice",
      ...
      "stdenv": "/nix/store/ajq5dfwn4hzlx1qf2xxwb6rj8a7s65nm-stdenv-linux",
      ...
    }

  }
}

stdenv is also using bash as the builder, and have it evaluate the script at /nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh. Let's see what's inside there:

$ cat /nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh
source $stdenv/setup
genericBuild

So the magic is hidden inside $stdenv/setup, with the $stdenv variable set to /nix/store/ajq5dfwn4hzlx1qf2xxwb6rj8a7s65nm-stdenv-linux.

We can open it and see what's inside.

nix-shell $drv --run 'cat $stdenv/setup'

There are quite a lot of shell scripts happening. If we search through the script, we can see that environment variables such as buildInputs, buildPhase, and installPhase are being referred inside $stdenv/setup.

In other words, instead of having to figure how to setup various environment variables to work with various dependencies, $stdenv/setup provides a higher level abstraction of doing the setup for us. We just have to define the build inputs and steps that we need, and $stdenv/setup will fill in the missing pieces from us.

In fact, $stdenv/setup is also being sourced when we enter a Nix shell of a stdenv derivation. From the nix-shell manual:

The command nix-shell will build the dependencies of the specified derivation, but not the derivation itself. It will then start an interactive shell in which all environment variables defined by the derivation path have been set to their corresponding values, and the script $stdenv/setup has been sourced. This is useful for reproducing the environment of a derivation for development.

Deriving Environment at Build Time

One question we might ask is, why is stdenv doing the heavy lifting steps only at build time inside a shell script. We could as well parse the dependencies inside Nix at evaluation time, and produce a derivation with everything setup already.

However recall from the previous example of fib-serialized.nix. If we try to peek into the content of a dependency derivation, that would instead become an evaluation time dependency. If stdenv is looking into the content of all dependencies inside Nix, then we can only know how to build the derivation after all dependencies have been built.

Instead, stdenv avoids this to allow the derivation dependencies to be built in parallel by Nix. With that, we can only read the content of our dependencies at build time, which happens inside the build script.

Build Phases

stdenv provides many different phases, with default behavior of what to run if no script for that phase is provided.

Many of the phases follow the build steps introduced by Autotools. When we are building non-C/C++ projects, only a few phases are essential. Still, it is useful to take a quick look at what phases are there, and what they offers.

The nixpkgs manual has the full list of phases.

The phases Attribute

We can force stdenv to run only specific phases by specifying them in the phases attribute.

nix-repl> nixpkgs.stdenv.mkDerivation {
            name = "hello";
            phases = [ "installPhase" ];
            installPhase = "echo 'Hello World!' > $out";
          }
«derivation /nix/store/m09hj2xs3yc45y3d4rdm8wks7cay00ak-hello.drv»
$ nix-build /nix/store/m09hj2xs3yc45y3d4rdm8wks7cay00ak-hello.drv
these derivations will be built:
  /nix/store/m09hj2xs3yc45y3d4rdm8wks7cay00ak-hello.drv
building '/nix/store/m09hj2xs3yc45y3d4rdm8wks7cay00ak-hello.drv'...
installing
/nix/store/hpj3y6as9s07444qi6nap0f5dp5k84b6-hello

$ cat /nix/store/hpj3y6as9s07444qi6nap0f5dp5k84b6-hello
Hello World!

You may notice that the build log for this version of hello is much shorter than the usual output of standard derivations. Messages such as post-installation fixup are not shown here.

This is because those are implicit steps performed in phases such as fixupPhase. We will go through later why those phases are there. But as you can see, these phases can be disabled by explicitly specifying the phases to run.

There is also no need to explicitly skip required phases like unpackPhase, which we previously set to true.

Unpack Phase

The unpack phase is used to unpack source code into temporary directories to be used for compilation. By default, unpackPhase unpacks the source code specified in $src, and if none is provided, it will abort with an error.

nix-repl> nixpkgs.stdenv.mkDerivation {
            name = "hello";
            installPhase = "echo 'Hello World!' > $out";
          }
«derivation /nix/store/cx3p30jp0y0l8ixl426drsp81vcqagpr-hello.drv»
$ nix-build /nix/store/cx3p30jp0y0l8ixl426drsp81vcqagpr-hello.drv
these derivations will be built:
  /nix/store/cx3p30jp0y0l8ixl426drsp81vcqagpr-hello.drv
building '/nix/store/cx3p30jp0y0l8ixl426drsp81vcqagpr-hello.drv'...
unpacking sources
variable $src or $srcs should point to the source
builder for '/nix/store/cx3p30jp0y0l8ixl426drsp81vcqagpr-hello.drv' failed with exit code 1
error: build of '/nix/store/cx3p30jp0y0l8ixl426drsp81vcqagpr-hello.drv' failed

This is why when we don't have any source code, we have to explicitly skip the unpackPhase by telling it to run true instead.

Let's try to see what is done in unpackPhase when we give it some source directories.

nix-repl> nixpkgs.stdenv.mkDerivation {
            name = "fibonacci-src";
            src = ./04-derivations/03-fibonacci;
            installPhase = ''
              set -x

              pwd
              ls -la .
              cp -r ./ $out/

              set +x
            '';
          }
«derivation /nix/store/8l0s01nk0fc1zicb9qkdmpwsw01qr5p8-fibonacci.drv»
$ nix-build /nix/store/8l0s01nk0fc1zicb9qkdmpwsw01qr5p8-fibonacci.drv
these derivations will be built:
  /nix/store/8l0s01nk0fc1zicb9qkdmpwsw01qr5p8-fibonacci.drv
building '/nix/store/8l0s01nk0fc1zicb9qkdmpwsw01qr5p8-fibonacci.drv'...
unpacking sources
unpacking source archive /nix/store/a5f73yy0a8dn0p12pfriqbqyag0ksfkq-03-fibonacci
source root is 03-fibonacci
patching sources
configuring
no configure script, doing nothing
building
no Makefile, doing nothing
installing
++ pwd
/build/03-fibonacci
++ ls -la .
total 16
drwxr-xr-x 2 nixbld nixbld 4096 Jan  1  1970 .
drwx------ 3 nixbld nixbld 4096 Dec  1 09:19 ..
-rw-r--r-- 1 nixbld nixbld 1772 Jan  1  1970 fib-serialized.nix
-rw-r--r-- 1 nixbld nixbld 1602 Jan  1  1970 fib.nix
++ cp -r ./ /nix/store/qqd4msyqya0xhqxcyra0lf7v09z2q522-fibonacci/
++ set +x
post-installation fixup
shrinking RPATHs of ELF executables and libraries in /nix/store/qqd4msyqya0xhqxcyra0lf7v09z2q522-fibonacci
strip is /nix/store/bnjps68g8ax6abzvys2xpx12imrx8949-binutils-2.31.1/bin/strip
patching script interpreter paths in /nix/store/qqd4msyqya0xhqxcyra0lf7v09z2q522-fibonacci
checking for references to /build/ in /nix/store/qqd4msyqya0xhqxcyra0lf7v09z2q522-fibonacci...
/nix/store/qqd4msyqya0xhqxcyra0lf7v09z2q522-fibonacci

$ ls -la /nix/store/qqd4msyqya0xhqxcyra0lf7v09z2q522-fibonacci
total 5728
dr-xr-xr-x    2 user user    4096 Jan  1  1970 .
drwxr-xr-x 5403 user user 5849088 Dez  1 10:19 ..
-r--r--r--    1 user user    1602 Jan  1  1970 fib.nix
-r--r--r--    1 user user    1772 Jan  1  1970 fib-serialized.nix

As we can see, unpackPhase copies the content of the source code specified in $src into the temporary build directory. It also modifies the chmod permissions to allow write permission to the files and directories.

Patch Phase

Configure Phase

Build Phase

Check Phase

Install Phase

Fixup Phase

Dependency Management

We have seen in previous chapters that Nix makes it easy to construct complex build dependencies with several benefits:

  • Non-related dependencies can be built in parallel.
  • Reproducible builds make it easy to cache and reuse dependencies.

However Nix does not provide any mechanism for dependency resolution, e.g. choose from multiple versions of dependencies and determining the most suitable versions.

As an example, we will build hypothetical Nix packages resembling Minecraft crafting recipes with versioning schemes following semver. Let's first try to build our first version of pickaxe, which is made of wood:

pickaxe
  - 1.0.0
    - stick ^1.0.1
    - planks ~2.1.0
stick
  - 1.0.3
  - 1.1.2
  - 2.0.0
planks
  - 2.1.0
  - 2.1.1
  - 2.2.1

The first step in deciding the appropriate versions to be used to build pickaxe-1.0.0 is to rule out invalid versions. With that, stick-2.0.0 is ruled out because it is outside of the ^1.0.1 range. Similarly planks-2.2.1 is outside the bound for ~2.1.0.

After filtering out the invalid versions, there are still multiple versions of stick and planks available. As a result there can be multiple version candidates for building pickaxe-1.0.0. For example, we can use stick-1.0.3 and planks-2.1.1. But are those the best versions to be used?

Depending on the dependency resolution algorithm used, we may get different answers. Though in general, we can usually expect the algorithm to choose the latest versions that are compatible with the required range. So we should expect to get stick-1.1.2 and planks-2.1.1 as the answers.

Nested Dependencies

In reality, dependency resolution can be more complicated because of nested dependencies. Let's say both stick and planks both depend on wood:

stick
  - 1.0.3
    - wood ^1.5.0
  - 1.1.2
    - wood ~2.0.0

planks
  - 2.1.0
    - wood ^2.0.0
  - 2.1.1
    - wood ~2.3.0

wood
  - 1.5.0
  - 2.0.1
  - 2.3.2

In such case, the only solution is to use stick-1.1.2 and planks-2.1.0, because the other version combinations do not have a common wood version usable by both stick and planks.

Package Managers

Dependency resolution is a complex topic on its own. Different languages have their own package managers that deal with dependency resolution differently. e.g. cabal-install, npm, mvn, etc. There are also OS-level package managers that have to deal with dependencies resolution. e.g. apt (for Debian and Ubuntu), rpm (Fedora), pacman (Arch Linux, Manjaro), etc.

To support package management across multiple languages and multiple platforms, Nix has its own unique challenge of managing dependencies. At this point, Nix itself do not provide any mechanism for resolving dependencies. Instead Nix users have to come out with their own higher level design patterns to resolve dependencies, such as in nixpkgs.

Package Registry

For a dependency resolution algorithm to determine what versions of dependency to use, it must first refer to a registry that contains all versions available to all packages. Each package manager have their own registry, e.g. Hackage, npm registry, Debian registry, etc.

Package registries are usually mutable databases that are constantly updated. This creates an issue with reproducibility: the result given back from a dependency resolution algorithm depends on the mutable state of the registry at the time the algorithm is executed.

In other words, say if we try to resolve the dependencies of pickaxe-1.0.0 today, we may get stick-1.1.2 and planks-2.1.0. But if we resolve the same dependencies tomorrow, we might get stick-1.1.3 because new version of stick is published. To make it worse, stick-1.1.3 may contain unexpected changes that causes pickaxe-1.0.0 to break.

Version Pinning

Even without Nix, there is a strong use case to pin the versions to a particular snapshot of the registry. This is to make sure that, no matter when we try to resolve the dependencies, we will always get back the same dependencies.

Package Lock

One common approach is to create a lock file containing the result of running the dependency resolution algorithm, and include the lock file into the version control system (e.g. GIT). For instance the lock file could be package-lock.json (for npm) or cabal.project.freeze (for haskell projects). With the lock files available, we can even skip dependency resolution in the future, and just use the result in the lock file.

Registry Snapshot

An alternative approach would be to specify the snapshot of the package registry itself. For example, cabal accepts an index-state option for us to specify a timestamp of the Hackage snapshot that it should resolve the dependencies from. With that we can specify the timestamp of the time we first build our dependencies, and not worry about new versions of packages being added in the future.

However there can still be other variables that can affect the outcome. For example, the package manager itself may update the dependency resolution algorithm, so we may still get different results depending on the version of package manager used.

Upgrading Dependencies

The strategies for pinning dependencies does not eliminate the need to resolving the plans in the first place, or the need to upgrade or install new dependencies.

In the ideal world, we would like to be able to just specify the dependencies we need, and have the package manager give us the best versions that just work. But reality is messy, and dependencies can have breaking changes all the time.

Versioning Schemes

There are many attempts at coming up with versioning schemes that carry breakage information with them, such as semver and PVP. However they require package authors to manually follow the rules, and rules can be broken, intentionally or not.

Exponential Versions

In reality, each combination of dependency versions produce a unique version of the full package that needs to be tested. There is never just one version of pickaxe-1.0.0, but exponential number of versions of pickaxe-1.0.0 depending on the versions of stick, planks, and their transitive dependencies.

To make matters worse, real world software also tend to have implicit dependencies on the runtime environment, such as executables, shared libraries, and operating system APIs.

So for each of the versions of pickaxe-1.0.0 with pinned dependencies, we would also have multiple versions of that software for different platforms, e.g. Linux, MacOS, Windows, Android, iOS, etc. Even among these platforms, there are also multiple releases of the platform, e.g. Debian 10, Ubuntu 20.04, MacOS Big Sur, etc.

Mono Versioning

Despite all these complexities, we still like to pretend that there is only one or few versions of pickaxe-1.0.0 ever existed. One way to tame down this complexity is through mono versioning.

Monorepo

The simplest kind of mono versioning is by having a single repository that contains all its components and dependencies. For each commit in this repository, there is exactly one version each component and dependecy. We simply ignore the possibility of other valid combinations of component versions, and not support them.

Lockfiles in Monorepo

Package managers such as godep check the source code of dependencies into a monorepo. As an alternative, we can check just the lockfiles into the repository, and have the package managers fetch them separately.

Checking the lock file is still effectively mono-versioning the dependencies. For each commit in the repository, we support only the exact dependencies specified in the lockfile of that commit. We simply pretend that no other versions of the dependencies are available.

Mono Registry

Taking the idea to extreme, we can also freeze all dependencies in a package registry and provide only one version of each dependency at any point in time.

This is the approach for registries such as Stackage, which guarantees that all Haskell dependencies only have one version that always work.

Mono registry tend to work more cleanly together with monorepo. In a project, we can specify just the snapshot version of the mono registry that we are using, and there is no need for messy details such as generating the lockfiles in the first place.

Mono Environment

Nixpkgs is also a mono registry for the standard packages in Nix. For each version of nixpkgs, there is exactly one version of packages such as bash, gcc, etc. But since these packages used to be provided by operating systems, we can say that nixpkgs is also providing a mono environment to our software.

When we create a monorepo with pinned nixpkgs, we are not only providing exactly one version of each dependencies, but also exactly one version of the environment to run on.

Mono environment restricts the specification of our software so that it does not just run on platforms such as any version of Linux or any version of Debian 10. We just pretend that there is exact one version of OS as specified in nixpkgs.

Pros and Cons of Mono Versioning

There is a fundamental difference in philosophy between multi-versioning and mono-versioning that makes it difficult for the two camps to reconcile. At its core, there are a few factors involved.

Stability

Mono-versioning places much higher value in stability. People in this camp want to make sure each version of the software always work. They achieve that by significantly limiting the number of versions of the software, and thoroughly testing softwares before upgrading any version.

Mono-versioning tend to put emphasis in LTS (long term support) releases, where its components are guaranteed to not have any breaking changes and be given years of support.

Rapid Releases

Mono versioning tend to suffer in providing slower releases. When a new version of component is available, it has to be tested to not break any other component before the new version can be released.

In contrast, multi-versioning allows a new component to be released immediately. This allows software to independently upgrade the dependencies, at the risk of it may break on some of the software.

Blurring the Line

There is no clear cut off whether the mono-versioning or multi-versioning approaches are better. In practice, we tend to take a hybrid approach in large software projects.

For example, in a company each team may have different monorepos for their projects to manage their own dependencies. The full release of the software suite is then a multi-versioning combination of each team's projects, which can break during development. Finally in production, the exact versions of each subprojects are pinned to specific versions before deployment.

Although Nix is more suitable for mono-versioning development, some of its features also make it easier to manage multi-versioned projects, by building a mono "Nixified" version of the projects.

Basic Haskell Project

We will take a quick look on the Nix structure for a trivial Haskell project, in haskell-project-v1.

Main.hs:

module Main where

main :: IO ()
main = putStrLn "Hello, Haskell!"

haskell-project.cabal:

cabal-version:       2.4
name:                haskell-project
version:             0.1.0.0
license:             ISC
build-type:          Simple

executable hello
  main-is:             Main.hs
  build-depends:       base >=4.13
  default-language:    Haskell2010

Nix Dependency Management with Niv

We will use multiple Nix sources including nixpkgs and Haskell.nix in our Haskell projects. However specifying the Nix dependencies explicitly like in nixpkgs.nix can be a bit cumbersome.

let
  nixpkgs-src = builtins.fetchTarball {
    url = "https://github.com/NixOS/nixpkgs/archive/fcc81bc974fabd86991b8962bd30a47eb43e7d34.tar.gz";
    sha256 = "1ysjmn79pl7srlzgfr35nsxq43rm1va8dqp60h09nlmw2fsq9zrc";
  };

  nixpkgs = import nixpkgs-src {};
in
nixpkgs

Instead we can use niv to manage the dependencies for us. Niv allows us to easily add any remote sources as a Nix dependency, and provide them in a single sources object.

We can initialize niv in the project directory as follows:

$ nix-shell -j4 -E \
  'let nixpkgs = import ./nixpkgs.nix;
    in nixpkgs.mkShell { buildInputs = [ nixpkgs.niv ]; }'

[nix-shell]$ 05-package-management/haskell-project-v1

[nix-shell]$ niv init

By default, niv will initialize with the latest nixpkgs version available. We can explicitly override the commit version of nixpkgs to the one in this tutorial.

[nix-shell]$ niv update nixpkgs --branch nixpkgs-unstable \
              --rev c1e5f8723ceb684c8d501d4d4a
e738fef704747e
Update nixpkgs
Done: Update nixpkgs

We can also add new dependencies such as Haskell.nix using niv add:

[nix-shell]$ niv add input-output-hk/haskell.nix \
              --rev 180779b7f530dcd2a45c7d00541f0f3e3d8471b5
Adding package haskell.nix
  Writing new sources file
Done: Adding package haskell.nix

Two new files, nix/sources.json and sources.nix will be created by niv. To load the source dependencies, we can simply do sources = import ./nix/sources.nix {} to import the source object. The source files are then available in the corresponding attributes of the sources object, such as sources.nixpkgs.

Naive Attempt

Let's try to create a naive default.nix that tries to build with cabal directly:

let
  sources = import ../sources.nix {};
  nixpkgs = import sources.nixpkgs {};

  hsPkgs = nixpkgs.haskell.packages.ghc8102;

  src = builtins.path {
    name = "haskell-project-src";
    path = ../../haskell;
    filter = path: type:
      let
        basePath = builtins.baseNameOf path;
      in
      basePath != "dist-newstyle"
    ;
  };
in
nixpkgs.stdenv.mkDerivation {
  inherit src;

  name = "haskell-project";

  buildInputs = [
    hsPkgs.ghc
    hsPkgs.cabal-install
  ];

  builPhase = ''
    cabal build all
  '';

  installPhase = ''
    cabal install --installdir=$out --install-method=copy
  '';
}
  • We use builtins.path to include our Haskell source code, with a filter function to filter out the local dist-newstyle directory.

  • We use GHC 8.10.2 provided from nixpkgs.haskell.packages.ghc8102.

  • We add ghc and cabal-install into buildInputs.

Try building it:

$ nix-build 05-package-management/haskell-project-v1/nix/01-naive/
these derivations will be built:
  /nix/store/w1yscims73lrypddqcnri2vphqfnbim6-haskell-project.drv
building '/nix/store/w1yscims73lrypddqcnri2vphqfnbim6-haskell-project.drv'...
unpacking sources
unpacking source archive /nix/store/rc0pr7b71fm84az7d3gk4pdk62v8s0j0-haskell-project-src
source root is haskell-project-src
patching sources
configuring
no configure script, doing nothing
building
no Makefile, doing nothing
installing
Config file path source is default config file.
Config file /homeless-shelter/.cabal/config not found.
Writing default configuration to /homeless-shelter/.cabal/config
dieVerbatim: user error (cabal: Couldn't establish HTTP connection. Possible cause: HTTP proxy server
is down.
)
builder for '/nix/store/w1yscims73lrypddqcnri2vphqfnbim6-haskell-project.drv' failed with exit code 1
error: build of '/nix/store/w1yscims73lrypddqcnri2vphqfnbim6-haskell-project.drv' failed

Not good. Cabal tries to access the network to get the current Hackage registry state and fails. There is good reason for this - there is no way for Nix to know that cabal's access to network is reproducible.

We can still use it as a Nix shell to build our project manually, because there is network access in Nix shell.

$ nix-shell 05-package-management/haskell-project-v1/nix/01-naive/

[nix-shell]$ cd 05-package-management/haskell-project-v1/haskell

[nix-shell]$ cabal build all
Resolving dependencies...
Build profile: -w ghc-8.10.2 -O1
In order, the following will be built (use -v for more details):
 - haskell-project-0.1.0.0 (exe:hello) (first run)
Configuring executable 'hello' for haskell-project-0.1.0.0..
Preprocessing executable 'hello' for haskell-project-0.1.0.0..
Building executable 'hello' for haskell-project-0.1.0.0..
[1 of 1] Compiling Main             ( Main.hs, /mnt/gamma/scrive/nix-workshop/code/05-package-management/haskell-project-v1/haskell/dist-newstyle/build/x86_64-linux/ghc-8.10.2/haskell-project-0.1.0.0/x/hello/build/hello/hello-tmp/Main.o )
Linking /mnt/gamma/scrive/nix-workshop/code/05-package-management/haskell-project-v1/haskell/dist-newstyle/build/x86_64-linux/ghc-8.10.2/haskell-project-0.1.0.0/x/hello/build/hello/hello ...

Default Attempt

We can instead try the default way of building Haskell packages in Nix. There is a full tutorial by Gabriel. Here we will just take a quick tour.

let
  sources = import ../sources.nix {};
  nixpkgs = import sources.nixpkgs {};

  hsPkgs = nixpkgs.haskell.packages.ghc8102;

  src = builtins.path {
    name = "haskell-project-src";
    path = ../../haskell;
    filter = path: type:
      let
        basePath = builtins.baseNameOf path;
      in
      basePath != "dist-newstyle"
    ;
  };

  project = hsPkgs.callCabal2nix "haskell-project" src;
in
hsPkgs.callPackage project {}
  • We use the hsPkgs.callCabal2nix function to create a nixpkgs-style package.

  • We then call hsPkgs.callPackage to "instantiate" our project with the dependencies taken from hsPkgs.

Now try to build it:

$ nix-build 05-package-management/haskell-project-v1/nix/02-nixpkgs/
building '/nix/store/8rgnd9620lf287i0nw4j3z4wb01pd36a-cabal2nix-haskell-project.drv'...
installing
these derivations will be built:
  /nix/store/as92yri0vvfi5yck5gajckfx064fy0qy-haskell-project-0.1.0.0.drv
building '/nix/store/as92yri0vvfi5yck5gajckfx064fy0qy-haskell-project-0.1.0.0.drv'...
...
Preprocessing executable 'hello' for haskell-project-0.1.0.0..
Building executable 'hello' for haskell-project-0.1.0.0..
[1 of 1] Compiling Main             ( Main.hs, dist/build/hello/hello-tmp/Main.o )
Linking dist/build/hello/hello ...
...
/nix/store/3aq34n1ba3pvl6cs6f63xd737fz6604r-haskell-project-0.1.0.0

$ /nix/store/3aq34n1ba3pvl6cs6f63xd737fz6604r-haskell-project-0.1.0.0/bin/hello
Hello, Haskell!

That works. We can also create a separate shell.nix to derive a Nix shell environment based on our Haskell environment.

let
  sources = import ../sources.nix {};
  nixpkgs = import sources.nixpkgs {};

  hsPkgs = nixpkgs.haskell.packages.ghc8102;

  project = import ./default.nix;
in
nixpkgs.mkShell {
  name = "cabal-shell";
  inputsFrom = [ project.env ];
  buildInputs = [
    hsPkgs.cabal-install
  ];
}

We use nixpkgs.mkShell to create a Nix derivation that is explicitly used for Nix shell. inputsFrom propagates all build inputs of a derivation to the new derivation. We use project.env which is a sub-derivation given by callCabal2nix which contains the GHC shell environment for building our project.

Notice that we have to explicitly provide cabal-install as buildInput to our shell derivation. This shows that internally, the Haskell packages in nixpkgs are built by directly calling GHC, skipping cabal entirely.

How Haskell in Nixpkgs Work

The Haskell packages in nixpkgs are mono-versioned. This means for each Haskell package such as base, aeson, etc, there is exactly one version provided by a Haskell packages set. There are however multiple versions of Haskell packages in nixpkgs, determined by the GHC versions.

For instance, nixpkgs.haskell.packages.ghc8102 contains mono-versioned Haskell packages that works in GHC 8.10.2, while nixpkgs.haskell.packages.ghc884 contains mono-versioned Haskell packages that works in GHC 8.8.4.

$ nix-shell 05-package-management/haskell-project-v1/nix/02-nixpkgs/shell.nix

[nix-shell]$ cd 05-package-management/haskell-project-v1/haskell/

[nix-shell]$ cabal build all
Resolving dependencies...
Build profile: -w ghc-8.10.2 -O1
In order, the following will be built (use -v for more details):
 - haskell-project-0.1.0.0 (exe:hello) (first run)
Configuring executable 'hello' for haskell-project-0.1.0.0..
Preprocessing executable 'hello' for haskell-project-0.1.0.0..
Building executable 'hello' for haskell-project-0.1.0.0..
[1 of 1] Compiling Main             ( Main.hs, /mnt/gamma/scrive/nix-workshop/code/05-package-management/haskell-project-v1/haskell/dist-newstyle/build/x86_64-linux/ghc-8.10.2/haskell-project-0.1.0.0/x/hello/build/hello/hello-tmp/Main.o )
Linking /mnt/gamma/scrive/nix-workshop/code/05-package-management/haskell-project-v1/haskell/dist-newstyle/build/x86_64-linux/ghc-8.10.2/haskell-project-0.1.0.0/x/hello/build/hello/hello ...

Stackage Upstream

The mono versions of Haskell packages used to follow Stackage LTS, which is also mono-versioned. However recently the team have switched to Stackage nightly to reduce the maintenance burden.

Callpackage Pattern

As we discussed in previous chapter, Nix itself does not provide any mechanism for dependency resolution. So nixpkgs come out with the Callpackage design pattern to manage dependencies in nixpkgs.

In short, we define new packages in function form which accept dependencies as function inputs. Let's call these functions partial packages, since they are packages with dependencies yet to be supplied.

For example, the pickaxe package we defined previously would have a partial package definition that looks something like:

let pickaxe = { stick, planks }: ...

The partial package is then instantiated into a Nix derivation by calling nixpkgs.callPackage with the package set containing all its dependencies as partial packages.

let minePackages = {
  wood = { ... }: ...;
  stick = { wood, ... }: ...;
  planks = { wood, ... }: ...;
  ...
}
in
nixpkgs.callPackage minePackages pickaxe {}

The nixpkgs.callPackage automagically inspects the function arguments as dependencies in the package set, and construct a dependency graph that connects all packages with their dependencies. If this succeeds we get a Nix derivation with the dependency derivations provided to our partial package.

Dependency Injection

callPackage is essentially a dependency injection pattern, where components can specify what they need without hardcoding the reference. This allows dependencies to be overridden before the callPackage is called.

Using functional programming techniques, it is relatively trivial to compose partial package functions so that dependencies can be overridden either locally or globally. For example, nixpkgs use the override pattern to allow dependencies of a package be overridden.

While functional programming makes it easy to override dependencies, it does not make it easy to inspect the dependency graph to find out what is overridden. This is the downside of composing using closures as blackboxes, as compared to composing ADTs (algebraic data types) as whiteboxes.

Because of this, heavy usage of callPackage and override may impact readability and maintainability. Readers of a Nix code base may no longer be able to statically infer which final versions of dependencies are linked to a package.

Haskell.nix

There is an alternative approach to managing Haskell dependencies using Haskell.nix. Unlike the mono-versioned Haskell packages in nixpkgs, Haskell.nix gives more flexibilities and allows interoperability with the multi-versioned approach of package management with cabal.

As we will see in the next chapter, the multi-versioned approach of Haskell.nix makes it much easier to add bleeding-edge dependencies from Hackage. Haskell.nix also offers many other features, such as cross compiling Haskell packages.

The biggest hurdle of adopting Haskell.nix is unfortunately to properly add Haskell.nix as a dependency in your Nix project. From the first section of the project readme:

Help! Something isn't working

The #1 problem that people have when using haskell.nix is that they find themselves building GHC. This should not happen, but you must follow the haskell.nix setup instructions properly to avoid it. If you find this happening to you, please check that you have followed the getting started instructions and consult the corresponding troubleshooting section.

As mentioned, the most important step to start using Haskell.nix is to add the hydra.iohk.io Nix cache to your ~/.config/nix/nix.conf:

trusted-public-keys = [...] hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= [...]
substituters = [...] https://hydra.iohk.io [...]

Haskell.nix-based derivation

Aside from that first hurdle, defining a Haskell.nix-based Nix derivation is relatively straightforward. First we define a project.nix:

let
  sources = import ../sources.nix {};

  haskell-nix = import sources."haskell.nix" {};

  nixpkgs = haskell-nix.pkgs;

  src = builtins.path {
    name = "haskell-project-src";
    path = ../../haskell;
    filter = path: type:
      let
        basePath = builtins.baseNameOf path;
      in
      basePath != "dist-newstyle"
    ;
  };

  project = nixpkgs.haskell-nix.cabalProject {
    inherit src;

    compiler-nix-name = "ghc8102";
  };
in
project

We use the version of Haskell.nix managed by niv and import it. Here we also use the version of nixpkgs provided by Haskell.nix, which adds additional functionalities in nixpkgs.haskell-nix. We then call the function haskell-nix.pkgs.haskell-nix.cabalProject to define a cabal-based Haskell.nix project.

We provide the Haskell source code through the src attribute, and also a compiler-nix-name field to specify the GHC version we want to use, GHC 8.10.2.

Project Outputs

To actually build our Haskell project, we define a default.nix to build the hello executable we have in our project:

let
  project = import ./project.nix;
in
project.haskell-project.components.exes.hello

A Haskell.nix project can have multiple derivation outputs for each cabal component. For our case, we do not have any library but have one executable named hello. To load that, the executable is unfortunately located in a long and obscure path project.haskell-project.components.exes.hello.

$ nix-build 05-package-management/haskell-project-v1/nix/03-haskell.nix/
trace: No index state specified, using the latest index state that we know about (2020-12-04T00:00:00Z)!
these derivations will be built:
  /nix/store/xdpklq1y86h6jw6d8fyw6xwhr93l8g73-haskell-project-exe-hello-0.1.0.0-config.drv
  /nix/store/j0azqvy5iccbfqp6s0gbfwdgjjp8x2ji-haskell-project-exe-hello-0.1.0.0-ghc-8.10.2-env.drv
  /nix/store/mp20hw7kjpqfwqsspjff0h8w8qng8n9d-haskell-project-exe-hello-0.1.0.0.drv
...
/nix/store/yr533l33zrpri7n47lsfm2cih5i0800a-haskell-project-exe-hello-0.1.0.0

$ /nix/store/yr533l33zrpri7n47lsfm2cih5i0800a-haskell-project-exe-hello-0.1.0.0/bin/hello
Hello, Haskell!

Version Conflicts in Haskell Dependencies

In the previous chapter we have created a trivial Haskell project with no dependency other than base. Let's look at what happen when our Haskell project have some dependencies, which happen to conflict with the dependencies available in nixpkgs.

We first define a new Haskell project, haskell-project-v2:

cabal-version:       2.4
name:                haskell-project
version:             0.1.0.0
license:             ISC
build-type:          Simple

executable hello
  main-is:             Main.hs
  build-depends:       base >=4.13
                     , yaml == 0.11.3.0
  default-language:    Haskell2010

We add a new dependency yaml and explicitly requiring version 0.11.3.0. At the time of writing, the latest version of yaml available on Hackage is 0.11.5.0. However let's pretend there are breaking changes in 0.11.5.0, and we only support 0.11.3.0.

Nixpkgs

Let's try to follow our previous approach to define our Haskell derivation using callCabal2nix. If we do that and try to build it, we would run into an error:

$ nix-build 05-package-management/haskell-project-v2/nix/01-nixpkgs-conflict/
building '/nix/store/3gab06pwqjc16wdqhj5akxk21g1z0qnx-cabal2nix-haskell-project.drv'...
these derivations will be built:
  /nix/store/242x69pl2la3lb201qd57rghisrwclpy-haskell-project-0.1.0.0.drv
building '/nix/store/242x69pl2la3lb201qd57rghisrwclpy-haskell-project-0.1.0.0.drv'...
...
Setup: Encountered missing or private dependencies:
yaml ==0.11.3.0

builder for '/nix/store/242x69pl2la3lb201qd57rghisrwclpy-haskell-project-0.1.0.0.drv' failed with exit code 1
error: build of '/nix/store/242x69pl2la3lb201qd57rghisrwclpy-haskell-project-0.1.0.0.drv' failed

Why is that so?

If we try to enter Nix shell, it will still succeed. But if we try to build our Haskell project in Nix shell, we will find out that the package yaml has to be explicitly built by cabal:

$ nix-shell 05-package-management/haskell-project-v2/nix/01-nixpkgs-conflict/shell.nix

[nix-shell]$ cd 05-package-management/haskell-project-v2/haskell/

[nix-shell]$ cabal --dry-run build all
Resolving dependencies...
Build profile: -w ghc-8.10.2 -O1
In order, the following would be built (use -v for more details):
 - yaml-0.11.3.0 (lib) (requires build)
 - haskell-project-0.1.0.0 (exe:hello) (first run)

Problem with Mono-versioning

If we look into nixpkgs source code, we can in fact see that the version of yaml available in nixpkgs is the latest, 0.11.5.0.

As discussed earlier, with the mono-versioning approach by nixpkgs, there is exactly one version of each package available. Mono-versioning conflicts can happen when we need packages that are either older or newer than the version provided by nixpkgs.

In theory we could switch to a version of nixpkgs that has yaml-0.11.3.0, however we would then have to buy into the versions of other Haskell packages available at that time.

Overriding Versions

Nixpkgs provides a workaround for mono-versioning conflicts, by using the override pattern. We can override the version of yaml to the one we want as follows:

let
  sources = import ../sources.nix {};
  nixpkgs = import sources.nixpkgs {};

  hsLib = nixpkgs.haskell.lib;
  hsPkgs-original = nixpkgs.haskell.packages.ghc8102;

  hsPkgs = hsPkgs-original.override {
    overrides = hsPkgs-old: hsPkgs-new: {
      yaml = hsPkgs-new.callHackage
        "yaml" "0.11.3.0" {};
    };
  };

  src = builtins.path {
    name = "haskell-project-src";
    path = ../../haskell;
    filter = path: type:
      let
        basePath = builtins.baseNameOf path;
      in
      basePath != "dist-newstyle"
    ;
  };

  project = hsPkgs.callCabal2nix "haskell-project" src;
in
hsPkgs.callPackage project {}

Essentially, we refer to the original haskell package set provided as hsPkgs-original, and we call hsPkgs-original.override to produce a new package set hsPkgs with yaml overridden to 0.11.3.0.

Using callHackage, we can fetch the version of yaml from the Hackage snapshot in nixpkgs. With that we can just provide the string "0.11.3.0" to specify the version that we want. Note however that this only works if the version can be found in the given Hackage snapshot, which may be outdated over time.

An issue with overriding dependencies this way is that the override affects the entire Haskell package set. This means that all other Haskell packages that depend on yaml will also get 0.11.3.0 instead of 0.11.5.0. As a result, this may have the unintended ripple effect of breaking other Haskell packages that we depends on.

Building Overridden Package

If we try to build our Haskell derivation with overridden yaml, it would work this time:

$ nix-build 05-package-management/haskell-project-v2/nix/02-nixpkgs-override/
these derivations will be built:
  /nix/store/l0v04zz36b5s5r3qc2jisvggyc0gkj5w-remove-references-to.drv
  /nix/store/bg5z6b7m24fxqn8qq2l2w8c0w30wkbp3-yaml-0.11.3.0.drv
  /nix/store/p5sr404mzr8bnqqprv72lxczdr9cnnim-haskell-project-0.1.0.0.drv
...
/nix/store/0m0mr11ncii3z4zkn9z0xkwk4nswprqm-haskell-project-0.1.0.0

We can also enter the Nix shell to verify that this time, cabal will not try to build yaml for us:

$ nix-shell 05-package-management/haskell-project-v2/nix/02-nixpkgs-override/shell.nix

[nix-shell]$ cd 05-package-management/haskell-project-v2/haskell/

[nix-shell]$ cabal --dry-run build all
Resolving dependencies...
Build profile: -w ghc-8.10.2 -O1
In order, the following would be built (use -v for more details):
 - haskell-project-0.1.0.0 (exe:hello) (first run)

Haskell.nix

In comparison with the mono-versioned nixpkgs, Haskell.nix is much more flexible in allowing any version of Haskell packages that are supported by cabal. So we can leave the Nix project unchanged and still build it successfully:

$ nix-build 05-package-management/haskell-project-v2/nix/03-haskell.nix/
trace: No index state specified, using the latest index state that we know about (2020-12-04T00:00:00Z)!
building '/nix/store/v4pf9jffq0dh6xang25qviwb77947s7s-plan-to-nix-pkgs.drv'...
Using index-state 2020-12-04T00:00:00Z
Warning: The package list for 'hackage.haskell.org-at-2020-12-04T000000Z' is
18603 days old.
Run 'cabal update' to get the latest list of available packages.
Warning: Requested index-state2020-12-04T00:00:00Z is newer than
'hackage.haskell.org-at-2020-12-04T000000Z'! Falling back to older state
(2020-12-03T20:14:57Z).
Resolving dependencies...
Build profile: -w ghc-8.10.2 -O1
In order, the following would be built (use -v for more details):
 - base-compat-0.11.2 (lib) (requires download & build)
 - base-orphans-0.8.3 (lib) (requires download & build)
 ...
 - aeson-1.5.4.1 (lib) (requires download & build)
 - yaml-0.11.3.0 (lib) (requires download & build)
 - haskell-project-0.1.0.0 (exe:hello) (first run)
these derivations will be built:
these derivations will be built:
...
  /nix/store/y8vbr0b6y8bzgmadj0rfjp3d2rzx5wgs-yaml-lib-yaml-0.11.3.0-config.drv
  /nix/store/fhmib5kqsxl82r1z23mm59njw2dn0c8v-yaml-lib-yaml-0.11.3.0-ghc-8.10.2-env.drv
  /nix/store/j837v0cxk9dxqpxfjfngii007hq8wn3w-yaml-lib-yaml-0.11.3.0.drv
  /nix/store/nxwvfjaj40adyq002khld7ngnq3wggn7-haskell-project-exe-hello-0.1.0.0-config.drv
  /nix/store/y27wbd58f5d1k3lzbzpr5qcc4pgqrxg2-haskell-project-exe-hello-0.1.0.0-ghc-8.10.2-env.drv
  /nix/store/b4i7xhnha8007zqxd4gidsf7xyy338an-haskell-project-exe-hello-0.1.0.0.drv
...
/nix/store/phm2jk6xnvxsgp640r66cwgipc62kbc5-haskell-project-exe-hello-0.1.0.0

$ /nix/store/phm2jk6xnvxsgp640r66cwgipc62kbc5-haskell-project-exe-hello-0.1.0.0/bin/hello
Hello, Haskell!

Transitive Haskell Version Conflicts

Let's look at another example of nixpkgs-based Haskell project, where version conflicts are propagated to transitive dependencies.

We have another Haskell project, haskell-project-v3 with a dependency on the latest QuickCheck-2.14.2:

cabal-version:       2.4
name:                haskell-project
version:             0.1.0.0
license:             ISC
build-type:          Simple

executable hello
  main-is:             Main.hs
  build-depends:       base >=4.13
                     , QuickCheck == 2.14.2
  default-language:    Haskell2010

Now try building the project with the default callCabal2nix:

let
  sources = import ../sources.nix {};
  nixpkgs = import sources.nixpkgs {};

  hsPkgs = nixpkgs.haskell.packages.ghc8102;

  src = builtins.path {
    name = "haskell-project-src";
    path = ../../haskell;
    filter = path: type:
      let
        basePath = builtins.baseNameOf path;
      in
      basePath != "dist-newstyle"
    ;
  };

  project = hsPkgs.callCabal2nix "haskell-project" src;
in
hsPkgs.callPackage project {}

We should see that the building failed, because the version of nixpkgs we are using only has QuickCheck-2.13.2 in it.

$ nix-build 05-package-management/haskell-project-v3/nix/01-nixpkgs-conflict
building '/nix/store/12qzjbk3514sj9v4j99brbxr4b83bzy5-cabal2nix-haskell-project.drv'...
installing
these derivations will be built:
  /nix/store/d5jyli1v9y12il9wyzs5x6gyx6q68gig-haskell-project-0.1.0.0.drv
...
Setup: Encountered missing or private dependencies:
QuickCheck ==2.14.2

builder for '/nix/store/d5jyli1v9y12il9wyzs5x6gyx6q68gig-haskell-project-0.1.0.0.drv' failed with exit code 1
error: build of '/nix/store/d5jyli1v9y12il9wyzs5x6gyx6q68gig-haskell-project-0.1.0.0.drv' failed

Using Latest Hackage Package

Now we try using the override pattern in the previous chapter to override the version of QuickCheck:

let
  sources = import ../sources.nix {};
  nixpkgs = import sources.nixpkgs {};

  hsLib = nixpkgs.haskell.lib;
  hsPkgs-original = nixpkgs.haskell.packages.ghc8102;

  hsPkgs = hsPkgs-original.override {
    overrides = hsPkgs-old: hsPkgs-new: {
      QuickCheck = hsPkgs-new.callHackage "QuickCheck" "2.14.2" {};
    };
  };

  src = builtins.path {
    name = "haskell-project-src";
    path = ../../haskell;
    filter = path: type:
      let
        basePath = builtins.baseNameOf path;
      in
      basePath != "dist-newstyle"
    ;
  };

  project = hsPkgs.callCabal2nix "haskell-project" src;
in
hsPkgs.callPackage project {}

This time the build actually failed with another error:

$ nix-build 05-package-management/haskell-project-v3/nix/01-nixpkgs-conflict
building '/nix/store/12qzjbk3514sj9v4j99brbxr4b83bzy5-cabal2nix-haskell-project.drv'...
installing
these derivations will be built:
  /nix/store/d5jyli1v9y12il9wyzs5x6gyx6q68gig-haskell-project-0.1.0.0.drv
...
Configuring haskell-project-0.1.0.0...
CallStack (from HasCallStack):
  $, called at libraries/Cabal/Cabal/Distribution/Simple/Configure.hs:1024:20 in Cabal-3.2.0.0:Distribution.Simple.Configure
  configureFinalizedPackage, called at libraries/Cabal/Cabal/Distribution/Simple/Configure.hs:477:12 in Cabal-3.2.0.0:Distribution.Simple.Configure
  configure, called at libraries/Cabal/Cabal/Distribution/Simple.hs:625:20 in Cabal-3.2.0.0:Distribution.Simple
  confHook, called at libraries/Cabal/Cabal/Distribution/Simple/UserHooks.hs:65:5 in Cabal-3.2.0.0:Distribution.Simple.UserHooks
  configureAction, called at libraries/Cabal/Cabal/Distribution/Simple.hs:180:19 in Cabal-3.2.0.0:Distribution.Simple
  defaultMainHelper, called at libraries/Cabal/Cabal/Distribution/Simple.hs:116:27 in Cabal-3.2.0.0:Distribution.Simple
  defaultMain, called at Setup.hs:2:8 in main:Main
Setup: Encountered missing or private dependencies:
QuickCheck ==2.14.2

builder for '/nix/store/d5jyli1v9y12il9wyzs5x6gyx6q68gig-haskell-project-0.1.0.0.drv' failed with exit code 1
error: build of '/nix/store/d5jyli1v9y12il9wyzs5x6gyx6q68gig-haskell-project-0.1.0.0.drv' failed
soares@soares-workstation:~/scrive/nix-workshop/code$ nix-build 05-package-management/haskell-project-v3/nix/02-nixpkgs-not-found
building '/nix/store/ybvimf0jbsbx997588kbipkpbq97iv3d-all-cabal-hashes-component-QuickCheck-2.14.2.drv'...
tar: */QuickCheck/2.14.2/QuickCheck.json: Not found in archive
tar: */QuickCheck/2.14.2/QuickCheck.cabal: Not found in archive
tar: Exiting with failure status due to previous errors
builder for '/nix/store/ybvimf0jbsbx997588kbipkpbq97iv3d-all-cabal-hashes-component-QuickCheck-2.14.2.drv' failed with exit code 2
cannot build derivation '/nix/store/73pfc98xfaj7l818nznr8r4gbls5xmls-cabal2nix-QuickCheck-2.14.2.drv': 1 dependencies couldn't be built
error: build of '/nix/store/73pfc98xfaj7l818nznr8r4gbls5xmls-cabal2nix-QuickCheck-2.14.2.drv' failed
(use '--show-trace' to show detailed location information)

What happened this time? If we check the commit log of our nixpkgs version, we will find out that the nixpkgs we have is commited on 9 November 2020, but on Hackage QuickCheck-2.14.2 is only released on 14 November 2020.

Hackage Index in Nixpkgs

Inside the override call, when we call callHackage to get a Haskell package from Hackage, we are really just downloading the Haskell source from Hackage based on the snapshot cached in nixpkgs.

Recall from the principal of reproducibility, with just a version number, there is no way Nix can tell if we will always get the exact same source code from Hackage every time a source code is requested. In theory the QuickCheck-2.14.2 we fetched today may be totally different from the QuickCheck-2.14.2 we fetch tomorrow, or when it is fetched by someone else.

Nixpkgs solves this by computing the content hash of every Hackage package at the time of snapshot. So we can know for sure that the same Hackage pacakge we fetch with the same nixpkgs will always give back the same result. Nixpkgs also does implicit patching on some Hackage package, if their default configuration breaks.

One option for us may be to simply update to the latest version of nixpkgs so that it contains QuickCheck-2.14.2. However nixpkgs do not immediately update the Hackage snapshot every time a new Haskell pacakge is published. Rather there is usually 1~2 weeks lag as the Haskell packages are updated in bulk. So we can't rely on that if we want to use a Haskell package just published an hour ago.

Furthermore, updating nixpkgs also means all other packages in nixpkgs also being updated. That may result in breaking some of our own Nix packages.

In theory we could use a stable Nix channel like nixos-20.09 instead of nixpkgs-unstable so that it is safer to update nixpkgs. But stability always needs tradeoff with rapid releases, so it will take even longer before the Hackage snapshot update is propagated there.

CallHackageDirect

If we want to get the latest Hackage package beyond what is available in nixpkgs, we can instead use callHackageDirect to directly download the package from Hackage, skipping nixpkgs entirely:

let
  sources = import ../sources.nix {};
  nixpkgs = import sources.nixpkgs {};

  hsLib = nixpkgs.haskell.lib;
  hsPkgs-original = nixpkgs.haskell.packages.ghc8102;

  hsPkgs = hsPkgs-original.override {
    overrides = hsPkgs-old: hsPkgs-new: {
      QuickCheck = hsPkgs-new.callHackageDirect {
        pkg = "QuickCheck";
        ver = "2.14.2";
        sha256 = "0rx4lz5rj0s1v451cq6qdxhilq4rv9b9lnq6frm18h64civ2pwbq";
      } {};
    };
  };

  src = builtins.path {
    name = "haskell-project-src";
    path = ../../haskell;
    filter = path: type:
      let
        basePath = builtins.baseNameOf path;
      in
      basePath != "dist-newstyle"
    ;
  };

  project = hsPkgs.callCabal2nix "haskell-project" src;
in
hsPkgs.callPackage project {}

callHackageDirect works similarly to other ways of fetching source code, such as builtins.fetchTarball and builtins.fetchgit. In fact, we can also override a Haskell dependency with a GitHub commit or source tarball. Here we just need to provide an additional information, which is the SHA256 checksum of the package.

There is currently straightforward way to compute the hash, but we can first supply a dummy hash such as 0000000000000000000000000000000000000000000000000000, then copy the correct hash from the hash mismatch error when the derivation is built.

Transitive Conflicts

If we try to build it this time however, we are greeted with another error:

$ nix-build 05-package-management/haskell-project-v3/nix/03-nixpkgs-transitive-deps
these derivations will be built:
  /nix/store/6my008rqdjb9kbmx0pr80c0zc0fyqqyh-QuickCheck-2.14.2.drv
  /nix/store/lvclw4q2jmk89v5ppkfw3mr72qb8ch2d-haskell-project-0.1.0.0.drv
building '/nix/store/6my008rqdjb9kbmx0pr80c0zc0fyqqyh-QuickCheck-2.14.2.drv'...
...
Configuring QuickCheck-2.14.2...
CallStack (from HasCallStack):
  $, called at libraries/Cabal/Cabal/Distribution/Simple/Configure.hs:1024:20 in Cabal-3.2.0.
  ...
  defaultMain, called at Setup.lhs:8:10 in main:Main
Setup: Encountered missing or private dependencies:
splitmix ==0.1.*

builder for '/nix/store/6my008rqdjb9kbmx0pr80c0zc0fyqqyh-QuickCheck-2.14.2.drv' failed with exit code 1
cannot build derivation '/nix/store/lvclw4q2jmk89v5ppkfw3mr72qb8ch2d-haskell-project-0.1.0.0.drv': 1 dependencies couldn't be built
error: build of '/nix/store/lvclw4q2jmk89v5ppkfw3mr72qb8ch2d-haskell-project-0.1.0.0.drv' failed

So it turns out that QuickCheck-2.14.2 depends on splitmix ==0.1.*, but nixpkgs only have splitmix-0.0.5, despite splitmix-0.1 has been released since May 2020.

We can see this as the effect of transitive dependency update. If splitmix is upgraded to version 0.1, it will break many packages that directly depend on it, which in turns breaks other packages that indirectly depend on it. With nixpkgs's mono-versioning approach, there is no easy way around this other than upgrading all affecting packages at once, or upgrading none of them. Mono-versioning is hard!

Still, we can workaround this by overriding splitmix as well:

let
  sources = import ../sources.nix {};
  nixpkgs = import sources.nixpkgs {};

  hsLib = nixpkgs.haskell.lib;
  hsPkgs-original = nixpkgs.haskell.packages.ghc8102;

  hsPkgs = hsPkgs-original.override {
    overrides = hsPkgs-old: hsPkgs-new: {
      QuickCheck = hsPkgs-new.callHackageDirect {
        pkg = "QuickCheck";
        ver = "2.14.2";
        sha256 = "0rx4lz5rj0s1v451cq6qdxhilq4rv9b9lnq6frm18h64civ2pwbq";
      } {};

      splitmix = hsPkgs-new.callHackage
        "splitmix" "0.1.0.3" {};
    };
  };

  src = builtins.path {
    name = "haskell-project-src";
    path = ../../haskell;
    filter = path: type:
      let
        basePath = builtins.baseNameOf path;
      in
      basePath != "dist-newstyle"
    ;
  };

  project = hsPkgs.callCabal2nix "haskell-project" src;
in
hsPkgs.callPackage project {}

Infinite Recursion

If we build this, we once again get another error: the infamous infinite recursion error:

$ nix-build --show-trace 05-package-management/haskell-project-v3/nix/04-nixpkgs-infinite-recursion/
error: while evaluating the attribute 'buildInputs' of the derivation 'haskell-project-0.1.0.0' at /nix/store/kk346951sg2anjjh8cgfbmrijg983z5q-nixpkgs-src/pkgs/development/haskell-modules/generic-builder.nix:291:3:
while evaluating the attribute 'propagatedBuildInputs' of the derivation 'QuickCheck-2.14.2' at /nix/store/kk346951sg2anjjh8cgfbmrijg983z5q-nixpkgs-src/pkgs/development/haskell-modules/generic-builder.nix:291:3:
while evaluating the attribute 'buildInputs' of the derivation 'splitmix-0.1.0.3' at /nix/store/kk346951sg2anjjh8cgfbmrijg983z5q-nixpkgs-src/pkgs/development/haskell-modules/generic-builder.nix:291:3:
while evaluating the attribute 'propagatedBuildInputs' of the derivation 'async-2.2.2' at /nix/store/kk346951sg2anjjh8cgfbmrijg983z5q-nixpkgs-src/pkgs/development/haskell-modules/generic-builder.nix:291:3:
while evaluating the attribute 'buildInputs' of the derivation 'hashable-1.3.0.0' at /nix/store/kk346951sg2anjjh8cgfbmrijg983z5q-nixpkgs-src/pkgs/development/haskell-modules/generic-builder.nix:291:3:
infinite recursion encountered, at undefined position

So QuickCheck depends on splitmix, but splitmix tests also indirectly depend on QuickCheck, causing a cyclic dependency. Unfortunately the callPackage pattern do not have a way to deal with cyclic dependencies, so we have to manually find ways around it.

Fortunately in this case, it is simple to avoid it by no running unit tests on splitmix:

let
  sources = import ../sources.nix {};
  nixpkgs = import sources.nixpkgs {};

  hsLib = nixpkgs.haskell.lib;
  hsPkgs-original = nixpkgs.haskell.packages.ghc8102;

  hsPkgs = hsPkgs-original.override {
    overrides = hsPkgs-old: hsPkgs-new: {
      QuickCheck = hsPkgs-new.callHackageDirect {
        pkg = "QuickCheck";
        ver = "2.14.2";
        sha256 = "0rx4lz5rj0s1v451cq6qdxhilq4rv9b9lnq6frm18h64civ2pwbq";
      } {};

      splitmix = hsLib.dontCheck
        (hsPkgs-new.callHackage
          "splitmix" "0.1.0.3" {});
    };
  };

  src = builtins.path {
    name = "haskell-project-src";
    path = ../../haskell;
    filter = path: type:
      let
        basePath = builtins.baseNameOf path;
      in
      basePath != "dist-newstyle"
    ;
  };

  project = hsPkgs.callCabal2nix "haskell-project" src;
in
hsPkgs.callPackage project {}

Now our package finally builds.

Haskell.nix

In comparison, there is no manual intervention needed for Haskell.nix-based derivation:

let
  sources = import ../sources.nix {};

  haskell-nix = import sources."haskell.nix" {};

  nixpkgs = haskell-nix.pkgs;

  src = builtins.path {
    name = "haskell-project-src";
    path = ../../haskell;
    filter = path: type:
      let
        basePath = builtins.baseNameOf path;
      in
      basePath != "dist-newstyle"
    ;
  };

  project = nixpkgs.haskell-nix.cabalProject {
    inherit src;

    compiler-nix-name = "ghc8102";
  };
in
project

Hoepfully this shows why we prefer to use Haskell.nix, especially if we want it to work the same way as cabal and get the latest Haskell packages from Hackage.

Multi-versioned Haskell Packages

As we seens in the previous chapters, the mono-versioned Haskell packages provided by nixpkgs do not always provide the exact versions of Haskell packages that we need. When version conflicts happen, it often require manual intervention, because there is no mechanism to automatically resolve the new set of dependency versions based on custom requirements.

In comparison, package managers like cabal-install are built with multi-versioned Haskell packages in mind. We can give various requirements of our dependencies to cabal by specifying version bounds. If a solution exists, cabal can automatically generate a dependency graph for us.

Haskell.nix

Haskell.nix takes multi-versioned approach toward managing Haskell packages in Nix. It does so by making use of cabal-install to resolve the dependency graph, and converting it into Nix expressions.

Since Haskell.nix uses cabal-install for the actual package management, it can usually work out of the box for existing cabal-based Haskell projects.

Nevertheless, Cabal offers a wide range of features, some of which are not currently well supported by Haskell.nix. Because of this, it is important to understand how Haskell.nix works internally. This would help us understand why certain limitations are present in Haskell.nix.

Haskell.nix also provides other mode of Haskell development in Nix, such as Stack-based Haskell projects. However in this chapter we will focus only on using Haskell.nix for cabal projects.

Hackage Index

To understand how Haskell.nix resolves dependencies, we have to first understand how Cabal resolves dependencies. Recall from the chapter Dependency Management that for a package manager to resolve dependencies, it has to run the dependency resolution algorithm against the state of a package registry.

For the case of Cabal, the dependency resolution is done against the Hackage registry. More specifically, Cabal resolves the dependencies by downloading the entire state of the Hackage index to your local machine.

Every time new packages are added to Hackage, a new Hackage index snapshot is generated, and the entire index has to be re-downloaded. (Technically, the update is incremental with append-only update to the snapshot. However this relies on explicit HTTP caching mechanism to work.)

Currently the Hackage index snapshot takes about 100 MiB after compression. Furthermore the official Hackage bandwidth is pretty low and do not always stay available. Because of this, it can often take a long time to update cabal, especially when running it for the first time.

When we build Haskell projects with cabal, the local Hackage index is not updated automatically. Instead we have to manually run cabal update to update the local Hackage index. As a result, it is a common problem that a new Haskell package is published to Hackage, but it cannot be found in local builds because the local index is not updated.

Accessing Hackage Index Inside Nix

Since Haskell.nix uses Cabal to resolve the dependencies inside Nix, it needs to download and provide the Hackage snapshot inside of a Nix build.

By default, Cabal downloads the Hackage snapshot into ~/.cabal/packages. However since there is no home directory inside a Nix build, we need to put it somewhere local. This can be done by specifying a --remote-repo-cache option when running Cabal.

Even so, the next challenge is that we need to download the Hackage snapshot, and save it to the Nix store. The SHA256 checksum of the Hackage snapshot is also needed, so that Nix can be sure that all Nix builds are using the exact version of Hackage snapshot.

Fortunately Haskell.nix takes care of all these details for us. However this comes at a cost: Haskell.nix needs to regularly update and convert the Hackage snapshot into Nix expressions.

In fact, Haskell.nix manages this through the Hackage.nix project. It triggers regular CI builds to update to the latest Hackage snapshot, and save the result as Nix expressions. Haskell.nix itself is also regularly updated, to update the Hackage snapshot through updating the version of Hackage.nix used in Haskell.nix.

Regardless, the state update do not run continuously, but rather usually once every day. There can be up to a few days delay for the Hackage snapshot to be included in Haskell.nix. This also means that when a new Haskell package is published to Hackage, we cannot always use it immediately in Haskell.nix.

If we really insist on getting the bleeding edge Haskell packages, a workaround would be that we can maintain our own Hackage snapshot. Since Hackage.nix is open source, there is no problem doing that. However this would also adds a lot of overhead, as we would have to regularly keep up to date our own version of Hackage.nix. Because of this, we recommend readers to be patient and wait for few days for a new Haskell packge to become available in Haskell.nix.

Freezing Hackage Index

The Hackage index at hackage.haskell.org is constantly updated, and there is no straightforward way to go back in time and get the old state of Hackage at a time in the past.

This can cause issue when we try to download the Hackage snapshot from Nix. To ensure reproducibility, the downloaded 01-index.tar.gz is supposed to have the exact same content for everyone by checking the SHA256 checksum. But since the Hackage index keeps changing, we cannot ask Nix to just download the index from the URL.

As a workaround, Hackage instead guarantees that the index state is updated in append-only mode. With this Haskell.nix uses a workaround to truncate the downloaded index with a known length of the snapshot at a particular time during fetching. This helps make sure that when Nix finally sees the index archive, the content is exactly the same regardless of when the code is evaluated.

Nevertheless, this workaround is one part of the complexity in Haskell.nix. In the daily update to Hackage.nix, it has to regularly check the new length of the latest Hackage index, while keeping the old lengths for each day in the past. As a .tar.gz file, the format is opaque, and it needs to rely on the obscure interaction of Hackage with the file compression to create a reproducible index snapshot.

Source Repository Package

Haskell.nix also allows Haskell packages to be provided outside of Hackage. This can be done by adding source-repository-package fields in your cabal.project file. Haskell.nix automatically takes into consideration these fields when resolving the depdendencies.

One additional detail that is needed when using source-repository-package with Haskell.nix is that you may optionally needs to add the SHA256 checksum of the Haskell package with a --sha256 comment line. More details here.

Resolving Dependencies

We have learned earlier that evaluation-time dependencies can cause various issues in Nix. However Haskell.nix uses evaluation-time dependency, or more specifically Import From Derivation (IFD), to resolve the dependency graph of our Haskell projects.

To understand why IFD is needed, we need to first look at how Cabal resolves the dependencies, and where it stores the dependency graph.

We can ask Cabal to resolve the dependencies of our Haskell project by running cabal configure. Inside this command, Cabal will read the cabal.project and .cabal file, read the local Hackage snapshot, and try to come out with a dependency graph as the solution. When successful, cabal stores the result locally in the dist-newstyle/cache directory.

Inside the dist-newstyle/cache directory, there is a plan.json file which contains the dependency graph that third party tools such as Haskell.nix can use to extract the result. There are libraries such as cabal-plan that are available, which Haskell.nix uses to parse the dependency graph.

Now remember that to build a Haskell project with Nix, we need to construct one or more Nix derivations that encapsulate our project and all its dependencies. Since a Haskell project contains many dependencies, we want to define each Haskell package as their own Nix derivation. This way Nix can parallellize the build for each of our dependencies, and also reuse the build result if only some of the dependencies changes.

However to construct our derivation, we need to first call cabal configure to resolve the dependency graph, and then extract the result from plan.json. In other words, to construct the actual Nix derivation of our project, we need to first construct another derivation that produces a build plan that tells us what dependencies are needed.

Derivation Plan

In Haskell.nix, the build plan that is produced from calling Cabal is called a plan. The name might be a bit confusing, but essentially it represents the evaluation-time dependency of a Haskell project, of which the Nix derivation that needs to be built first before the actual Nix derivation for the Haskell project can be constructed.

Recall that Nix cannot identify which derivation is an evaluation-time dependency of another package. This means that if we try to inspect the derivation for our Haskell project, the derivation for the plan would never show up there. This also means that the plan cannot be cached easily.

Haskell.nix exports the plan for a Haskell project in the plan-nix attribute. We can build this to see how the plan looks like:

$ nix-build -A plan-nix 05-package-management/haskell-project-v3/nix/06-haskell.nix/project.nix
trace: No index state specified, using the latest index state that we know about (2020-12-04T00:00:00Z)!
checking outputs of '/nix/store/5v1835nh26s8dldssz03cysm3aay6d7q-plan-to-nix-pkgs.drv'...
Using index-state 2020-12-04T00:00:00Z
Warning: The package list for 'hackage.haskell.org-at-2020-12-04T000000Z' is
18610 days old.
Run 'cabal update' to get the latest list of available packages.
Warning: Requested index-state2020-12-04T00:00:00Z is newer than
'hackage.haskell.org-at-2020-12-04T000000Z'! Falling back to older state
(2020-12-03T20:14:57Z).
Resolving dependencies...
Build profile: -w ghc-8.10.2 -O1
In order, the following would be built (use -v for more details):
 - splitmix-0.1.0.3 (lib) (requires download & build)
 - random-1.2.0 (lib) (requires download & build)
 - QuickCheck-2.14.1 (lib) (requires download & build)
 - haskell-project-0.1.0.0 (exe:hello) (first run)
/nix/store/pgdqan44d8ky1yz0d687d2nhqsqflc48-plan-to-nix-pkgs

$ cat /nix/store/pgdqan44d8ky1yz0d687d2nhqsqflc48-plan-to-nix-pkgs/default.nix
{
  pkgs = hackage:
    {
      packages = {
        "ghc-prim".revision = (((hackage."ghc-prim")."0.6.1").revisions).default;
        "mtl".revision = (((hackage."mtl")."2.2.2").revisions).default;
        "rts".revision = (((hackage."rts")."1.0").revisions).default;
        "QuickCheck".revision = (((hackage."QuickCheck")."2.14.1").revisions).default;
        ...
      }
      compiler = {
        version = "8.10.2";
        nix-name = "ghc8102";
        packages = {
          "ghc-prim" = "0.6.1";
          "mtl" = "2.2.2";
          ...
        }
      }
    }
  ...
}

We can see that the plan contains quite detailed information on the exact versions of dependencies that are needed to build our Haskell project.

Caching Plans with Materialization

The dependency resolution algorithm is not a cheap operation to run, especially in large Haskell projects. This is why Cabal caches the result inside of dist-newstyle/cache instead of recomputing it every time commands such as cabal build are called.

In the case of Nix, we know that a Nix build can be cached and reused if there are no changes in the build inputs. However for the case of a materialized plan, the result of the plan depends on the input source code of the Haskell project. This means that every time the source code changes, the plan needs to be recomputed from scratch.

Furthermore, computing the materialized plan requires a number of dependencies that are not needed when building the Haskell project itself. On the other hand, We know that the materialized plan only changes when either the dependencies list is updated, or when the Hackage index state is updated. So we should be able to cache the materialized plan so that there is no need for Haskell.nix to recompute the dependencies all the time.

In Haskell.nix, the act of manually caching the derivation plan is called materialization. To put it simply, we can materialize the derivation plan by copying the build output of the plan-nix derivation and check that into the source control of the project.

If we already have the plan files cached in version control, we can then pass the path to the .nix file as the materialized attribute when defining our Haskell.nix project.

Example Materialized Haskell Project

The Nix project 07-haskell.nix-materialized contains an example Haskell.nix project that provides basic support for materialization.

As compared to the simplified version, the project.nix has a few more additional fields passed to Haskell.nix:

{ useMaterialization ? true }:
let
  sources = import ../sources.nix {};

  haskell-nix = import sources."haskell.nix" {};

  nixpkgs = haskell-nix.pkgs;

  src = builtins.path {
    name = "haskell-project-src";
    path = ../../haskell;
    filter = path: type:
      let
        basePath = builtins.baseNameOf path;
      in
      basePath != "dist-newstyle"
    ;
  };

  project = nixpkgs.haskell-nix.cabalProject {
    inherit src;

    compiler-nix-name = "ghc8102";

    index-state = "2020-12-04T00:00:00Z";

    materialized = if useMaterialization
      then ./plan else null;

    plan-sha256 = if useMaterialization
      then nixpkgs.lib.removeSuffix "\n"
        (builtins.readFile ./plan-hash.txt)
      else null;

    exactDeps = true;
  };
in
project

The project.nix file is now a function accepting a useMaterialization argument. If it is set to true, then the materialized plan is passed to Haskell.nix through the materialized attribute.

We also need to explicitly provide Haskell.nix the version of Hackage snapshot we want to use, which we hard code to "2020-12-04T00:00:00Z".

We set the exactDeps option here to true, so that when the materialized plan is out of sync, we get error when running cabal build inside the Nix shell.

There are also a new plan directory, a plan-hash.txt file, and a sync-materialized.sh script:

#!/usr/bin/env bash

plan=$(nix-build -j4 --no-out-link --arg useMaterialization false -A plan-nix project.nix)

rm -rf plan

cp -r $plan plan

find plan -type d -exec chmod 755 {} \;

nix-hash --base32 --type sha256 plan > plan-hash.txt

The script sync-materialized.sh creates a Haskell.nix project with useMaterialization set to false. It then copies the build output of plan-nix to the plan/ directory, and compute the SHA256 hash of the directory and save it to plan-hash.txt.

As we can see, a Haskell.nix project using materialized plan is a bit more involved. However the performance trade off of using materialized plan in a large Haskell project can be significant. The example project template can hopefully serve as a reference on how to setup a Haskell.nix project with materialization.

Syncing Materialized Plan

When we explicitly cache the materialized plans, there is a risk of the plan file diverging from the actual Haskell dependencies when the .cabal file is updated. It can also introduce additional noise in git commits when dependencies are updated. This can introduce friction in Haskell projects, as all team members become responsible to update the materialized plans when the Haskell dependencies are updated.

Checking in dependencies metadata into version control is a common problem in software projects, particularly Nix projects. As discussed in Dependency Management, this is a necessary evil to make sure that our dependencies are reproducible. Syncing materialized plans can be seen as the same methods as syncing other dependencies lock files such as cabal.project.freeze and package-lock.json.

Nevertheless, the current workflow for managing materialization plans in Haskell.nix is not very convenient, and there are a lot of improvements that could have been made. We can only hope that these steps can be improved in future versions of Haskell.nix.

Or perhaps there are better ways of managing Haskell dependencies, as described in the next chapter.

Caching Haskell.nix Dependencies

One of the downside of multi-versioning approach taken by Haskell.nix is that it is much more difficult to provide a general Nix cache for Haskell.nix projects.

When Haskell projects are built using unmodified nixpkgs, there is usually no need to build any Haskell dependencies at all. Instead the Haskell packages are downloaded directly from cache.nixos.org.

This is possible thanks to the mono-versioning approach of nixpkgs. Since there is exactly one version of each package and their dependencies, the build result of the same dependency can always be reused for multiple packages. As such, the number of Nix packages that need to be cached are relatively small, and the default NixOS cache takes care of caching all of them.

In contrast, in Haskell.nix the dependencies for a package can change depending on the requirements of its dependents. So instead of caching one copy of each version of Haskell package, there is a combinatory explosion of packages to be cached - one for each possible combination of transitive dependencies for that package alone.

Because of this, it is common that building a Haskell.nix project also requires building all the Haskell dependencies. We will discuss about how to use Cachix to cache such Nix dependencies in the future.

Other Package Management Strategies

So far we have covered two approaches to managing dependencies in Nix. Nixpkgs takes a mono-versioning approach by providing exactly one version of each package in a particular version of nixpkgs. In contrast, Haskell.nix captures the entire Hackage snapshot and uses Cabal to resolve the dependencies with the plan derivation being an evaluation-time Nix dependency.

However these two are not the only approaches to managing dependencies in Nix. In fact, other languages like Node.js and Rust have come out with an alternative approach, which is much closer to generating traditional package lock files.

Size of Package Registry

Compared to mainstream languages like Node.js, the Haskell ecosystem is much smaller and has much less packages available. Despite that, the state of the Hackage registry is pretty large, taking over 100 MiB after compression.

Capturing the entire state of the package registry is obviously not scaleable. If the Haskell ecosystem were to grow to the size of Node.js, not only the nixpkgs and Haskell packages in nixpkgs have to be re-architected, but also Cabal itself has to come up with better ways of managing dependencies.

Compared to Cabal, package managers like npm do not simply download the entire state of the package registries. Instead, the package registries provide APIs for the package managers to query the available versions of specific packages. This way, the local package managers only have to query the relevant packages they need, and ignore the rest of the packages exist in the registry.

This approach has the advantage of significantly reducing the bandwidth required to compute the dependency graph. However since it requires network communication, this cannot be used inside a Nix build. After all, there is no way Nix can know if a package registry always give back the same answer given the same set of queries.

Generating Nix Plans

Languages like Node.js and Rust come out with tools like node2nix and cargo2nix to convert the dependency resolution results directly into Nix expressions.

For these tools to work, they have to run outside of a Nix build to query the package registry, and then generate the Nix expressions. Typically, the Nix expressions are also derived directly from the lock files such as package-lock.json and Cargo.lock.

The generated Nix expressions directly construct the dependency graph required to build a Node.js or Rust project. When nix-build is called, Nix can then easily use the dependency graph to download and build the dependencies concurrently.

Similar Approach In Haskell

While there is a similarly named cabal2nix command for Haskell, that is actually tied to the mono-versioning approach of nixpkgs. As a result, cabal2nix never computes any dependency graph, but instead simply generates a Nix expression that ask for the dependencies from nixpkgs.

There is no reason why Haskell cannot adopt similar approaches as node2nix or cargo2nix. It might be possible that such approaches are more difficult to achieve, due to how Cabal and Hackage index works. But that should not be an excuse to stop us from trying.

Whether to use Nix or not, if we want Haskell to gain wider adoption, then the package manager Cabal needs significant improvement to be made. We need to improve Cabal to not just have better integrations with tools like Nix and IDEs, but also make it much easier for users to use.

Caching Nix Packages

Nix can be used to build all kinds of packages from the ground up. A Nix packages can be as simple as a hello world output, or as large as projects such as GHC and GCC. If every time we try to build our Nix package from scratch together with all its dependencies, it is going to take unreasonably long time.

Fortunately, NixOS provides a cache for most packages provided by nixpkgs. The cache is usually enabled by default in your local nix.conf file. Thanks to this, we can simply download the packages in nixpkgs instead of building them from scratch.

The default cache from cache.nixos.org is usually sufficient if we are just using Nix to build small projects. But what if we are using Nix to build large projects that take hours to build? What if we need to use Nix to build something like fib(50)?

Cachix

Cachix is a cloud service for us to easily host our own Nix cache. The service is free to use for public cache, and paid plan is available for private Nix cache.

Other than Cachix, there are other options available for hosting Nix cache, such as nix-serve and hydra. However they require rolling up your own infrastructure to host the servers, which can take quite a bit of effort.

Cachix is by far the easiest way to setup a Nix cache. In this section we will go through the details of caching Nix packages using Cachix. There are also official documentation available on using Cachix, which you should check out.

To get started using Cachix, first sign up for an account at https://app.cachix.org/signup. Or if you have already signed up, login at https://app.cachix.org/login. After logging in, create a new binary cache at https://app.cachix.org/cache.

Create Cache

Choose a unique name for your cache. For write access, leave with the default option "API tokens". We will also use the default public read access, since our test cache do not contain sensitive data.

After creating the cache, we will also need to create an auth token to push to our cache. Go to https://app.cachix.org/personal-auth-tokens to generate one.

Create Cache

Enter a suitable description for your Cachix token, then click the "Generate" button.

Create Cache

The web page will then show us the generated token, which we can later enter in our shell. For the purpose of this workshop, also save the auth token as cachix-token in the nix-workshop/config directory. (Remember to exclude the cachix authtoken prefix).

You should keep the generated token secret, as it can be used to push to the Cachix store you own, as well as accessing private cache. Having a proper description can help us identify the tokens that we are no longer using, so that we can revoke them at a later time.

Now that we have setup our Cachix account, we can install Cachix on our local machine using Nix:

$ nix-channel --update
...
$ nix-env -i cachix
...
$ cachix --version
cachix 0.6.0

At the point of writing the version of our cachix command is 0.6.0. We can then configure Nix to use our Cachix store by running:

cachix use my-awesome-nix-cache

(Replace the store name my-awesome-nix-cache with the Cachix store name that you have created)

Docker

It is difficult to test whether a Nix cache works when using a single machine. Since Nix also locally caches a Nix package after building it, we can't really verify locally that Nix will download from our cache the next time we build it on a fresh machine.

Although we could force rebuild everything using nix-collect-garbage, that would also destroy all local builds we have. So we might not want to use that nuclear option.

For the purpose of this workshop, we provide a Dockerfile that you can use to enter an Ubuntu Docker container with fresh Nix environment. You can enter the container by simply running:

make docker

Inside the docker container, you can run as the nix user with the workshop directory mounted at ~/nix-workshop.

The Docker container is started with the --rm option, so storage for the local Nix store is reclaimed when you exit the container. You can run multiple copies of the container in separate shell, to test whether the Nix packages are cached properly.

We still have to configure our Docker container to use the Cachix store that we have just created. To simplify the configuration, save your Cachix credentials in nix-workshop/config, with the file cachix-store containing the name of your Cachix store, and cachix-token containing the auth token we have just created earlier.

When entering the Docker container, it will automatically source scripts/setup.sh to read the config files and run the following:

CACHIX_STORE=$(cat ~/nix-workshop/config/cachix-store)
cachix use $CACHIX_STORE
cachix authtoken $(cat ~/nix-workshop/config/cachix-token)

Caching Fibonacci

Now that our Cachix store is setup properly, we can try to actually cache some Nix builds. We will reuse the fib.nix derivation that we defined in earlier chapter.

First of all, we use nix-instantiate to instantiate our Nix derivation, and then save the result into $drv. We will create the fib(4) derivation with the prefix "foo". (You can choose your own prefix here)

$ drv=$(nix-instantiate -E '
  import ./code/04-derivations/03-fibonacci/fib.nix
    "foo" 4
')
...
warning: you did not specify '--add-root'; the result might be removed by the garbage collector

When running nix-instantiate, it will produce the warning line warning: you did not specify '--add-root' ... which we can usually ignore.

Now we can build $drv, which can take a while to finish.

$ echo $drv
/nix/store/a4qb7vq7ws2q01jd5a07zpml5hw381nl-foo-fib-4.drv

$ nix-build --no-out-link $drv
...
/nix/store/zdq2p21pq836n3k1xkh4yb8wkvl9fy0l-foo-fib-4

Single Build Output

Now that we have built our derivation, how do we actually cache it using Cachix? At the most basic level, we can use cachix push to push a particular Nix store path to Cachix:

$ nix-build --no-out-link $drv | cachix push $CACHIX_STORE
compressing and pushing /nix/store/zdq2p21pq836n3k1xkh4yb8wkvl9fy0l-foo-fib-4 (288.00 B)
All done.

In our naive attempt, we simply pipe the build output path of fib(4) to cachix push. From the output we can see that Cachix has pushed a single path to our store.

Now if we try to build fib(4) again in another container, we can see that Nix would fetch the result directly from Cachix:

$ nix-build --no-out-link $drv
these paths will be fetched (0.00 MiB download, 0.00 MiB unpacked):
  /nix/store/zdq2p21pq836n3k1xkh4yb8wkvl9fy0l-foo-fib-4
copying path '/nix/store/zdq2p21pq836n3k1xkh4yb8wkvl9fy0l-foo-fib-4' from 'https://scrive-nix-workshop.cachix.org'...
/nix/store/zdq2p21pq836n3k1xkh4yb8wkvl9fy0l-foo-fib-4

Everything looks fine so far. But in fact fib(0) to fib(3) have not yet been cached in our store. So Nix will still have to rebuild the dependencies when they are needed, such as when entering Nix shell:

$ nix-shell $drv
these derivations will be built:
  /nix/store/6mc3ccymdyfmqacrq5vyc43zb2gl81ml-foo-fib-1.drv
  /nix/store/hj0g7hn703axx44x29l27xb1nrdg83rh-foo-fib-0.drv
  /nix/store/k65i01s85dix9xcgxyaggc8l13lx1rrz-foo-fib-2.drv
  /nix/store/wgcp26v3g23x9i9iqiirn20pgmv4mgki-foo-fib-3.drv

The dependencies also need to be rebuilt when we try to build something like fib(6), which depends on not only fib(4) but also fib(3):

$ drv=$(nix-instantiate -E '
  import ./code/04-derivations/03-fibonacci/fib.nix
    "foo" 6
')
$ nix-build --no-out-link $drv
these derivations will be built:
  /nix/store/6mc3ccymdyfmqacrq5vyc43zb2gl81ml-foo-fib-1.drv
  /nix/store/hj0g7hn703axx44x29l27xb1nrdg83rh-foo-fib-0.drv
  /nix/store/k65i01s85dix9xcgxyaggc8l13lx1rrz-foo-fib-2.drv
  /nix/store/wgcp26v3g23x9i9iqiirn20pgmv4mgki-foo-fib-3.drv
  /nix/store/m3sspba1wz9ffp5qyjplg8fjbnhy7d73-foo-fib-5.drv
  /nix/store/zyhq28hxak4jk7xak6lixa4lbfxdjwvz-foo-fib-6.drv
...

How cachix push works

The cachix push command works by reading the Nix store paths from STDIN, and push each of the path to the specified Cachix store. More specifically, cachix push pushes the closure of the Nix path.

For the case above, the build result of fib(4) does not have any runtime dependency, so only the build itself is pushed to Cachix.

We can also try pushing fib-4.drv itself to Cachix, and we can see that it pushes the .drv derivation of all its dependencies as well.

$ echo $drv | cachix push $CACHIX_STORE
compressing and pushing /nix/store/0hfyfy1wxlri4gdcmikg7v0ybvpkl3yl-Python-3.8.6.tar.xz.drv (856.00 B)
compressing and pushing /nix/store/0vjq3889mc2z9v02hcw072ay0fivbshx-nuke-references.drv (1.41 KiB)
compressing and pushing /nix/store/0rgf63snfi078knpghs1jf2q3913gd17-bootstrap-stage4-gcc-wrapper-10.2.0.drv (7.05 KiB)
...
compressing and pushing /nix/store/hj0g7hn703axx44x29l27xb1nrdg83rh-foo-fib-0.drv (1.41 KiB)
...

Similarly if we instead try to push the build result of upper-greet.nix, we can see that the build result of greet.nix is pushed as well.

$ drv=$(nix-instantiate ./code/04-derivations/02-dependencies/upper-greet.nix)
$ nix-build --no-out-link $drv | cachix push $CACHIX_STORE
these derivations will be built:
  /nix/store/wirssa651gwxv6z8ik78ac05c7f9ml3b-greet.drv
  /nix/store/aqznnbrbplwm2mvybzhp6wxw5inrq2aj-upper-greet.drv
...
compressing and pushing /nix/store/391ab6p11dh6gk4crc9a8pxym7c2v7lc-upper-greet (704.00 B)
compressing and pushing /nix/store/ws8lpdazs14zjcsgifkpdy060qsiakk6-greet (568.00 B)
All done.

However for neither approach can push the build output of fib(0) to fib(3), which are build dependencies of fib(4).

Caching Build Dependencies

In practice, when we are are caching a derivation such as fib(4), we would also want to cache it together with all its build dependencies, including fib(0) to fib(3) which we have just built. To do that we have to use cachix push together with other Nix commands.

Recall that the nix-store -qR command gives us all the dependencies of a derivation:

$ nix-store -qR $drv
/nix/store/01n3wxxw29wj2pkjqimmmjzv7pihzmd7-which-2.21.tar.gz.drv
/nix/store/03f77phmfdmsbfpcc6mspjfff3yc9fdj-setup-hook.sh
...

We can grep specifically for the fib dependencies that we are interested in:

$ nix-store -qR $drv | grep fib-
/nix/store/6mc3ccymdyfmqacrq5vyc43zb2gl81ml-foo-fib-1.drv
/nix/store/hj0g7hn703axx44x29l27xb1nrdg83rh-foo-fib-0.drv
/nix/store/k65i01s85dix9xcgxyaggc8l13lx1rrz-foo-fib-2.drv
/nix/store/wgcp26v3g23x9i9iqiirn20pgmv4mgki-foo-fib-3.drv
/nix/store/a4qb7vq7ws2q01jd5a07zpml5hw381nl-foo-fib-4.drv

To push the build result of all dependencies, we can add the --include-outputs option:

$ nix-store -qR --include-outputs $drv | grep fib-
/nix/store/20flzbyx97kly3n34krlmjg9awjn6a5z-foo-fib-3
/nix/store/52j5p1a03vi8dxn7rh4s8y6n5ml318rq-foo-fib-0
/nix/store/6mc3ccymdyfmqacrq5vyc43zb2gl81ml-foo-fib-1.drv
/nix/store/hj0g7hn703axx44x29l27xb1nrdg83rh-foo-fib-0.drv
/nix/store/k65i01s85dix9xcgxyaggc8l13lx1rrz-foo-fib-2.drv
/nix/store/wgcp26v3g23x9i9iqiirn20pgmv4mgki-foo-fib-3.drv
/nix/store/a4qb7vq7ws2q01jd5a07zpml5hw381nl-foo-fib-4.drv
/nix/store/c7lwn4mfn3pk0hhvc98lg1r6z6c8pb6c-foo-fib-1
/nix/store/qih0iazs5yl3dg694a2fz0jzzlxzy7k8-foo-fib-2
/nix/store/zdq2p21pq836n3k1xkh4yb8wkvl9fy0l-foo-fib-4

Finally, we don't really need to push the .drv files themselves to Cachix, as Nix always regenerate them during evaluation of the .nix files. We can use grep -v to exclude them:

$ nix-store -qR --include-outputs $drv | grep -v .drv | grep fib-
/nix/store/20flzbyx97kly3n34krlmjg9awjn6a5z-foo-fib-3
/nix/store/52j5p1a03vi8dxn7rh4s8y6n5ml318rq-foo-fib-0
/nix/store/c7lwn4mfn3pk0hhvc98lg1r6z6c8pb6c-foo-fib-1
/nix/store/qih0iazs5yl3dg694a2fz0jzzlxzy7k8-foo-fib-2
/nix/store/zdq2p21pq836n3k1xkh4yb8wkvl9fy0l-foo-fib-4

Now that we got the list of build outputs to push, we can then pipe them to cachix push:

$ nix-build $drv && nix-store -qR --include-outputs $drv | grep -v .drv | cachix push $CACHIX_STORE
compressing and pushing /nix/store/20flzbyx97kly3n34krlmjg9awjn6a5z-foo-fib-3 (288.00 B)
compressing and pushing /nix/store/52j5p1a03vi8dxn7rh4s8y6n5ml318rq-foo-fib-0 (288.00 B)
compressing and pushing /nix/store/c7lwn4mfn3pk0hhvc98lg1r6z6c8pb6c-foo-fib-1 (288.00 B)
compressing and pushing /nix/store/qih0iazs5yl3dg694a2fz0jzzlxzy7k8-foo-fib-2 (288.00 B)
compressing and pushing /nix/store/zdq2p21pq836n3k1xkh4yb8wkvl9fy0l-foo-fib-4 (288.00 B)
All done.

This time if we try to build fib(6) on a fresh machine, it will download the cache result of both fib(3) and fib(4) from Cachix:

$ nix-build -E '
  import ./code/04-derivations/03-fibonacci/fib.nix
    "foo" 6
'
these derivations will be built:
  /nix/store/m3sspba1wz9ffp5qyjplg8fjbnhy7d73-foo-fib-5.drv
  /nix/store/zyhq28hxak4jk7xak6lixa4lbfxdjwvz-foo-fib-6.drv
these paths will be fetched (61.76 MiB download, 260.53 MiB unpacked):
  /nix/store/20flzbyx97kly3n34krlmjg9awjn6a5z-foo-fib-3
  /nix/store/zdq2p21pq836n3k1xkh4yb8wkvl9fy0l-foo-fib-4
  ...
building '/nix/store/m3sspba1wz9ffp5qyjplg8fjbnhy7d73-foo-fib-5.drv'...
...
building '/nix/store/zyhq28hxak4jk7xak6lixa4lbfxdjwvz-foo-fib-6.drv'...
...
/nix/store/vrzxqqj6q11lgpizsd78r2cx2c7zfban-foo-fib-6

Evaluation-time Dependencies

As mentioned in chapter 14, It is much more tricky to find the evaluation time dependencies and push them to Cachix.

If we try to Cache the build dependencies of fib-serialized.nix, it wouldn't really workd.

$ drv=$(nix-instantiate -E '
  import ./code/04-derivations/03-fibonacci/fib-serialized.nix
    "foo" 4
')
building '/nix/store/6mc3ccymdyfmqacrq5vyc43zb2gl81ml-foo-fib-1.drv'...
...
building '/nix/store/hj0g7hn703axx44x29l27xb1nrdg83rh-foo-fib-0.drv'...
...
building '/nix/store/bg0kqrl14p99y1k0g47gcx7a4ik4qk1m-foo-fib-3.drv'...
...

$ nix-build $drv && nix-store -qR --include-outputs $drv | grep -v .drv | cachix push $CACHIX_STORE
compressing and pushing /nix/store/rlllddnljlv1qzlizdr97q5wbzlqpq5k-foo-fib-4 (288.00 B)
All done.

At this point, there is no simple way to find out all evaluation time dependencies to push. But in case we really want to push all evaluation time dependencies, there is still one nuclear option.

Push All Nix Derivations

cachix push provides a watch-exec command to watch the global Nix store, and push all new paths that are added to the Nix store during the execution of our command.

$ cachix watch-exec $CACHIX_STORE nix-build -- \
  --no-out-link \
  -E 'import ./code/04-derivations/03-fibonacci/fib-serialized.nix
    "foo" 4'
...
Watching /nix/store for new store paths ...
building '/nix/store/hj0g7hn703axx44x29l27xb1nrdg83rh-foo-fib-0.drv'...
compressing and pushing /nix/store/52j5p1a03vi8dxn7rh4s8y6n5ml318rq-foo-fib-0 (288.00 B)
building '/nix/store/6mc3ccymdyfmqacrq5vyc43zb2gl81ml-foo-fib-1.drv'...
...
compressing and pushing /nix/store/c7lwn4mfn3pk0hhvc98lg1r6z6c8pb6c-foo-fib-1 (288.00 B)
building '/nix/store/74x1nl7paqin5zcrkkj94bbkm25shpx9-foo-fib-2.drv'...
...
compressing and pushing /nix/store/ia5arkikhbyd9drjzxm2lqgr5a1b6n9m-foo-fib-2 (288.00 B)
building '/nix/store/bg0kqrl14p99y1k0g47gcx7a4ik4qk1m-foo-fib-3.drv'...
...
compressing and pushing /nix/store/ykvgv0hvpm93glrjzpyb7hkashq5rr1q-foo-fib-3 (288.00 B)
these derivations will be built:
  /nix/store/m25lspbpyv09vl3pz3sf3q20ndcrilq9-foo-fib-4.drv
building '/nix/store/m25lspbpyv09vl3pz3sf3q20ndcrilq9-foo-fib-4.drv'...
...
/nix/store/rlllddnljlv1qzlizdr97q5wbzlqpq5k-foo-fib-4
Stopped watching /nix/store and waiting for queue to empty ...
compressing and pushing /nix/store/rlllddnljlv1qzlizdr97q5wbzlqpq5k-foo-fib-4 (288.00 B)
Waiting to finish: 1 pushing, 0 in queue
Done.

We can see during the build that Nix pushes a lot of things to Cachix, including the downloaded tarballs from sources like nixpkgs, which are in fact also evaluation time dependencies.

Now if we go to a fresh machine and try to build fib-serialized, we can see that this time it is correctly downloading the build results of fib(0) to fib(4) from Cachix:

$ nix-build -E '
  import ./code/04-derivations/03-fibonacci/fib-serialized.nix
    "foo" 6'
...
copying path '/nix/store/c7lwn4mfn3pk0hhvc98lg1r6z6c8pb6c-foo-fib-1' from 'https://scrive-nix-workshop.cachix.org'...
copying path '/nix/store/52j5p1a03vi8dxn7rh4s8y6n5ml318rq-foo-fib-0' from 'https://scrive-nix-workshop.cachix.org'...
copying path '/nix/store/ia5arkikhbyd9drjzxm2lqgr5a1b6n9m-foo-fib-2' from 'https://scrive-nix-workshop.cachix.org'...
copying path '/nix/store/ykvgv0hvpm93glrjzpyb7hkashq5rr1q-foo-fib-3' from 'https://scrive-nix-workshop.cachix.org'...
copying path '/nix/store/rlllddnljlv1qzlizdr97q5wbzlqpq5k-foo-fib-4' from 'https://scrive-nix-workshop.cachix.org'...
...
building '/nix/store/9ykk0mg8b72nr73hfb82vs49pfyqinzg-foo-fib-5.drv'...
...
these derivations will be built:
  /nix/store/wfqhsi5rccz5wb620axn1w3sbjhalw1s-foo-fib-6.drv
building '/nix/store/wfqhsi5rccz5wb620axn1w3sbjhalw1s-foo-fib-6.drv'...
/nix/store/7rx619rpisnh9sw2g3sk6yq07jb563yh-foo-fib-6

There is also cachix watch-store command that provides very coarse-grained control for caching everything to Cachix. This can be useful if we do not care about what is being cached, and instead just want to cache everything.

However as we will see in the next chapter, there might be things that we do not want to cache to Cachix, such as proprietary source code or secret credentials.

Caching Haskell Nix Packages

Similar to the previous chapter, we can cache our Haskell.nix project in similar way.

$ drv=$(nix-instantiate ./code/05-package-management/haskell-project-v3/nix/07-haskell.nix-materialized)

$ nix-build $drv && nix-store -qR --include-outputs $drv | grep -v .drv | cachix push $CACHIX_STORE
...
/nix/store/8vrdfinxxnwczn4jzknm44bsn3k5nghl-haskell-project-exe-hello-0.1.0.0
compressing and pushing /nix/store/8vrdfinxxnwczn4jzknm44bsn3k5nghl-haskell-project-exe-hello-0.1.0.0 (3.60 MiB)
compressing and pushing /nix/store/6apx83l6ss3hkn0kd4z4rkjbkgs0w4w2-default-Setup-setup (18.07 MiB)
compressing and pushing /nix/store/3pfy3dd8ch77km1wkwd6cdgqn57d4347-haskell-project-exe-hello-0.1.0.0-config (304.02 KiB)
compressing and pushing /nix/store/b2j1nrsjr8cpzmk58d476fc2snz17w75-ghc-8.10.2 (1.71 GiB)
...
All done.

As simple as it might look, the naive approach however has some flaws, especially when dealing with private projects.

Leaking Source Code

The first issue with pushing everything is source code contamination, i.e. the source code of the project leaking to the cache. For instance, suppose we modify the main function to print "Hello, World!" instead of "Hello, Haskell!":

$ sed -i 's/Hello, Haskell!/Hello, World!/g' ./code/05-package-management/haskell-project-v3/haskell/Main.hs
$ cat ./code/05-package-management/haskell-project-v3/haskell/Main.hs
module Main where

main :: IO ()
main = putStrLn "Hello, World!"

If we try to rebuild our Haskell project and push it to Cachix, we can notice that the modified source code is also pushed as well. (Notice the drv=$(nix-instantiate ...) assignment has to be re-run to get the new derivation with the modified source)

$ drv=$(nix-instantiate ./code/05-package-management/haskell-project-v3/nix/07-haskell.nix-materialized)
$ nix-build $drv && nix-store -qR --include-outputs $drv | grep -v .drv | cachix push $CACHIX_STORE
...
/nix/store/hkqkig7y1dx96qbdwkhk0anb0xdmx6hm-haskell-project-exe-hello-0.1.0.0
compressing and pushing /nix/store/hkqkig7y1dx96qbdwkhk0anb0xdmx6hm-haskell-project-exe-hello-0.1.0.0 (3.60 MiB)
compressing and pushing /nix/store/wq6ry5x7b5x3ld0d7wd2wx3vkxp4wi66-haskell-project-src (1.49 KiB)
All done.

We can list the files in /nix/store/6a049f3fv8x2rdxv34k14cxrwi9an43f-haskell-project-src and verify that it indeed contains our modified source code. Yikes!

$ ls -la /nix/store/6a049f3fv8x2rdxv34k14cxrwi9an43f-haskell-project-src
total 180
dr-xr-xr-x 2 user user   4096 Jan  1  1970 .
drwxr-xr-x 1 user user 151552 Jan  7 15:41 ..
-r--r--r-- 1 user user     15 Jan  1  1970 .gitignore
-r--r--r-- 1 user user     65 Jan  1  1970 Main.hs
-r--r--r-- 1 user user     46 Jan  1  1970 Setup.hs
-r--r--r-- 1 user user     12 Jan  1  1970 cabal.project
-r--r--r-- 1 user user    307 Jan  1  1970 haskell-project.cabal

$ cat /nix/store/6a049f3fv8x2rdxv34k14cxrwi9an43f-haskell-project-src/Main.hs
module Main where

main :: IO ()
main = putStrLn "Hello, World!"

Pushing source code to Cachix might not be a big deal for open source projects. However this may be an issue for propritary projects with strict IP policies. This could be partially mitigated by paying for a private Cachix store. But we just have to be aware of it and be careful.

Leaking Secrets

Even for the case of open source projects, indiscriminately pushing everything to Cachix still carries another risk, which is accidentally leaking secrets such as authentication credentials.

Suppose that we have some security credentials stored locally in the secret.key file in the project directory. Since the file is included in .gitignore, it is not pushed to the git repository.

$ echo secret > ./code/05-package-management/haskell-project-v3/haskell/secret.key
$ ls -la ./code/05-package-management/haskell-project-v3/haskell/
total 32
drwxrwxr-x 2 user user 4096 Jan  7 15:58 .
drwxrwxr-x 4 user user 4096 Dec  8 08:23 ..
-rw-rw-r-- 1 user user   26 Jan  7 15:58 .gitignore
-rw-r--r-- 1 user user   67 Jan  7 15:45 Main.hs
-rw-r--r-- 1 user user   46 Dec  7 08:37 Setup.hs
-rw-rw-r-- 1 user user   12 Dec  7 08:37 cabal.project
-rw-rw-r-- 1 user user  307 Jan  7 09:35 haskell-project.cabal
-rw-rw-r-- 1 user user    7 Jan  7 15:58 secret.key

But is secret.key being included when pushing to Cachix? Let's find out:

$ drv=$(nix-instantiate ./code/05-package-management/haskell-project-v3/nix/07-haskell.nix-materialized)
$ nix-build $drv && nix-store -qR --include-outputs $drv | grep -v .drv | cachix push $CACHIX_STORE
...
compressing and pushing /nix/store/nrmyzkww87ndyp44jkn56hrra8m9d9vy-haskell-project-exe-hello-0.1.0.0 (3.60 MiB)
compressing and pushing /nix/store/ryz8an9z9bw7j1357k9b5w99fxvnhb74-haskell-project-src (1.69 KiB)
All done.

$ ls -la /nix/store/ryz8an9z9bw7j1357k9b5w99fxvnhb74-haskell-project-src
total 188
dr-xr-xr-x 2 user user   4096 Jan  1  1970 .
drwxr-xr-x 1 user user 155648 Jan  7 16:00 ..
-r--r--r-- 1 user user     26 Jan  1  1970 .gitignore
-r--r--r-- 1 user user     67 Jan  1  1970 Main.hs
-r--r--r-- 1 user user     46 Jan  1  1970 Setup.hs
-r--r--r-- 1 user user     12 Jan  1  1970 cabal.project
-r--r--r-- 1 user user    307 Jan  1  1970 haskell-project.cabal
-r--r--r-- 1 user user      7 Jan  1  1970 secret.key

$ cat /nix/store/ryz8an9z9bw7j1357k9b5w99fxvnhb74-haskell-project-src/secret.key
secret

That's not good! Our local security credentials have been leaked to Cachix! If we also have a public Cachix store, the credentials can potentially be obtained by anyone!

The real culprit is in how we create our source derivation in project.nix:

src = builtins.path {
  name = "haskell-project-src";
  path = ../../haskell;
  filter = path: type:
    let
      basePath = builtins.baseNameOf path;
    in
    basePath != "dist-newstyle"
  ;
};

Previously, we made a naive attempt of filtering our source directory and excluding only the dist-newstyle directory to avoid rebuilding the Nix build when the directory is modified by local cabal runs. However if we want to push our source code to Cachix, we better be much more careful.

Gitignore.nix

One way we can protect local secrets is by filtering out all gitignored files so that our source code is close to a fresh git checkout when copied into the Nix store. This can be done using Nix helper libraries such as gitignore.nix.

Using gitignore.nix, we can now create a new haskell-project-v4 project with the source filtered with gitignore.nix:

gitignore = (import sources."gitignore.nix" {
  inherit (nixpkgs) lib;
}).gitignoreSource;

src = nixpkgs.lib.cleanSourceWith {
  name = "haskell-project-src";
  src = gitignore ../../haskell;
};

We first add gitignore.nix into sources using niv, and then import it as above. Following that, we use gitignore ../../haskell to filter the gitignored files in the haskell directory. We then use nixpkgs.lib.cleanSourceWith as a hack to give the filtered source a name haskell-project-src, so that we can grep for it during inspection.

Now if we try to build our derivation, we should get the project source with the local secret filtered out:

$ drv=$(nix-instantiate ./code/06-infrastructure/haskell-project-v4/nix/01-gitignore-src)
$ nix-store -qR --include-outputs $drv | grep haskell-project-src
/nix/store/mhlj5xql8g6ib1wna4g9pc6cpraiz1q8-haskell-project-src-root

$ ls -la /nix/store/mhlj5xql8g6ib1wna4g9pc6cpraiz1q8-haskell-project-src-root
total 140
dr-xr-xr-x 2 nix nix   4096 Jan  1  1970 .
drwxr-xr-x 1 nix nix 114688 Jan 11 11:21 ..
-r--r--r-- 1 nix nix     26 Jan  1  1970 .gitignore
-r--r--r-- 1 nix nix     67 Jan  1  1970 Main.hs
-r--r--r-- 1 nix nix     46 Jan  1  1970 Setup.hs
-r--r--r-- 1 nix nix     12 Jan  1  1970 cabal.project
-r--r--r-- 1 nix nix    307 Jan  1  1970 haskell-project.cabal

Caveats

Gitignore.nix can help us filter out files specified in .gitignore. However it might still be possible that developers would add new secrets locally without adding them to .gitignore. In such case, the secret can still potentially leak to Cachix.

The best way to prevent secrets from leaking is to build from a published git or tarball URL. That way it will be less likely for us to accidentally mix up and leak the secrets in our local file systems. This will however require more complex project organization, as we have to place the Nix code separately from the source code.

Otherwise, it is still recommended to avoid pushing source code to Cachix in the first place, both for proprietary and open source projects. After all, users will almost always build a Nix project with their own local source code, or source that are fetched directly from git or remote URLs. There is rarely a need to use Cachix to distribute source code to our users.

Filtering Out Source

One simple way to filter out the source code is to filter out the name of the source derivation using grep before pushing to Cachix:

$ nix-store -qR --include-outputs $drv \
  | grep -v .drv | grep -v haskell-project-src \
  | cachix push $CACHIX_STORE

Note however this may only work if no other paths pushed to Cachix depends on the source code. This is because Cachix automatically pushes the whole closure of a Nix path. For instance this would not work if we try to push the .drv file of the build derivation to Cachix, because that would also capture the source derivation as part of the closure.

This approach also would not work if there are some intermediate derivations that make copy of the original source code and modify them to produce new source derivation. The intermediate derivation may have a different name, or even a generic one, which it would be difficult for us to filter out without inspecting the derivation source.

As a result, it is best to make use of the patchPhase in stdenv.mkDerivation to modify the source code if necessary.

Caching Nix Shell

Another way to exclude source code from derivation is by creating a Nix shell derivation and cache that instead. Haskell.nix provides a shellFor function that creates a Nix shell derivation from the original Haskell.nix project we defined.

{ useMaterialization ? true }:
let
  project = import ./project.nix {
    inherit useMaterialization;
  };
in
project.shellFor {
  withHoogle = false;
}

If we inspect the derivation tree from shell.nix, we can confirm that indeed the source code not present in the list. And so we can safely push only the Haskell.nix dependencies to Cachix.

drv=$(nix-instantiate ./code/06-infrastructure/haskell-project-v4/nix/01-gitignore-src/shell.nix)

$ nix-store -qR --include-outputs $drv | grep haskell-project-src

We first use nix-shell --run true $drv to build only the dependencies of our shell derivation and push them to Cachix.

$ nix-shell --run true $drv && nix-store -qR --include-outputs $drv | grep -v .drv | cachix push $CACHIX_STORE
...
All done.

If we want to cache the final build artifact as well, we can still run nix-build $drv and then push only the build output to Cachix.

$ nix-build ./code/06-infrastructure/haskell-project-v4/nix/01-gitignore-src | cachix push $CACHIX_STORE
...
compressing and pushing /nix/store/9in65nlw9s255x8zh5g7hlvbnl23rqbz-haskell-project-exe-hello-0.1.0.0 (3.60 MiB)
All done.

Double Check Leaking with Code Changes

Our attempt to cache only the Nix shell derivation seems to exclude the source code, but is it really excluded? If we are not careful, we could easily let Nix give a generic name like source to our source derivation. In that case it would not be possible to detect it through grep if our source code has leaked through.

As a result, it is best to double check what is being cached by slightly modifying our source code, and then try pushing to Cachix again.

$ sed -i 's/Hello, Haskell!/Hello, World!/g' ./code/06-infrastructure/haskell-project-v4/haskell/Main.hs
$ cat ./code/06-infrastructure/haskell-project-v4/haskell/Main.hs
module Main where

main :: IO ()
main = putStrLn "Hello, World!"

$ drv=$(nix-instantiate ./code/06-infrastructure/haskell-project-v4/nix/01-gitignore-src/shell.nix)
$ nix-shell --run true $drv && nix-store -qR --include-outputs $drv | grep -v .drv | cachix push $CACHIX_STORE
All done.

$ nix-build ./code/06-infrastructure/haskell-project-v4/nix/01-gitignore-src | cachix push $CACHIX_STORE
these derivations will be built:
  /nix/store/52qqdj4pq564ivyawpvfzsz2s3kv9wmp-haskell-project-exe-hello-0.1.0.0.drv
...
compressing and pushing /nix/store/fdb6b3dj79gqff0lz0xf34lrs4gpb5a0-haskell-project-exe-hello-0.1.0.0 (3.60 MiB)
All done.

As we expect, even though Main.hs has been modified, there is no new source artifact being pushed to Cachix. Only nix-build produced a new binary, which is then pushed to Cachix.

You can apply the same method on your own project to double check if your source code is leaking to Cachix. Even if you do not care about the source code leaking, this can still serve as a good way to check if any secret is leaking.

Caching Multiple Projects

The technique for caching Nix shell can only work if we have projects made of a single Nix derivation. If we instead have a large project with multiple source repositories, it is much harder to filter out the source code if the derivations depend on each others.

In such cases, the simple way is to use grep -v and hope that it can filter out all the source derivations. Otherwise you may need to use project-specific techniques to make sure that only intended Nix artifacts are being cached.

Conclusion

As we seen in this chapter, caching build results is not as straighforward if there are things that we want to prevent from being cached, such as proprietary source code or local secrets. This is probably not a big issue right now, because many people may not even be aware that their source code and secrets are leaking!

Even without considering leaking secrets, there are still too many different ways of caching build results in Nix. While this provides more flexibility for us to control what to cache, the learning curve is way too high for new users who just want to get their Nix builds cached.

Nix and Cachix may need to implement additional features to help make caching easier, and to protect sensitive data. For example, Cachix may add a command line option to exclude paths matching specific pattern to never be pushed.