--- 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.