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:
-
pname
- the name of the package. -
version
- the [note: In my example code, I set the Nix package version to the commit hash. Doing this alone is probably not the best idea, since it will prevent version numbers from being ordered. However, version0.1.0
didn't make sense either, since the project technically doesn't have a release yet. You should set this to an actual package version if you have one. ] of the package, as usual. -
crystalBinaries.<xxx>.src
- the source Crystal file for binaryxxx
.
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.