I really like the idea of Nix: you can have reproducible builds, written more or less declaratively. I also really like the programming language Crystal, which is a compiled Ruby derivative. Recently, I decided to try learn NixOS as a package author, and decided to make a Crystal project of mine, pegasus, my guinea pig. In this post, I will document my experience setting up Nix with Crystal.

Getting Started

Pegasus is a rather simple package in terms of the build process - it has no dependencies, and can be built with nothing but a Crystal compiler. Thus, I didn’t have to worry about dependencies. However, the nixpkgs repository does have a way to specify build dependencies for a Nix project: crystal2nix.

crystal2nix is another Nix package, which consists of a single Crystal binary program of the same name. It translates a shards.lock file, generated by Crystal’s shards package manager, into a shards.nix file, which allows Nix to properly build the dependencies of a Crystal package. If you have a project with a shards.lock file, you can use shards2nix inside a nix-shell as follows:

nix-shell -p crystal2nix --run crystal2nix

The above command says, create an environment with the crystal2nix package, and run the program. Note that you should run this inside the project’s root. Also note that if you don’t depend on other Crystal packages, you will not have a shards.lock, and running crystal2nix is unnecessary.

The Crystal folder in the nixpkgs repository contains one more handy utility: buildCrystalPackage. This is a function exported by the crystal Nix package, which significantly simplifies the process of building a Crystal binary package. We can look to crystal2nix.nix (linked above) for a concrete example. We can observe the following attributes:

Using these attributes, I concocted the following expression for pegasus and all of its included programs:

{ stdenv, crystal, fetchFromGitHub }:

let
    version = "0489d47b191ecf8501787355b948801506e7c70f";
    src = fetchFromGitHub {
        owner = "DanilaFe";
        repo = "pegasus";
        rev = version;
        sha256 = "097m7l16byis07xlg97wn5hdsz9k6c3h1ybzd2i7xhkj24kx230s";
    };
in
    crystal.buildCrystalPackage {
        pname = "pegasus";
        inherit version;
        inherit src;

        crystalBinaries.pegasus.src = "src/pegasus.cr";
        crystalBinaries.pegasus-dot.src = "src/tools/dot/pegasus_dot.cr";
        crystalBinaries.pegasus-sim.src = "src/tools/sim/pegasus_sim.cr";
        crystalBinaries.pegasus-c.src = "src/generators/c/pegasus_c.cr";
        crystalBinaries.pegasus-csem.src = "src/generators/csem/pegasus_csem.cr";
        crystalBinaries.pegasus-crystal.src = "src/generators/crystal/pegasus_crystal.cr";
        crystalBinaries.pegasus-crystalsem.src = "src/generators/crystalsem/pegasus_crystalsem.cr";
    }

Here, I used Nix’s fetchFromGitHub helper function. It clones a Git repository from https://github.com/<owner>/<repo>, checks out the rev commit or branch, and makes sure that it matches the sha256 hash. The hash check is required so that Nix can maintain the reproducibility of the build: if the commit is changed, the code to compile may not be the same, and thus, the package would be different. The hash helps detect such changes. To generate the hash, I used nix-prefetch-git, which tries to clone the repository and compute its hash.

In the case that your project has a shards.nix file generated as above, you will also need to add the following line inside your buildCrystalPackage call:

shardsFile = ./shards.nix;

The shards.nix file will contain all the dependency Git repositories, and the shardsFile attribute will forward this list to buildCrystalPackage, which will handle their inclusion in the package build.

That’s pretty much it! The buildCrystalPackage Nix function does most of the heavy lifting for Crystal binary packages. Please also check out this web page: I found out from it that pname had to be used instead of name, and it also has some information regarding additional compiler options and build inputs.

Appendix: A Small Caveat

I was running the crystal2nix (and doing all of my Nix-related work) in a NixOS virtual machine. However, my version of NixOS was somewhat out of date (19.04), and I could not retrieve crystal2nix. I had to switch channels to nixos-19.09, which is the current stable version of NixOS.

There was one more difficulty involved in switching channels: I had to do it as root. It so happens that if you add a channel as non-root user, your system will still use the channel specified by root, and thus, you will experience the update. You can spot this issue in the output of nix-env -u; it will complain of duplicate packages.