From ee90351c170f6a7b56435af7fd0e17e6b8e390b2 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Sun, 16 Feb 2020 22:50:44 -0800 Subject: [PATCH] Add Crystal Nix post --- content/blog/crystal_nix.md | 116 ++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 content/blog/crystal_nix.md diff --git a/content/blog/crystal_nix.md b/content/blog/crystal_nix.md new file mode 100644 index 0000000..0766d38 --- /dev/null +++ b/content/blog/crystal_nix.md @@ -0,0 +1,116 @@ +--- +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.