Home

Functional Fika — Nix and Haskell

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:

import nix/shell.nix

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:

git rev-parse --short HEAD

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:

nix-prefetch-url --unpack <LINK TO HACKAGE TAR/ZIP ARCHIVE>

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.

cabal2nix /local/project/directory > project.nix

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 in nix/haskell/default.nix on line 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.

If this guide was helpful to you, or if you have comments about this article, please feel free to reach out. I want to hear what you think! If you want to see more content like this, view the archive, subscribe to my newsletter or support me on Ko-Fi.

Resources: