Nixifying a Haskell project using nixpkgs
This tutorial enables you to write a flake using nothing but nixpkgs to nixify an existing Haskell project. The tutorial serves a pedagogic purpose; in the real-world scenario, we recommend that you use haskell-flake.
nixpkgs provides two important functions for developing Haskell projects that we'll extensively use here. They are callCabal2nix
and shellFor
, and are described below.
callCabal2nix
callCabal2nix
produces a derivation for building a Haskell package from source. This source can be any path, including a local directory (eg.: ./.
) or a flake input. We'll use callCabal2nix
to build a package from source during overriding the Haskell package set using overlays (see below).
Package sets
nixpkgs also provides a Haskell package set (built, in part, from Stackage but also Hackage) for each GHC compiler version. The default compiler's package set is provided in pkgs.haskellPackages
. In the repl session below, we locate and build the aeson
package:
❯ nix repl github:nixos/nixpkgs/nixpkgs-unstable
nix-repl> pkgs = legacyPackages.${builtins.currentSystem}
nix-repl> pkgs.haskellPackages.aeson
«derivation /nix/store/sjaqjjnizd7ybirh94ixs51x4n17m97h-aeson-2.0.3.0.drv»
nix-repl> :b pkgs.haskellPackages.aeson
This derivation produced the following outputs:
doc -> /nix/store/xjvm45wxqasnd5p2kk9ngcc0jbjhx1pf-aeson-2.0.3.0-doc
out -> /nix/store/1dc6b11k93a6j9im50m7qj5aaa5p01wh-aeson-2.0.3.0
Overlays
Using the overlay system, you can extend this package set, to either add new packages or override existing ones. The package set exposes a function called extend
for this purpose. In the repl session below, we extend the default Haskell package set to override the shower
package to be built from the Git repo instead:
nix-repl> :b pkgs.haskellPackages.shower
This derivation produced the following outputs:
doc -> /nix/store/crzcx007h9j0p7qj35kym2rarkrjp9j1-shower-0.2.0.3-doc
out -> /nix/store/zga3nhqcifrvd58yx1l9aj4raxhcj2mr-shower-0.2.0.3
nix-repl> myHaskellPackages = pkgs.haskellPackages.extend
(self: super: {
shower = self.callCabal2nix "shower"
(pkgs.fetchgit {
url = "https://github.com/monadfix/shower.git";
rev = "2d71ea1";
sha256 = "sha256-vEck97PptccrMX47uFGjoBVSe4sQqNEsclZOYfEMTns=";
}) {};
})
nix-repl> :b myHaskellPackages.shower
This derivation produced the following outputs:
doc -> /nix/store/vkpfbnnzyywcpfj83pxnj3n8dfz4j4iy-shower-0.2.0.3-doc
out -> /nix/store/55cgwfmayn84ynknhg74bj424q8fz5rl-shower-0.2.0.3
Notice how we used callCabal2nix
to build a new Haskell package from the source (located in the specified Git repository).
Development shell
A Haskell development environment can be provided in one of the two ways. The native way will use the (language-independent) mkShell
function (Generic shell). However to get full IDE support, it is best to use the (haskell-specific) shellFor
function (Haskell shell).
Haskell shell
Suppose we have a Haskell project called "foo" with foo.cabal
. You would create the development shell for this project as follows:
devShells.default = pkgs.haskellPackages.shellFor {
packages = p: [
p.foo
];
buildInputs = with pkgs.haskellPackages; [
ghcid
cabal-install
haskell-language-server
];
}
The packages
argument to shellFor
simply indicates that the given packages are available locally in the flake root, and that cabal
should build them from the local source (rather than using the Nix store derivation for example). The buildInputs
argument is similar to that of mkShell
-- it allows you to specify the packages you want to be made available in the development shell.
From inside of nix develop
shell, launch your pre-configured text editor (for example, VSCode with the Haskell extension installed). You should have full IDE support.
Example
The flake for haskell-multi-nix is presented below. This project has two Haskell packages "foo" and "bar".
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
};
outputs = { self, nixpkgs, ... }:
let
# TODO: Change this to your current system, or use flake-utils/flake-parts.
system = "aarch64-darwin";
pkgs = nixpkgs.legacyPackages.${system};
overlay = self: super: {
# Local packages in the repository
foo = self.callCabal2nix "foo" ./foo { };
bar = self.callCabal2nix "bar" ./bar { };
# TODO: Put any library dependency overrides here
};
# Extend the `pkgs.haskellPackages` attrset using an overlay.
#
# Note that we can also extend the package set using more than one
# overlay. To do that we can either chain the `extend` calls or use
# the `composeExtensions` (or `composeManyExtensions`) function to
# merge the overlays.
haskellPackages' = pkgs.haskellPackages.extend overlay;
in
{
packages.${system} = {
inherit (haskellPackages') foo bar;
default = haskellPackages'.bar;
};
# This is how we provide a multi-package dev shell in Haskell.
# By using the `shellFor` function.
devShells.${system}.default = haskellPackages'.shellFor {
packages = p: [
p.foo
p.bar
];
buildInputs = with haskellPackages'; [
ghcid
cabal-install
haskell-language-server
];
};
};
}
You can confirm that the package builds by running either nix build .#foo
or nix build .#bar
, as well as that IDE support is configured correctly by running nix develop -c haskell-language-server
.
A variation of this flake supporting multiple systems (via use of flake-parts) can be found here.