## Packaging a Haskell library for artefact evaluation using nix

Posted on September 19, 2018

This year I packaged two artefacts for the ICFP artefact evaluation process. This post explains the system I used to make it easy to produce the docker images using nix. I hope this documentation will be useful for anyone else submitting a Haskell library for evaluation.

The end result will be an artefact.nix file which is used to build a docker image to submit. It will be an entirely reproducible process as we will fix the versions of all the dependencies we use.

## The structure of the artefact

In this example, I am going to package the artefact from the paper “Generic Deriving of Generic Traversals”. The artefact was a Haskell library and an executable which ran some benchmarks. The resulting artefact will be a docker image which contains:

1. The source code of the library
2. The source code for the benchmarks
3. The executable of the benchmarks
4. An environment where it is possible to compile the library and benchmarks

To start with, I will assume that we have placed the source code and benchmarks code in our current directory. We will add the rest of the files

>>> ls
generic-lens-1.0.0.1/
benchmarks/

## Step 1: Pinning nixpkgs

The most important step of the whole process is to “pin” our version of nixpkgs to a specific version so that anyone else trying to build the image will use the same versions of all the libraries and system dependencies.

Once we have established a commit of nixpkgs that out package builds with. We can use nix-prefetch-git in order to create nixpkgs.json which will provide the information about the pin.

nix-prefetch-git --rev 651239d5ee66d6fe8e5e8c7b7a0eb54d2f4d8621 --url https://github.com/NixOS/nixpkgs.git > nixpkgs.json

Now we have a file, nixpkgs.json which specifies which version of nixpkgs we should use.

We then need to load this file. Some boilerplate, nixpkgs.nix, will do that for us.

opts:
let
hostPkgs = import <nixpkgs> {};
pinnedVersion = hostPkgs.lib.importJSON ./nixpkgs.json;
pinnedPkgs = hostPkgs.fetchFromGitHub {
owner = "NixOS";
repo = "nixpkgs";
inherit (pinnedVersion) rev sha256;
};
in import pinnedPkgs opts

nixpkgs.nix will be imported in artefact.nix and will determine precisely the version of all dependencies we will use.

## Step 2: Using dockerTools

Now we have specified the set of dependencies we want to use we can go about starting to build our docker image. Nixpkgs provides a convenient set of functions called dockerTools in order to create docker images in a declarative manner. This is the start of our artefact.nix file.

let
pkgs = import ./nixpkgs.nix { };
in
with pkgs;
let
debian = dockerTools.pullImage
{ imageName = "debian"
; imageTag = "9.5"
; sha256 = "1jxci0ph7l5fh0mm66g4apq1dpcm5r7gqfpnm9hqyj7rgnh44crb"; };
in
dockerTools.buildImage {
name = "generic-lens-artefact";

fromImage = debian;

contents = [  bashInteractive
glibcLocales
];

config = {
Env = ["LANG=en_US.UTF-8"
mkdir -p $out/programs/benchmarks cp -r${benchmarks-raw}/* $out/programs/benchmarks ''; All the derivation does is copy the directory into the nix store at a specific path. We then just add this to the contents list again and also do the same for the library itself and the README.  contents = [ bashInteractive glibcLocales run-benchmarks benchmarks readme library]; Now once we build the docker image, we’ll have the executable bench available and also a file called README and two folders containing the library code and benchmarks code. ## Step 5: An environment to build the source code Finally, we need to do two more things to make it possible to build the source programs in the container. Including cabal-install in the contents is the first so that we can use cabal in the container.  contents = [ bashInteractive glibcLocales run-benchmarks benchmarks readme library cabal-install ]; The second is much less obvious, we need to make sure that the necessary dependencies are already installed in the environment so that someone can just use cabal build in order to build the package. The way to achieve this is to modify the benchmarks.nix file and change isLibrary to true. - isLibrary = false; + isLibrary = true; This means that all the build inputs for the benchmarks are propagated to the container so all the dependencies for the benchmarks will be available to rebuild them again. ## Complete artefact.nix Here’s the complete artefact.nix that we ended up with. We also generated nixpkgs.json, nixpkgs.nix and benchmarks.nix along the way. let pkgs = import ./nixpkgs.nix {}; in with pkgs; let debian = dockerTools.pullImage { imageName = "debian" ; imageTag = "9.5" ; sha256 = "1y4k42ljf6nqxfq7glq3ibfaqsq8va6w9nrhghgfj50w36bq1fg5"; }; benchmarks-raw = ./benchmarks; benchmarks = runCommand "benchmarks" {} '' mkdir -p$out/programs
mkdir -p $out/programs/benchmarks cp -r${benchmarks-raw}/* $out/programs/benchmarks ''; library-raw = ./generic-lens-1.0.0.1; library = runCommand "benchmarks" {} '' mkdir -p$out/programs
mkdir -p $out/programs/library cp -r${library-raw}/* $out/programs/library ''; readme-raw = ./README; readme = runCommand "readme" {} '' mkdir -p$out/programs
cp ${readme-raw}$out/programs/README
'';

run-benchmarks = haskellPackages.callPackage ./benchmarks.nix {};

in
dockerTools.buildImage {
name = "generic-lens-artefact";

fromImage = debian;

contents = [  bashInteractive
cabal-install
glibcLocales
run-benchmarks
benchmarks
readme
library];

config = {
Env = ["LANG=en_US.UTF-8"
"LOCALE_ARCHIVE=\${glibcLocales}/lib/locale/locale-archive"];
WorkingDir = "/programs";
};
}

## Conclusion

Hopefully this tutorial will be useful for anyone having to package a Haskell library in future. Each artefact is different so you’ll probably have to modify some of the steps in order to make it work perfectly for you. It’s also possible that the dockerTools interface will change but it should be possible to modify the examples here to adapt to any minor changes. If you’re already using nix, you probably know what you’re doing anyway.