How to create a custom nixpkg for a Haskell application

December 17, 2017

Package a custom Haskell application as a nix package to simplify and automate both deploys and rollbacks.

Every nix package is immutable and identified (at least in part) by a hash:

The inputs used to compute the hash are all attributes of the derivation … These typically include the sources, the build commands, the compilers used by the build, library dependencies, and so on. This is recursive: for instance, the sources of the compiler also affect the hash.

This makes deploys and rollbacks safe to automate. Instead of a deploy that overwrites the “global variable” /usr/local/bin/eventarelli-api with a new executable, nix overwrites it with a symlink that points to the newly-built and immutable executable in /nix/store. A rollback points the symlink to the previous version.

Steps

Software used for this HOWTO:

  1. nix-build (Nix) 1.11.15
  2. nix channel https://nixos.org/channels/nixpkgs-17.09-darwin

Step 1: Install nixpkgs

  1. Follow steps at https://nixos.org/nix/download.html
  2. Add 17.09 channel to your user environment:

OSX:

$ nix-channel add https://nixos.org/channels/nixpkgs-17.09-darwin

GNU/Linux:

$ nix-channel add https://nixos.org/channels/nixpkgs-17.09

Step 2: Install cabal2nix

$ nix-env -i cabal2nix
installing ‘cabal2nix-2.6’
these paths will be fetched (0.00 MiB download, 0.00 MiB unpacked):
  /nix/store/5qsfk8sm53bkzb29mdj0z92hzdpl955h-cabal2nix-2.6-doc
fetching path ‘/nix/store/5qsfk8sm53bkzb29mdj0z92hzdpl955h-cabal2nix-2.6-doc’…

*** Downloading ‘https://cache.nixos.org/nar/0zw5lhqzz21cyf7l6cs211alv211ykngjx11ngk0y8yd37gr0ih9.nar.xz’ (signed by ‘cache.nixos.org-1’) to ‘/nix/store/5qsfk8sm53bkzb29mdj0z92hzdpl955h-cabal2nix-2.6-doc’…

building path(s) ‘/nix/store/33jqmv0asmxk3sy84f5h86fikm95l9yj-user-environment’
created 251 symlinks in user environment
$

Step 3: Generate nix expressions for the package.

This step is taken directly from section 9.5.3 How to create Nix builds for your own private Haskell packages of the Nixpkgs Contributors Guide (Version 17.09.2378.af7e47921c4).

Convert dependencies in cabal file to a nix expression:

$ cabal2nix . > eventarelli-api.nix
$ cat eventarelli-api.nix
{ mkDerivation, aeson, base, bytestring, data-default-class, hspec
, http-types, mime-types, monad-control, mtl, mustache, QuickCheck
, random, scotty, sqlite-simple, stdenv, tagsoup, text, time
, timezone-olson, timezone-series, utf8-string, wai, wai-extra
, warp, warp-tls
}:
mkDerivation {
  pname = "eventarelli-api";
  version = "1.1.0";
  src = ./.;
  isLibrary = true;
  isExecutable = true;
  libraryHaskellDepends = [
    aeson base bytestring mustache sqlite-simple tagsoup text time
    timezone-olson timezone-series utf8-string
  ];
  executableHaskellDepends = [
    aeson base bytestring data-default-class http-types mime-types
    monad-control mtl mustache random scotty sqlite-simple text time
    timezone-olson timezone-series utf8-string wai wai-extra warp
    warp-tls
  ];
  testHaskellDepends = [
    base hspec mtl QuickCheck sqlite-simple time timezone-olson
    timezone-series
  ];
  description = "Backend for Eventarelli web site";
  license = stdenv.lib.licenses.unfree;
}
$ 
$ cat > default.nix
{ nixpkgs ? import  {}, compiler ? "ghc802" }:
nixpkgs.pkgs.haskell.packages.${compiler}.callPackage ./eventarelli-api.nix { } 
$ 
$ 
$ cat > shell.nix
{ nixpkgs ? import  {}, compiler ? "ghc802" }:
(import ./default.nix { inherit nixpkgs compiler; }).env
$ 

Step 4: Build the package

Per nixpkgs manual, we are ready to build:

At this point, you can run nix-build to have Nix compile your project and install it into a Nix store path. The local directory will contain a symlink called result after nix-build returns that points into that location.

