Using Nix for Haskell projects

Nix is the best way to develop and build Haskell projects.

Creating a new project

Initialise a new Cabal project by running this command:

$ nix shell nixpkgs\#{cabal-install,ghc} --command cabal init --interactive

Then create the following Nix flake:

{
  inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

  outputs = inputs:
    with builtins;
    let
      pname = "hello"; # CHANGE THIS TO YOUR CABAL PROJECT NAME

      inherit (inputs.nixpkgs) lib;
      foreach = xs: f: with lib; foldr recursiveUpdate { } (
        if isList xs then map f xs
        else if isAttrs xs then mapAttrsToList f xs
        else throw "foreach: expected list or attrset but got ${typeOf xs}"
      );
      sourceFilter = root: with lib.fileset; toSource {
        inherit root;
        fileset = fileFilter (file: any file.hasExt [ "cabal" "hs" "md" ]) root;
      };
      ghcsFor = pkgs: with lib; foldlAttrs
        (acc: name: hp:
          let
            version = getVersion hp.ghc;
            majorMinor = versions.majorMinor version;
            ghcName = "ghc${replaceStrings ["."] [""] majorMinor}";
          in
          if hp ? ghc && ! acc ? ${ghcName} && versionAtLeast version "9.2" && versionOlder version "9.11"
          then acc // { ${ghcName} = hp; }
          else acc
        )
        { }
        pkgs.haskell.packages;
      hpsFor = pkgs: { default = pkgs.haskellPackages; } // ghcsFor pkgs;
      overlay = lib.composeManyExtensions [
        (final: prev: {
          haskell = prev.haskell // {
            packageOverrides = lib.composeManyExtensions [
              prev.haskell.packageOverrides
              (hfinal: hprev: with prev.haskell.lib.compose; {
                ${pname} = hfinal.callCabal2nix pname (sourceFilter ./.) { };
              })
            ];
          };
        })
      ];
    in
    foreach inputs.nixpkgs.legacyPackages
      (system: pkgs':
        let
          pkgs = pkgs'.extend overlay;
          hps = hpsFor pkgs;
          bins = pkgs.buildEnv {
            name = "${pname}-bins";
            paths = [ hps.default.${pname} ];
            pathsToLink = [ "/bin" ];
          };
          libs = pkgs.buildEnv {
            name = "${pname}-libs";
            paths = map (hp: hp.${pname}) (attrValues hps);
            pathsToLink = [ "/lib" ];
          };
          docs = pkgs.haskell.lib.documentationTarball hps.default.${pname};
          sdist = pkgs.haskell.lib.sdistTarball hps.default.${pname};
          docsAndSdist = pkgs.linkFarm "${pname}-docsAndSdist" { inherit docs sdist; };
        in
        {
          formatter.${system} = pkgs.nixpkgs-fmt;
          legacyPackages.${system} = pkgs;
          packages.${system}.default = pkgs.symlinkJoin {
            name = "${pname}-all";
            paths = [ bins libs docsAndSdist ];
            inherit (hps.default.syntax) meta;
          };
          devShells.${system} =
            foreach hps (ghcName: hp: {
              ${ghcName} = hp.shellFor {
                packages = ps: [ hp.${pname} ];
                nativeBuildInputs = [
                  pkgs'.haskellPackages.cabal-install
                  hp.fourmolu
                  hp.haskell-language-server
                ];
              };
            });
        }
      ) // {
      overlays.default = overlay;
    };
}

Now you’re probably thinking:

Whoa, that’s a big flake!

It is, but it has a number of nice features:

  • support for all Nixpkgs-provided systems (as defined in nixpkgs.legacyPackages)
  • support for all Nixpkgs-provided GHC versions (as defined in nixpkgs.legacyPackages.${system}.haskell.packages)
  • a devshell that actually works out of the box
  • an overlay for easy flake composability

Basic operations

  • add Haskell modules and dependencies to the generated Cabal file
    • if using Direnv, make sure to direnv reload after changing the Cabal file to update the devshell
  • build and run the project locally with cabal build and cabal run
  • build the project for all GHC versions with nix build
  • build for a specific GHC version with nix build .#haskell.packages.ghc98.hello