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:
- nix-build (Nix) 1.11.15
- nix channel https://nixos.org/channels/nixpkgs-17.09-darwin
Step 1: Install nixpkgs
- Follow steps at https://nixos.org/nix/download.html
- 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,
- nix-shell: configure a shell with a different set of installed packages than your user environment. One for python 2.7 and another for python 3.0 for example.
- nixos: takes immutable-packages concepts and extends it to declaratively configure users, daemon configuration, and daemon’s that run at system startup. As with packages, this configuration is immutable and you can rollback to a previous configuration.
- nixops: one more step up in abstraction; declaratively configure a system (or systems) and deploy that configuration to
a hosting providerthe cloud; supports:- Amazon EC2 instances and other resources (such as S3 buckets, EC2 key pairs, elastic IPs, etc.)
- Google Cloud Engine instances and other resources (such as networks, firewalls, IPs, disks, etc.)
- Azure resources
- VirtualBox virtual machines Note: I could not get the VirtualBox deploy to work on OSX.
- Datadog resources
- Hetzner machines
- NixOS containers
- Any machine already running NixOS.
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:
- it only deletes unused packages
- a package is used if a profile refers to it
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.