$ export NIX_PATH=nixpkgs=/Users/mark/.nix-defexpr/channels/nixpkgs-17.09-darwin
$ nix-build 
error: Package ‘eventarelli-api-1.1.0’ in /Users/mark/src/mycode/eventarelli/eventarelli/api/eventarelli-api.nix:8 has an unfree license (‘unfree’), refusing to evaluate.

a) For ‘nixos-rebuild‘ you can set
  { nixpkgs.config.allowUnfree = true; }
in configuration.nix to override this.

b) For ‘nix-env‘, ‘nix-build‘, ‘nix-shell‘ or any other Nix command you can add
  { allowUnfree = true; }
to ~/.config/nixpkgs/config.nix.

$ find $HOME -maxdepth 2 -type f | grep config.nix
/Users/mark/.nixpkgs/config.nix
$ vi /Users/mark/.nixpkgs/config.nix
$ cat /Users/mark/.nixpkgs/config.nix
 {
   allowBroken = true;
   pkgs = {
     vim = {
       python = true;
     };
   };
   allowUnfree = true;
}
$ nix-build
these derivations will be built:
  /nix/store/74wxmnrac9d777h5asj6fpvv5hkis7sb-remove-references-to.drv
  /nix/store/5kwhv5qn0qq9bjn4agqw51pmy1nsckgg-eventarelli-api-1.1.0.drv
these paths will be fetched (264.60 MiB download, 2159.94 MiB unpacked):
  /nix/store/009s1l4g2f3cc9ndb27j5inplry6xd6r-async-2.1.1.1-doc
  /nix/store/06mi088axz39hn09llyciciwm9wf6ri5-regex-posix-0.95.2-doc
  /nix/store/0bg4sdpkngwfpyrqq55jjkmd3k8s9qb6-libyaml-0.1.7

Creating package registration file:
/nix/store/4asrshpg52rh9dnrissailjs4xwyl9x1-eventarelli-api-1.1.0/lib/ghc-8.0.2/package.conf.d/eventarelli-api-1.1.0.conf
post-installation fix up
stripping (with flags -S) in /nix/store/4asrshpg52rh9dnrissailjs4xwyl9x1-eventarelli-api-1.1.0/lib  /nix/store/4asrshpg52rh9dnrissailjs4xwyl9x1-eventarelli-api-1.1.0/bin 
patching script interpreter paths in /nix/store/4asrshpg52rh9dnrissailjs4xwyl9x1-eventarelli-api-1.1.0
patching script interpreter paths in /nix/store/ghvid80b6q5hygh4a6sav2fm7lmvamwr-eventarelli-api-1.1.0-doc
/nix/store/4asrshpg52rh9dnrissailjs4xwyl9x1-eventarelli-api-1.1.0
$

Step 5: Install the package

Immutability:

$ rm -rf result
$ stack clean
$ nix-build
/nix/store/4asrshpg52rh9dnrissailjs4xwyl9x1-eventarelli-api-1.1.0
$ 

Note that if we hadn’t deleted the result directory, we would have rebuild a different hash (i.e., another version of the application) nix expression generated by cabal2nix has the line src=./.;.

Ok, let’s install it:

$ rm result
$ nix-env -f default.nix -i eventarelli-api
installing ‘eventarelli-api-1.1.0’
building path(s) ‘/nix/store/qzhcw2ypa4h1ll62qf3d77ivgbgk7gha-user-environment’
created 259 symlinks in user environment
$

And run it:

$ eventarelli-api 
eventarelli-api: error: export ELMARELLI_TEMPLATES=
CallStack (from HasCallStack):
  error, called at src/Main.hs:78:16 in main:Main
$ 

Poking around

Where is executable?

$ which eventarelli-api
/Users/mark/.nix-profile/bin/eventarelli-api
$ ls -l $(which eventarelli-api)
lrwxr-xr-x  1 root  wheel  85 Dec 31  1969 /Users/mark/.nix-profile/bin/eventarelli-api -> /nix/store/4asrshpg52rh9dnrissailjs4xwyl9x1-eventarelli-api-1.1.0/bin/eventarelli-api
$ 

What is in /nix/store for the app?

