---
title: Building a Basic Crystal Project with Nix
date: 2020-02-16T14:31:42-08:00
tags: ["Crystal", "Nix"]
---
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](https://crystal-lang.org/),
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](https://github.com/DanilaFe/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`](https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/compilers/crystal/crystal2nix.nix).
`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:
```Bash
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](https://github.com/NixOS/nixpkgs/blob/21bfc57dd9eb5c7c58b6ab0bfa707cbc7cf04e98/pkgs/development/compilers/crystal/build-package.nix#L2). 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
{{< sidenote "right" "version-note" "version" >}}
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, version 0.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.
{{< /sidenote >}} of the package, as usual.
* `crystalBinaries..src` - the source Crystal file for binary `xxx`.
Using these attributes, I concocted the following expression for pegasus and all
of its included programs:
```nix
{ 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//`, 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](https://edef.eu/~qyliss/nixlib/file/nixpkgs/doc/languages-frameworks/crystal.section.md.html):
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](https://nixos.wiki/wiki/Nix_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.