Haskell project structure in NixOS

Posted in category nixos on 2017-10-15
Tags: haskell, nixos

Table of Contents

This blog post was inspired by the work of Gabriel Gonzalez at haskell-nix github repository which has a lot of practical information on how to structure a Haskell project in NixOS. Here I will show my own view on a typical Haskell project structure in NixOS taking into account Gabriel’s work.

I would encourage everyone interested to fork haskell-nix and go through Gabriel’s tutorial. That helped me understand some of the issues I was not aware before.

Requirements

  1. default.nix must not change other than with $ cabal2nix . > default.nix
  2. shell.nix must be able to bring developer environment with Hoogle server that will serve documentation for all dependencies used in Cabal file
  3. release.nix must declare at least two packages - one with dynamically linked dependencies, and another - with statically linked dependencies
  4. release.nix must produce output expected by Hydra build system
  5. shell.nix as well as release.nix must use pinned nixpkgs to ensure reproducibility of builds
  6. shell.nix as well as release.nix must be open for choosing version of ghc compiler

Generate default.nix

This is mostly straightforward:

$ cabal2nix . > default.nix

Pinning nixpkgs

To ensure reproducible builds it is important to fixate on a specific version of nixpkgs. Let’s now generate nixpkgs.json that will be used later in shell.nix and release.nix by issuing the following command:

$ nix-prefetch-git --no-deepClone --quiet \
    https://github.com/NixOS/nixpkgs.git \
    f162d54b7602a620f288309bc5d9c832069140b4 > nixpkgs.json
$ cat nixpkgs.json

This should produce the following output:

{
  "url": "https://github.com/NixOS/nixpkgs.git",
  "rev": "f162d54b7602a620f288309bc5d9c832069140b4",
  "date": "2017-11-08T17:11:10+01:00",
  "sha256": "0mnazqaarpnpyxjxk0dlqcp416yyig3mssz6pkddmhs4nmlzw686",
  "fetchSubmodules": true
}

Building shell.nix

It is possible to generate shell.nix with $ cabal2nix --shell . > shell.nix, but it is not recommended as we are going to make sure we meet the requirement of running a Hoogle server locally with all the used dependencies.

{ compiler ? "ghc821"
, withHoogle ? true
}:

let
  bootstrap = import <nixpkgs> {};
  nixpkgs = builtins.fromJSON (builtins.readFile ./nixpkgs.json);
  src = bootstrap.fetchFromGitHub {
    owner = "NixOS";
    repo  = "nixpkgs";
    inherit (nixpkgs) rev sha256;
  };
  pkgs = import src {};
  f = import ./default.nix;
  packageSet = pkgs.haskell.packages.${compiler};
  hspkgs = (
    if withHoogle then
      packageSet.override {
        overrides = (self: super: {
          ghc = super.ghc // { withPackages = super.ghc.withHoogle; };
          ghcWithPackages = self.ghc.withPackages;
        });
      }
      else packageSet
  );
  drv = hspkgs.callPackage f {};
in
  if pkgs.lib.inNixShell then drv.env else drv

Then in order to start your local Hoogle server:

$ nix-shell
shell$ hoogle server --port=8080 --local --haskell

Then navigate to 127.0.0.1:8080 in order to test if documentation server is running.

Building release.nix

release.nix also needs to use pinned version of nixpkgs. It also declares two haskell packages - one package-name and another package-name-static - dynamically and statically linked versions respectively. It also produces a set of derivations as a result which is what Hydra expects.

{ compiler ? "ghc821" }:

let
  bootstrap = import <nixpkgs> {};
  nixpkgs = builtins.fromJSON (builtins.readFile ./nixpkgs.json);
  src = bootstrap.fetchFromGitHub {
    owner = "NixOS";
    repo  = "nixpkgs";
    inherit (nixpkgs) rev sha256;
  };
  config = {
    packageOverrides = pkgs: rec {
      haskell = pkgs.haskell // {
        packages = pkgs.haskell.packages // {
          "${compiler}" = pkgs.haskell.packages."${compiler}".override {
            overrides = self: super: rec {
              package-name = self.callPackage ./default.nix {};
              package-name-static =
                pkgs.haskell.lib.overrideCabal
                  (self.callPackage ./default.nix {})
                  (oldDerivation: { enableSharedExecutables = false; });
            };
          };
        };
      };
    };
  };
  pkgs = import src { inherit config; };
in
{ package-name = pkgs.haskell.packages.${compiler}.package-name;
  package-name-static = pkgs.haskell.packages.${compiler}.package-name-static;
}

Make sure to rename package-name with your project name.

Then building the project locally ends up being as simple as:

$ nix-build release.nix

And installing dynamically linked package with $ nix-env -i ./result. Or statically linked package with $ nix-env -i ./result-2.