$ tree -d -L 3 /nix/store/4asrshpg52rh9dnrissailjs4xwyl9x1-eventarelli-api-1.1.0/
/nix/store/4asrshpg52rh9dnrissailjs4xwyl9x1-eventarelli-api-1.1.0/
├── bin
├── lib
│   ├── ghc-8.0.2
│   │   ├── eventarelli-api-1.1.0
│   │   ├── package.conf.d
│   │   └── x86_64-osx-ghc-8.0.2
│   └── links
└── nix-support

Lots of stuff! In fact: du -sh says 7.4M of stuff in 39 different files.

The ghc-8.0.2 directory holds outputs from ghc. This makes up 37 of the files.

$ tree /nix/store/4asrshpg52rh9dnrissailjs4xwyl9x1-eventarelli-api-1.1.0/lib/ghc-8.0.2 | head
/nix/store/4asrshpg52rh9dnrissailjs4xwyl9x1-eventarelli-api-1.1.0/lib/ghc-8.0.2
├── eventarelli-api-1.1.0
│   ├── Aggregates
│   │   ├── Fan.dyn_hi
│   │   ├── Fan.hi
│   │   ├── Schedule.dyn_hi
│   │   ├── Schedule.hi
│   │   ├── Venue.dyn_hi
│   │   └── Venue.hi
│   ├── Commands.dyn_hi

The lib/links directory holds a ton of symbolic links (149 to be exact).

$ ls -l |head -2 | tail -1
lrwxr-xr-x  1 root  wheel  149 Dec 31  1969 libHSHUnit-1.5.0.0-DvjF79OHhCC7SzfeEty4OI-ghc8.0.2.dylib -> /nix/store/7r29b8b1xf7h6pfxh0r1b2n5grg9bm0d-HUnit-1.5.0.0/lib/ghc-8.0.2/x86_64-osx-ghc-8.0.2/libHSHUnit-1.5.0.0-DvjF79OHhCC7SzfeEty4OI-ghc8.0.2.dylib
$ 

My app depends on HUnit, and the version it uses is /nix/store/7r29b8b1xf7h6pfxh0r1b2n5grg9bm0d-HUnit-1.5.0.0. If this dependency was upgraded, it would be a different immutable package and my application hash would be different. But my the previous version of my app would still be there under /nix/store.

Notes

Haskell nix-build: lints and generates Haddock documentation

By default, the nix build produces both hlint output as well as documentation. I had never bothered to look at those with stack build, and it was a pleasant surprise to see that output from the build.

Nix: this post just scratches the surface of what nix can do

The original nixos paper from 2010 is good read and explains the step from nixpkgs to nixos. You can find it at https://nixos.org/~eelco/pubs/nixos-jfp-final.pdf.

There’s lots more interesting stuff with nix. For example,

What not to do with nix!

As always, this blog does not document all the mistakes I made along the way; it flows from one successful step to the next. Of course, the real world is not like that.

One of the biggest mistakes I made was to manually delete some directories under /nix/store.

Don’t do that.

Nix stores a database of packages that have been used, and although I can grep with the best of them, I could not find where it was keeping a reference to the hash I had deleted from the store.

So I popped on #nixos channel and learned the right way to delete packages is to use nix’s garbage collection, available with the nix-store --gc command.

It works like this:

You can list what profiles nix-store uses by

