In this post I want to explain how we set up a CI infrastructure with Gitlab for our Nix-based project.
tl;dr: arianvp put up a nice definition of a gitlab-runner to execute nix-based builds efficiently. All credits go to him, I’m only writing this blog post because I haven’t seen it discussed anywhere.
With a small web project (which I will write about sometime, maybe) we wanted to explore Haskell in the browser. Given that bootstrapping GHCJS and managing dependencies can be a sub-optimal experience, we naturally gravitated towards using Nix.
Although the scope of our project (part-time student and me working on it on-and-off) doesn’t really demand CI, having CI is still nice to have. And to be honest, I’m very used to it and working without CI really feels like something is missing.
The default for Nix-based continous integration seems to be Hydra. Reasons why we did not choose Hydra:
- There seems to be only limited documentation on how to set it up
- It’s not used outside the Nix community
- We’re quite happy with GitLab for our other projects
That’s why I explored possibilities on how to use GitLab for our purpose.
A few words about GitLab
GitLab is a web platform to manage your git repositories that tries to integrate all parts of a modern software developement workflow. Similiary to GitHub, for example, it also includes a way to track issues and host static pages. GitLab includes proper CI functionality (unlike GitHub which requires you to use an external service like Circle CI or Travis).
A simple example how a CI pipeline setup with GitLab could look:
Per repository you can create one such pipeline by adding a file named
gitlab-ci.yml at the top level. Every pipeline consists of one or more stages (e.g. “build”) and each stage contains one or more jobs (e.g. “nix-build”). The execution of the pipeline is usually triggered by a push to the repository which in turn causes jobs to be added to a queue.
Other computers running instances of
gitlab-runner continously poll the queue for jobs and execute those. There are different kinds of runners, the two most relevants are the so called shell runner which executes the script specified in the
script: section of the yaml file on the host and the docker runner which executes the script inside a docker container.
The naive solution
Caching is an essential part of every CI pipeline, because it’s important to get feedback soon after you committed your code and ideally, you would like to check every single commit. Here’s an example of a pipeline that builds an Angular web application:
We’re telling GitLab to cache the folder
node_modules. This works great for builds like this. For a Maven project you might export a variable like
MAVEN_HOME=./maven and tell GitLab to cache the folder
maven. Naturaly we came up with the idea to specify a job for nix-build like this:
But there are two reasons why this is a bad idea.
- Reason 1:
nix/storeis way too big to be cached between builds. (GitLab runners upload the contents of the cache after every job execution to an S3 store so that it can be downloaded by another runner).
- Reason 2: Even if that wasn’t a problem, it still doesn’t work because - as it turns out - GitLab only allows to cache files inside the checked-out git repository. Most tools allow you to configure the location for the temporary files (e.g. the node_modules or
$MAVEN_HOMEfolder). Unfortunately, with Nix we do not have that luxury.
Arian van Putten has put together a nice GitLab runner here.
You can add that build server as binary cache on your own system. That means that whenever you checkout a new commit or a new branch
nix-build downloads the artifacts from the CI server instead of building it locally, saving you time (and battery!).