Fika: The Swedish tradition of periodic coffee breaks to recharge and socialize.
Functional Fika: Tidbits of knowledge passed from one Haskeller to another.
There’s a great divide in the Haskell community between existing dependency management systems. The two main contenders are Cabal and Stack. Stack was born to solve tooling and build reproduction issues within Cabal, but over time Cabal has evolved. The two build systems now compete with each other.
This conflict is ongoing, but as the dust settles a third option has emerged. Nix allows you to create a consistent environment for Haskell projects using Cabal, or Stack. Nix acts as a more general version of a good build system, extending the capacity for reproducible builds by adding the ability to create reproducible development environments.
The end result of this configuration is a fully reproducible shell with Hoogle, Ghcid, HLint, Stylish-Haskell, and HIE as a Language Server. Inside this shell are a well-described set of packages which are available to your Haskell build system. I’ve chosen to use Cabal for this guide, but Stack has good integration with Nix as well.
In my opinion the primary reason to use Nix is its ability to facilitate painless collaboration with other Haskellers. Creating a consistent development experience increases the community’s ability to work together and build amazing projects!
TLDR: Here’s a TAR file for fully reproducible Cabal + Nix builds with HIE and Hoogle. Here are steps to use the archive with Cabal.
Skeleton Nix Builds for Haskell
In this guide, I’ve assumed that you have Nix installed. If you have issues installing Nix, rerun their install script (after thoroughly examining it for malicious behavior):
curl https://nixos.org/nix/install | sh
Using Nix is worthwhile for the assured reproduction of builds, but it comes with a complexity cost. The documentation is very general, and, although good resources such as Nix Pills and Gabriel’s Haskell and Nix Tutorial exist, I have yet to find a resource which gives you the bare minimum required to get a Cabal and Nix project up and running with local Hoogle and LSP editor integration.
Nix Directory Structure
Attached to this guide is a TAR file containing everything required to handle Nix builds for any Haskell project when using Cabal. Here’s the directory structure:
.
├── nix
│ ├── haskell
│ │ ├── default.nix
│ │ └── overlay.nix
│ ├── nixpkgs.nix
│ ├── shell.nix
│ └── sources.nix
└── shell.nix
2 directories, 6 files
When running nix-shell
, Nix will attempt to parse the Nix configuration stored in shell.nix
within the same directory. The contents of our top level shell.nix
file look like this:
This file only exists so that we can separate all Nix related files into their own directory. It will immediately pass control to the nix/shell.nix
file.
nix/shell.nix
This file takes a pinned set of packages from nixpkgs.nix
, looks for any Haskell specific packages in the haskell
directory, and builds a shell that contains all these requirements:
let
nixpkgs = import ./nixpkgs.nix {};
inherit (nixpkgs) pkgs;
haskell = import ./haskell { inherit pkgs; };
in
pkgs.mkShell {
buildInputs = with pkgs; [
haskell.cabal-install
haskell.ghc
haskell.ghcid
haskell.ghcide
haskell.hlint
haskell.stylish-haskell
pkgs.zlib
];
# Use the libraries from the derivation created by ghcWithHoogle.
NIX_GHC_LIBDIR = "${haskell.ghc}/lib/ghc-${haskell.ghc.version}";
LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:/usr/lib/";
}
The important line here is pkgs.mkShell
, which is responsible for building our reproducible environment. You can see inside the buildInputs
line that we are specifying that our environment should use the haskell.ghc
that we provided from the haskell/default.nix
file. This lets us fully control the packages which eventually will be available to Cabal.
It’s also worth noting that Nix provides system binaries in addition to Haskell packages. When you choose a nixpkgs
set, you get a set of Haskell packages and system binaries that are guaranteed to work together. As a result, we can include things like haskell.hlint
, which will build our environment with the linting tool.
nix/nixpkgs.nix
args@{ ... }:
let
sources = import ./sources.nix;
nixpkgs = import sources.nixpkgs (args // {
overlays = [
(import ./haskell/overlay.nix)
];
});
in
nixpkgs
This file is responsible for building the set of nixpkgs
that we would like available. It looks for a pinned set of nixpkgs
which we’ve described in sources.nix
. This file will provide a base set of packages which the Nix team has built that are guaranteed to work together.
Next we create our set of overlays
. This is Nix jargon for a set of packages which override the default packages available in the base nixpkgs
set. When using Nix and Haskell, it’s very common that you need to use a specific version of a Haskell package which is not available in the base pinned nixpkg
set. The overlays functionality lets you solve this issue by manually specifying packages and their corresponding versions.
We’ll dive more into the concept of overlays shortly, but first let’s examine how we pin a nixpkgs
set.
nix/sources.nix
If you don’t pin a specific version of your nixpkgs
, you’ll get the most up to date version within the default channel. The packages within these channels change over time as the Nix team updates the packages that they’ve verified to work together.
To ensure complete build reproduction, we want to freeze the set of packages so that they don’t change and break our build in the future. We can accomplish this using the following Nix code:
{
nixpkgs = builtins.fetchGit {
url = "https://github.com/NixOS/nixpkgs.git";
rev = "05626cc86b8a8bbadae7753d2e33661400ff67de";
};
}
Here we specify our base nixpkgs
using the Nix function fetchGit
. This function takes a set with two keys, the url
of the Git package, and the rev
that we’d like to freeze. A rev
stands for a Git revision, essentially a synonym for a specific commit within a branch.
To get the rev
for an arbitrary repository that you’ve locally cloned, you can use the following command:
Then you just have to replace the rev
parameter within the Nix configuration file.
Haskell Overlays Revisited
Running into a package in the pinned Nix set that’s too old is a common issue during development. To solve it you can manually specify the package in the nix/haskell/overlay.nix
file.
nixpkgsSelf: nixpkgsSuper:
let
inherit (nixpkgsSelf) pkgs;
ghcVersion = "ghc865";
hsPkgs = nixpkgsSuper.haskell.packages.${ghcVersion}.override {
overrides = self: super: {
ghcide = pkgs.haskell.lib.dontCheck (self.callCabal2nix
"ghcide"
(builtins.fetchGit {
url = "https://github.com/digital-asset/ghcide.git";
rev = "0838dcbbd139e87b0f84165261982c82ca94fd08";
})
{});
hie-bios = pkgs.haskell.lib.dontCheck (self.callHackageDirect {
pkg = "hie-bios";
ver = "0.3.2";
sha256 = "08b3z2k5il72ccj2h0c10flsmz4akjs6ak9j167i8cah34ymygk6";
} {});
haskell-lsp = pkgs.haskell.lib.dontCheck (self.callHackageDirect {
pkg = "haskell-lsp";
ver = "0.18.0.0";
sha256 = "0pd7kxfp2limalksqb49ykg41vlb1a8ihg1bsqsnj1ygcxjikziz";
} {});
haskell-lsp-types = pkgs.haskell.lib.dontCheck (self.callHackageDirect {
pkg = "haskell-lsp-types";
ver = "0.18.0.0";
sha256 = "1s3q3d280qyr2yn15zb25kv6f5xcizj3vl0ycb4xhl00kxrgvd5f";
} {});
shake = pkgs.haskell.lib.dontCheck (self.callHackage "shake" "0.18.3" {});
};
};
in
{
haskell = nixpkgsSuper.haskell // {
inherit ghcVersion;
packages = nixpkgsSuper.haskell.packages // {
"${ghcVersion}" = hsPkgs;
};
};
}
In this file, I’ve included a number of packages using the callHackageDirect
function, including the relevant packages to build HIE for Language Server integration with the editor of your choice. This function will look through Hackage to find the package matching your specified version and validate that the hash provided matches for integrity protection.
In most cases, this is what you’ll want to use when the package of your choice exists on Hackage. To get the relevant sha256
hash, you can use the following command:
If this command doesn’t exist on your system after installing Nix, you can use nix-env -i nix-prefetch-git
to retrieve it.
Fetching from Git Directly
Some packages never make their way onto Hackage, and exist only in a Git repository. To include these packages we can use the fetchGit
function with the repository URL and rev
. This is the same technique we used in the nix/sources.nix
file to pin the base nixpkgs
version.
Fetching Local Packages
This one stumped me for a long time. How do you include local Cabal packages in your ongoing project?
The answer turns out to be the lovely callCabal2nix
function. You can use it to take a local Cabal package and build a Nix file which specifies how to import it.
If cabal2nix
is not found, then install it with nix-env -i cabal2nix
. The resulting Nix file can be imported in the overlay.nix
file using the callPackage
function in the same block as the other overlay packages.
Including Overlays and Custom Tools
Once we have defined all the overlays for your project and have a complete nixpkgs
set, we need to tell Nix to include those overlays. We also need Nix to look at your Cabal project configuration file so it can determine the subset of Haskell packages your project needs from the set of all packages available in your nixpkgs
set. This is accomplished in the nix/haskell/default.nix
file:
{pkgs ? import ../nixpkgs {} }:
let
inherit (pkgs.haskell) ghcVersion;
hsPkgs = pkgs.haskell.packages.${ghcVersion};
pkgDrv = hsPkgs.callCabal2nix "CHANGEME" ../.. {};
haskellDeps = pkgDrv.getBuildInputs.haskellBuildInputs;
ghc = hsPkgs.ghcWithHoogle (_: haskellDeps);
in
{
inherit ghc;
inherit (hsPkgs) cabal-install ghcide hlint ghcid stylish-haskell;
}
This file includes the CHANGEME
variable within the callCabal2Nix
function that we discussed earlier. By changing CHANGEME
to the name of your new Cabal package, you inform Nix that you want it to use Cabal to find the set of packages required for your new project. It will automatically build a new Nix configuration file from the dependencies you specified in your Cabal project configuration.
You’ll also notice that in the final code block we use the inherit
function to return our modified version of ghc
that includes all our dependencies and a local version of Hoogle.
Finally, we use the inherit (hsPkgs)
syntax to extend our current list of hsPkgs
with our custom development tools. This line is what makes custom tools like ghcid or hlint
available to the mkShell
command in the nix/shell.nix
file.
Workflow
When you want to start a new Haskell + Nix project, follow these steps:
- Make a new directory for the project
- Copy the contents of the [Nix skeleton] into the project directory
- Run
cabal init
and define your package name - Replace the
CHANGEME
variable innix/haskell/default.nix
online 8
with the name of your cabal package. - Run
nix-shell
- Run cabal commands as usual!
The last hiccup is that you’ll need to start the editor of your choice from within the nix-shell if you want HIE and editor integration to be available.
Afterword
Nix is a beast of a program, and although frustrating at times, when it works, it feels magical. I’m still a beginner myself, and there is plenty more for me to learn. If you’re interested in more advanced configurations, get in touch with the community and check out the resources I’ve included below. I hope you spent some quality time with a hot cup of coffee and enjoyed this morning’s Fika!
Shout out to Riccardo for early feedback on this article. If you liked this content, check him out, he has excellent resources for Haskellers.