$ nix-store --gc --print-roots
/Users/mark/src/mycode/eventarelli/eventarelli-deploy/elmarelli-api/result -> /nix/store/z8iikvbgxclxfa213h5p32ph8qk8dlci-elmarelli-api-1.1.0
/nix/var/nix/profiles/default-1-link -> /nix/store/asgx51wzbswgg1j506pg7sq4jyclv0qn-user-environment
/nix/var/nix/profiles/default-2-link -> /nix/store/4yj1xa5dbdy1ccdhnqpma16hfi6aly10-user-environment
/nix/var/nix/profiles/per-user/mark/channels-1-link -> /nix/store/p4y8j01s28xy9z8yqschnwl0gnkbvyql-user-environment
/nix/var/nix/profiles/per-user/mark/channels-2-link -> /nix/store/cfdi9c63z5c82k002xjb937f4gl3mnv7-user-environment
/nix/var/nix/profiles/per-user/mark/profile-1-link -> /nix/store/a8jn4axpj24sa43zmi1f9gc1g9in0jr1-user-environment
/nix/var/nix/profiles/per-user/mark/profile-10-link -> /nix/store/b54m0gp0dp5n5xdai0vh570rd98q06k9-user-environment
/nix/var/nix/profiles/per-user/mark/profile-11-link -> /nix/store/mrj4nb5nsp3lvijiazccp73mjzjn3ffy-user-environment
/nix/var/nix/profiles/per-user/mark/profile-12-link -> /nix/store/qzhcw2ypa4h1ll62qf3d77ivgbgk7gha-user-environment
/nix/var/nix/profiles/per-user/mark/profile-13-link -> /nix/store/mrj4nb5nsp3lvijiazccp73mjzjn3ffy-user-environment
/nix/var/nix/profiles/per-user/mark/profile-2-link -> /nix/store/wv3qbp2jhxi21wgfz8bjir37hwydkm2k-user-environment
/nix/var/nix/profiles/per-user/mark/profile-3-link -> /nix/store/kli1w7b79j4rwc1rdbrrlnw9lcm0jhpi-user-environment
/nix/var/nix/profiles/per-user/mark/profile-4-link -> /nix/store/9i0i0j8lxc4541i09njhzfzbdmcck4fq-user-environment
/nix/var/nix/profiles/per-user/mark/profile-5-link -> /nix/store/nkq331785flvbydry3f66j9l69qz9yyf-user-environment
/nix/var/nix/profiles/per-user/mark/profile-6-link -> /nix/store/wvm0s8qv3a76kr44qxgn49v6n5df2sda-user-environment
/nix/var/nix/profiles/per-user/mark/profile-7-link -> /nix/store/33jqmv0asmxk3sy84f5h86fikm95l9yj-user-environment
/nix/var/nix/profiles/per-user/mark/profile-8-link -> /nix/store/6j9kadfschyvsqxnkx4x7k575wvqdqiv-user-environment
/nix/var/nix/profiles/per-user/mark/profile-9-link -> /nix/store/xmz347y7r53hz2j8v8gx1ayipl0481cs-user-environment
/nix/var/nix/profiles/per-user/root/channels-1-link -> /nix/store/y9bzakg6jiac6nz6z79njcyqwfy1cjp9-user-environment
/nix/var/nix/profiles/per-user/root/channels-2-link -> /nix/store/p4y8j01s28xy9z8yqschnwl0gnkbvyql-user-environment
$ 

This is getting into the innards of how nix works; every time you change your list of installed packages, nix generates a new profile for your user.

To garbage collect a package, you must delete any profile that refers to that package. You can either do this manually; for example,

$ rm /nix/var/nix/profiles/per-user/mark/profile-8-link

or use the nix-collect-garbage utility; for example:

$ nix-collect-garbage --delete-older-than 30d
removing old generations of profile /nix/var/nix/profiles/per-user/mark/profile
removing generation 3
removing generation 2
removing generation 1
removing old generations of profile /nix/var/nix/profiles/per-user/mark/channels
finding garbage collector roots…
deleting garbage…
deleting ‘/nix/store/f9hcj6p17kxx1rd3p4979ky3y85zc30y-user-environment.drv’
deleting ‘/nix/store/kli1w7b79j4rwc1rdbrrlnw9lcm0jhpi-user-environment’
deleting ‘/nix/store/y6vkk87yg28rx8mmvafy7fz964kpn1fr-env-manifest.nix’
deleting ‘/nix/store/ssnz2ngjrz0dcaax2f5sgsm099avd4sp-user-environment.drv’
deleting ‘/nix/store/wv3qbp2jhxi21wgfz8bjir37hwydkm2k-user-environment’
deleting ‘/nix/store/hcg4wdyaj75msjdl90zv3sf2jma4ir9k-user-environment.drv’
deleting ‘/nix/store/a8jn4axpj24sa43zmi1f9gc1g9in0jr1-user-environment’
deleting ‘/nix/store/4jh7mj8a7qq2ywb5cl4yprli56fjxipl-env-manifest.nix’
deleting ‘/nix/store/x316m7rgs38306sdyx7ypy4mhkcjm116-env-manifest.nix’
deleting ‘/nix/store/trash’
deleting unused links…
note: currently hard linking saves 0.00 MiB
9 store paths deleted, 0.02 MiB freed
$ 

Another way to fix things, which I figured out before getting on chat, was to run nix-build --repair as root.