Get your Haskell projects up and running with no fuss using Nix.
Have nix and direnv installed:
$ git clone https://github.com/pwm/nixkell.git world && cd world
$ ./init.sh
$ hpack && cabal build
$ cabal run world
Hello world!
The aim of Nixkell is to provide a seamless experience setting up Haskell projects utilising Nix.
There are other tools for setting up Haskell projects, some of them with great user experience. Nix on the other hand historically had a reputation of being complicated, difficult to learn and not beginner friendly. So why do people use Nix despite its reputation? What are the benefits?
Having a dedicated per-project shell with all the tooling required to work on the project is a game-changer. You can cd foo-project
and have everything ready to work on foo
, then cd ../bar-project
and have everything at hand to work on bar
. This applies even within a single project. For example would you like to quickly upgrade or downgrade the version of GHC to test something? Just update it in nixkell.toml
, cabal build
and everything will automatically rebuild using the choosen version.
It gets better. Anyone working on the project will have the same nix shell and thus the exact same tooling available. As a consequence the bar for contribution becomes a lot lower, as simply pulling a repo and entering nix shell sets the contributor up with everything they need to get hacking.
It gets even better. When building the project itself with nix it happens the same way with the same dependencies pinned to the same versions all around on everyone's machine. No more "Uhm, so how do I build this?".
You guessed it right, it gets even better. As a consequence of reproducibility, people can push the result of their builds into shared binary caches where others can pull from, saving a ton of time not having to build it themselves. This is how the 80,000+ strong nixpkgs are distributed from cache.nixos.org
while "binary cache as a service" solutions, like Cachix, are lifting productivity to new levels.
I hope these points convinces you to give Nix and Nixkell a try.
MacOs specific notes:
- you will need the Xcode Command Line Tools
xcode-select --install
- Install Nix
$ sh <(curl -L https://nixos.org/nix/install)
$ nix --version
- Install direnv:
$ nix-env -iA nixpkgs.direnv
$ direnv --version
-
Once direnv is installed you need to enable it in your shell!
-
Optional: Install cachix to take advantage of Nixkell's own binary cache:
$ nix-env -iA cachix -f https://cachix.org/api/v1/install
$ cachix use nixkell
$ git clone https://github.com/pwm/nixkell.git my-project
$ cd my-project
$ ./init.sh
The purpose of init.sh
is to turn the cloned Nixkell repository into your own. It will:
- Delete the
.git
directory (after making sure it's really Nixkell's) and initiate a new repo - Reset
README.md
to an empty one with your project's name - Set your project's name (my-project in the example) in all relevant files
- Create an
.envrc
file telling direnv to use nix and watchnixkell.toml
,package.yaml
andnix/*
for changes - Fire up the nix shell (note: this can take a while...)
- Finally it deletes itself as you won't need it anymore
The end result is a new haskell project, ready for you to get hacking!
From now on, every time you enter the project's directory direnv will automatically enter the nix shell. Fair warning: it is easy to get used to this :)
Other than loading the nix shell direnv also watches some files (via .envrc
) so when those files are changed direnv will automatically rebuild your shell to reflect those changes. If, for any reason, you want to manually reload:
$ direnv reload
A sensible next step is to open up nixkell.toml
, Nixkell's config file, which is one of the files direnv watches. In there you will see a few options:
- The version of GHC to use
- Tooling you'd like available in your nix shell
- A set of files and paths to ignore by
nix-build
, meaning that nix won't rebuild anything when you change these.
By default Nixkell uses package.yaml
to manage haskell dependencies and utilise hpack
to compile it to cabal. If you rather use the cabal file directly then just run hpack
, delete package.yaml
and add the cabal file to .envrc
for watching. I personally prefer editing the yaml file and auto-generate the cabal file but it's entirely optional.
The usual build cycle is:
$ hpack
$ cabal build
$ cabal run my-project
To test:
$ cabal test --test-show-details=direct
To add dependencies just put them into package.yaml
as usual and direnv will rebuild automatically.
Side note: So why are we using cabal instead of nix to build you might ask? Well, why not both? :) Nix builds are reproducible which is amazing for all the reasons detailed in the elevator pitch and are ideal for your CI. As an example check .github/workflows/nix.yml
. On the other hand nix builds are not incremental whilst cabal builds are. Thus, for local development, cabal leads to a nicer user experience as it will only rebuild what's necessary after a change. If you look in nix/scripts.nix
you will see a few small scripts, one of which is build
, a shorthand for nix-build nix/release.nix
and another is run
which is shorthand for result/bin/my-project
. There are Nixkell's equivalent of cabal build
and cabal run my-project
, respectively. To build and run your project with nix:
$ hpack && build && run
To add something directly from hackage:
cabal2nix cabal:https://some-package-1.2.3.4 > nix/packages/some-package.nix
To add something directly from github:
cabal2nix https://github.com/some-user/some-package > nix/packages/some-package.nix
In both cases direnv will rebuild automatically. This works thanks to the packagesFromDirectory function used in our packages.nix
.
To tweak things further you can add things to the manual section of ourHaskell
in packages.nix
, eg. say you want ot remove version bound checks on some-package
:
some-package = pkgs.haskell.lib.doJailbreak(hprev.some-package);
If you look into nix/sources.json
you will see that packages there are pinned to exact git hashes. Reproducibility, yay! The sources file itself is managed by niv, another tool in our nix shell. To update sources and thus rebuild your shell (as direnv is watching nix/sources.json
):
$ niv update
Most of the nix code in in nix/
:
default.nix
- Theindex.html
of the nix world. Called fromshell.nix
andrelease.nix
overlays.nix
- extends nixpkgs, most importantly with our ownpackages.nix
- The meat, where our package is being assembledrelease.nix
- points to our package, used by thebuild
scriptscripts.nix
- home forbuild
,run
andlogo
sources.{json,nix}
- generated by Nivutil.nix
- some internal helper functionsshell.nix
(in the root) - entry point to the nix shell. Called by direnv upon entering the directory.
That's all there is to it really. Ultimately Nixkell is just a skeleton, a starting point. Once set up it's up to you to mould it to whatever shape your project dictates. It is also less than 200 lines of Nix code, making it easy to just dig in and learn a bit about Nix.
Happy hacking!
I found these links particularly helpful for learning about Nix. In my opinion picking up the language part is easy for people already familiar with Haskell as they have a lot in common.