Nix for Coq Development

I first learnt about Nix because of Haskell, eventually running into cabal hell while working on many different projects simultaneously. These used the same dependencies but with different versions, which could not be handled by cabal. The common solution is to use the stack build tool. This manages a database of compatible libraries in called stackage which contains a snapshot of hackage packages that have been tested together. This works very well for packages that are on stackage, however, if one uses dependencies that are only on hackage or, even worse, only available through git, it can become hard to integrate these into stack and use them. In addition to that, if one depends on specific versions of command line tools, these can only be documented and will not be installed or tracked by stack.

The other option is to use Nix, which is a general purpose package manager. The feature that sets it apart from other package managers is that it completely isolates every package using hashes and can therefore seamlessly support multiple versions of the same package. This inherently eliminates the cabal hell problem. In addition to that, if you create a nix derivation (name for a nix package) for the current project one is working on, you can launch a shell containing the exact versions that were specified in the derivation to let it build. This also gives Nix the ability to act as a package manager for reproducible builds, instead of needing to resort to Docker. In addition to that, Nix provides one command, nix-build, which will build the whole project.

For projects using Coq, this is exceptionally useful as one can specify dependencies for Coq, OCaml and the shell using the same nix derivation, and launch a shell which contains all the right versions which will let the project build.

Nix Basics

First, to understand nix derivations it is important to go over some Nix basics and explain the language itself, as it is quite different to usual package descriptions. The most interesting fact about the language is that it is purely functional, only using immutable data structures.

The most important thing to know when learning a new language is where to look up existing functions and new syntax for that language. This is particularly important for Nix as it does not have that big of a presence on stackoverflow, and it can be hard to google for the Nix language as that is often an abbreviation "*nix" used for linux. I therefore tend to google for the term “nixpkgs” instead of “nix” which seems to give better results. The main resources are the following:

Searching for anything on those sites should be much more useful than using Google.

There are three main important structures that need to be understood in Nix. There are

  • sets ({ a = 2; b = 3; }),
  • lists ([ 1 2 3 4 ]), and
  • functions (pattern: body).

These are the structures you will come across most often, however, there are many other useful features Nix has that makes it pleasant to work with.

Just like many other functional languages, Nix has let expressions which bind an expression to a name.

let name = expr; in body

It also supports importing an expression, which just evaluates and inserts an expression.

import ./expression.nix;

The with expression is also interesting, which makes all the attributes of a set available in the next expression, unqualified.

with set; expr

There are many other useful constructs such as recursive sets (allows you to refer to keys from the set inside the set), inheriting (copy the current scope into a set or let expression) or conditionals (if c then e1 else e2). However, this should be enough to learn about derivations.

Nix Integration with Coq

I’ll go through an example of how I created a nix derivation for a Coq project which extracted to OCaml, going through all the steps that were necessary to make it more general. Each package in nix is actually a derivation. These are functions that take in the whole collection of all the derivations that are available, select the derivations that are needed using the pattern matching that functions inherently do and returns a new derivation. This is just a set containing information on how to build the package by defining multiple different stages in the build pipeline.

The main function we will use is the mkDerivation helper function which is a wrapper around the more manual derivation function. This function takes in a set which can be used to override various build stages and dependencies.

This example will build a Nix derivation for the Vellvm, so that it builds without any errors and contains all the nix packages that are required.

The first derivation one could come up with is the following, which is just a declaration of all the packages that are needed. The with declaration can be used to bring all the members of the <nixpkgs> set into scope. We then call the mkDerivation function to override some of the attributes inside the set, such as the name (name), the location of the source code (src). These are the only two required attributes for the mkDerivation function, however, that does not mean it will build yet.

with import <nixpkgs> {};

stdenv.mkDerivation {
  # Name of the derivation.
  name = "coq${coq.coq-version}-vellvm";

  # Location of the source code which is available on GitHub.
  src = fetchGit {
    url = "https://github.com/vellvm/vellvm";
  };
}

To actually get it to build, there are a few attributes that we need to specify in addition to those. The first is customise the build step using the buildPhase attribute. By default, the build will just execute make, however, in this project the makefile is actually in the src directory. We therefore have to change to that directory first before we can do anything.

buildPhase = ''
  cd src && make
'';

This will now execute the makefile correctly, however, it will fail the build because Vellvm has a few dependencies that need to be installed first. These are described in the README, so we can just try and find them in Nix and can add them as build dependencies. Here we can specify Coq dependencies using coqPackages, OCaml dependencies using ocamlPackages, and finally command line tools such as the OCaml compiler or the OCaml build system dune.

buildInputs = [ git coq ocamlPackages.menhir dune coqPackages.flocq
                coqPackages.coq-ext-lib coqPackages.paco 
                coqPackages.ceres ocaml ];

Finally, Nix will execute make install automatically at the end to install the program correctly. In this case, we need to set the COQLIB flag so that it knows where to place the compiled Coq theories. These can be set using the installFlags attribute.

installFlags = [ "COQLIB=$(out)/lib/coq/${coq.coq-version}/" ];

We then have the following Nix derivation which should download Vellvm and build it correctly.

with import <nixpkgs> {};

stdenv.mkDerivation {
  name = "coq${coq.coq-version}-vellvm";

  src = ./.;

  buildInputs = [ git coq ocamlPackages.menhir dune coqPackages.flocq
                  coqPackages.coq-ext-lib coqPackages.paco 
                  coqPackages.ceres ocaml ];

  buildPhase = ''
      cd src && make
  '';

  installFlags = [ "COQLIB=$(out)/lib/coq/${coq.coq-version}/" ];
}

However, one last problem we’ll have is that coqPackages.ceres does not actually exist in coqPackages, we were a bit too optimistic. To solve this, however, we can easily define a derivation for ceres from the GitHub repo and insert it as a dependency into the set. Luckily ceres has a nice makefile at the base of the repository and does not have any external dependencies, except for Coq itself. We can therefore define a derivation in the following way. We can use propagatedBuildInputs to define dependencies that the package needs and that derivations using this package will also need. In this case, any derivation using ceres will need Coq, otherwise it would not be useful.

ceres = stdenv.mkDerivation {
  name = "coq${coq.coq-version}-ceres";

  src = fetchGit {
    url = "https://github.com/Lysxia/coq-ceres";
  };

  propagatedBuildInputs = [ coq ];

  installFlags = [ "COQLIB=$(out)/lib/coq/${coq.coq-version}/" ];
};

Finally, we can use a let expression to insert it as a dependency into our own derivation. We now have a complete nix expression that will always build Vellvm correctly in a containerised manner.

with import <nixpkgs> {};

let
  ceres = stdenv.mkDerivation {
    name = "coq${coq.coq-version}-ceres";

    src = fetchGit {
      url = "https://github.com/Lysxia/coq-ceres";
    };

    propagatedBuildInputs = [ coq ];

    installFlags = [ "COQLIB=$(out)/lib/coq/${coq.coq-version}/" ];
  };
in
stdenv.mkDerivation {
  name = "coq${coq.coq-version}-vellvm";

  src = fetchGit {
    url = "https://github.com/vellvm/vellvm";
  };

  buildInputs = [ git coq ocamlPackages.menhir dune coqPackages.flocq
                  coqPackages.coq-ext-lib coqPackages.paco ceres ocaml ];

  buildPhase = ''
      cd src && make
  '';

  installFlags = [ "COQLIB=$(out)/lib/coq/${coq.coq-version}/" ];
}

If one saves the file in default.nix, one can then build the nix expression using the nix-build command. This should return a binary that runs the compiled OCaml code, which was extracted from Coq.