I’ve been using Nix to develop and deploy Haskell for quite some time now and I’d like to share some of the gems I have found/figured out here. For this post I want to focus on just project development. I’m no expert and I’m sure community members will have additions/improvements on what I post. Hopefully we can distill this post into a new wiki entry afterwards.
Pinning Nixpkgs
There are two easy ways to pick a specific commit of sources, niv or nix-prefetch-git
(install with nix-env -i nix-prefetch-git
. For this guide I use the later since you will end up with a better understanding of what is involved with fetching sources. However, niv
is the better choice when you want to abstract away the details.
nix-prefetch-git
will give you back some JSON that you can import and fill a fetchFromGitHub
command with. I recommend creating a nix
directory in the root of your project to put this and other nix files in (not shell.nix
or default.nix
).
Here is an example of how to get this for unstable:
nix-prefetch-git https://github.com/nixos/nixpkgs-channels.git refs/heads/nixpkgs-unstable > nix/nixpkgs-unstable.json
Here is an example of how to get this for nixos-19.09:
nix-prefetch-git https://github.com/nixos/nixpkgs-channels.git refs/heads/nixos-19.09 > nix/nixos-19-09.json
If want to import the nixos-19.09 package set then I would create nix/nixos-19-09.nix:
let
hostNix = import <nixpkgs> {};
nixpkgsPin = hostNix.pkgs.lib.importJSON ./nixos-19-09.json;
pinnedPkgs = hostNix.pkgs.fetchFromGitHub {
owner = "NixOS";
repo = "nixpkgs-channels";
inherit (nixpkgsPin) rev sha256;
};
in
pinnedPkgs
Now you can import ./nix/nixos-19-09.nix
in your default.nix
or shell.nix
which is now pinned to that specific commit.
hoogle and callCabal2nix
To work effectively with Haskell you need a local hoogle instance with the dependencies you have in your cabal or package.yaml file and the versions of those dependencies you are compiling with. It is important to note you can create either a shell.nix
or a default.nix
in the root of your project and both can be loaded with nix-shell
. The important difference is you may want to use default.nix
for deployment since nix-build
defaults to using default.nix
. Here is an example of how do to this I learned from the state machine testing course:
{ nixpkgs ? import ./nix/nixos-19-09.nix }:
let
overlay = self: super: {
myHaskellPackages =
super.haskell.packages.ghc865.override (old: {
overrides = self.lib.composeExtensions (old.overrides or (_: _: {}))
(hself: hsuper: {
ghc = hsuper.ghc // { withPackages = hsuper.ghc.withHoogle; };
ghcWithPackages = hself.ghc.withPackages;
});
});
};
pkgs = import nixpkgs {
overlays = [overlay];
};
drv = pkgs.myHaskellPackages.callCabal2nix "name-of-project" ./name-of-project.cabal {};
drvWithTools = drv.env.overrideAttrs (
old: with pkgs.myHaskellPackages; {
nativeBuildInputs = old.nativeBuildInputs ++ [
ghcid
# Add other development tools like ormolu here
];
shellHook = ''
source .config/secrets '';
}
);
in
drvWithTools
Lets break down what is going on here.
{ nixpkgs ? import ./nix/nixpkgs.nix }:
The first line is the arguments to this derivation that I called nixpkgs
. It has a default value, the import after the ?
, so you can just call nix-shell
to load up the file. That import is pulling in the pinned nix packages we discussed in the previous section.
overlay = self: super: {
myHaskellPackages =
super.haskell.packages.ghc865.override (old: {
overrides = self.lib.composeExtensions (old.overrides or (_: _: {}))
(hself: hsuper: {
ghc = hsuper.ghc // { withPackages = hsuper.ghc.withHoogle; };
ghcWithPackages = hself.ghc.withPackages;
});
});
};
This is an overlay, meaning it is a function that takes a self
and super
as arguments. Here is a good guide on how to write overlays. What its doing is setting the attribute myHaskellPackages
to the package set of ghc865
in this case with a few adjustments to the package set. Later on we can add other overrides to the package set here.
pkgs = import nixpkgs {
overlays = [overlay];
};
Here we are importing the pinned nixpkgs with the overlay. Importantly we can import other overlays like ormolu.
drv = pkgs.myHaskellPackages.callCabal2nix "name-of-project" ./name-of-project.cabal {};
This is where we create the derivation for your project. It uses the package set myHaskellPackages
we just modified with the overlay and uses callCabal2nix
to convert our cabal file into a nix derivation without us having to call cabal2nix
ourselves externally.
Note: This also works if you have just a project.yaml, but you have to set the path to ./.
instead of ./name-of-project.cabal
or ./package.yaml
(I wish it would). This can have the unintended concequence of extra rebuilds since Nix is importing the current directory and all its subdirectories as a dependency of your project. In this case (builtins.filterSource (path: type: type != "directory" || baseNameOf path != ".svn") ./.;)
can be used to filter out unwanted files.
drvWithTools = drv.env.overrideAttrs (
old: with pkgs.myHaskellPackages; {
nativeBuildInputs = old.nativeBuildInputs ++ [
ghcid
# Add other development tools like ormolu here
];
shellHook = ''
source .config/secrets
'';
}
);
Lastly we have the environment for the shell. This is made for you by callCabal2nix
and is in the env
attribute of drv
. However, we want to add some extra packages for development, namely ghcid. Plus I often like to use the shellHook
attribute to pull in development secrets (think tokens for development) or other cleanup functions. The overrideAttrs
function input is the old
attributes of the env
. In this case we use it to append our development tools to nativeBuildInputs
, otherwise we wouldn’t get hoogle. The with pkgs.myHaskellPackages;
means we don’t have to type pkgs.myHaskellPackages
before ghcid
or any other tools you want (hlint, apply-refactor, doctest, …).
Adding packages or package overrides
Lets say you realize the version of req
you want to use is not in the package set. One way is to use callHackageDirect
to create a derivation that will use hackage as its source for the version of req
you want. A trick to get the sha256
for the package is to put 52 zeros (the correct length of a sha256) as a placeholder, then invoke nix-shell
and when the build fails copy over the correct sha256
. Keep in mind not to just copy another sha256
from another derivation (like what nix-prefetch-git
generated). See this issue for more information.
This would look like:
overlay = self: super: {
myHaskellPackages =
super.haskell.packages."${compiler}".override (old: {
overrides = self.lib.composeExtensions (old.overrides or (_: _: {}))
(hself: hsuper: {
ghc = hsuper.ghc // { withPackages = hsuper.ghc.withHoogle; };
ghcWithPackages = hself.ghc.withPackages;
## Overridden Package here!
req = hself.callHackageDirect
{ pkg = "req";
ver = "3.1.0";
sha256 = "0000000000000000000000000000000000000000000000000000";
} {};
});
});
};
Now this will fail to build since the sha256
nix will calculate won’t be all zeros, but once you replace it with the one Nix reports back then it will build (Note, this is what Niv handles for you). The result is req
is replaced with verison 3.1.0 in your package set and your local hoogle instance will generate haddocks for 3.1.0. Note, you will have to reload the nix-shell that hoogle is running in as well as restart hoogle (direnv and Iorri can automate part of that process).
Note: You can also use callHackage
which does not need a sha256
, but req
was not in the all-cabal-hashes
so I needed to use callHackageDirect
. Search for callHackageDirect
on this if you want to read more.
dontCheck
and justStaticExecutables
A common issue you will run into by pulling in extra or different packages is that you have to build them yourself. Haskell build times are reasonable (depending on the library), but often the tests are slow. You can fix this by adding dontCheck
in front of the dependency you know works or just want to test out. Note, Distributed Builds can help when you don’t want to skip tests or just want faster build times. Anyways, the line we added above would become:
req = self.haskell.lib.dontCheck
(hself.callHackageDirect
{ pkg = "req";
ver = "3.1.0";
sha256 = "0rwbdmp2g74yq7fa77fywsgm0gaxcbpz9m6g57l5lmralg95cdxh";
} {}
);
Another issue is when you are pulling in a tool, not a library. Lets say you want the latest version of ghcid
because it has --allow-eval
. You would start much the same way, but instead you want to put it in your nativeBuildInputs
:
(pkgs.haskell.lib.justStaticExecutables (pkgs.haskellPackages.callHackage "ghcid" "0.8.2"))
In context this would be:
drvWithTools = drv.env.overrideAttrs (
old: with pkgs.myHaskellPackages; {
nativeBuildInputs = old.nativeBuildInputs ++ [
(pkgs.haskell.lib.justStaticExecutables (pkgs.haskellPackages.callHackage "ghcid" "0.8.2"))
];
shellHook = ''
source .config/secrets '';
}
);
The justStaticExecutables
will just make the executable, not the haddock, etc.
However, when this doesn’t work (version not on hackage, won’t compile, etc.) you can take a more direct approach and callCabal2nix
on the source yourself. For example, when I tried getting that version of ghcid
building the package set was missing extra
. So, here is what you do:
- Get the source (use same
sha256
trick):
ghcidSrc = pkgs.fetchFromGitHub {
owner = "ndmitchell";
repo = "ghcid";
rev = "e3a65cd986805948687d9450717efe00ff01e3b5";
sha256 = "1xhyl7m3yaka552j8ipvmrbfixgb8x0i33k2166rkiv7kij0rhsz";
};
- Add to
nativeBuildInputs
(This didn’t build for me sinceetxra
was missing)
drvWithTools = drv.env.overrideAttrs (
old: with pkgs.myHaskellPackages; {
nativeBuildInputs = old.nativeBuildInputs ++ [
# Added here!
(pkgs.haskell.lib.justStaticExecutables
(pkgs.haskell.packages.ghc865.callCabal2nix "ghcid" ghcidSrc {}))
];
);
- Get the missing library for ghcid
extra = pkgs.haskell.lib.dontCheck
(pkgs.haskellPackages.callHackageDirect
{ pkg = "extra";
ver = "1.6.20";
sha256 = "0fy5bkn8fn3gcpxksffim912q0ifk057x0wmrvxpmjjpfi895jzj";
} {}
);
- Add the missing libary to the
callCabal2nix
call
drvWithTools = drv.env.overrideAttrs (
old: with pkgs.myHaskellPackages; {
nativeBuildInputs = old.nativeBuildInputs ++ [
(pkgs.haskell.lib.justStaticExecutables
# Fixed here!
(pkgs.haskell.packages.ghc865.callCabal2nix "ghcid" ghcidSrc {inherit extra;}))
];
);
All together in a full example
{ nixpkgs ? import ./nix/nixos-19-09.nix }:
let
overlay = self: super: {
myHaskellPackages =
super.haskell.packages.ghc865.override (old: {
overrides = self.lib.composeExtensions (old.overrides or (_: _: {}))
(hself: hsuper: {
ghc = hsuper.ghc // { withPackages = hsuper.ghc.withHoogle; };
ghcWithPackages = hself.ghc.withPackages;
});
});
};
pkgs = import nixpkgs {
overlays = [overlay];
};
drv = pkgs.myHaskellPackages.callCabal2nix "name-of-project" ./name-of-project.cabal {};
ghcidSrc = pkgs.fetchFromGitHub {
owner = "ndmitchell";
repo = "ghcid";
rev = "e3a65cd986805948687d9450717efe00ff01e3b5";
sha256 = "1xhyl7m3yaka552j8ipvmrbfixgb8x0i33k2166rkiv7kij0rhsz";
};
extra = pkgs.haskell.lib.dontCheck
(pkgs.haskellPackages.callHackageDirect
{ pkg = "extra";
ver = "1.6.20";
sha256 = "0fy5bkn8fn3gcpxksffim912q0ifk057x0wmrvxpmjjpfi895jzj";
} {}
);
drvWithTools = drv.env.overrideAttrs (
old: with pkgs.myHaskellPackages; {
nativeBuildInputs = old.nativeBuildInputs ++ [
(pkgs.haskell.lib.justStaticExecutables
(pkgs.haskell.packages.ghc865.callCabal2nix "ghcid" ghcidSrc {inherit extra;}))
];
);
in
drvWithTools
Adding local packages
Sometimes you find a bug in one of your dependencies and you want to try out a quick fix. Now you could fork the repo, make some changes, push them up and use callCabal2nix
to create a derivation for that commit and overide that package just like above. However, that is a lot of work for a change that might not even work. Instead you can just clone the repository you want to modify and use callCabal2nix
to import it directly. The addition to the overlay looks like this:
project = self.haskell.lib.dontCheck (hself.callCabal2nix "project" /full/path/to/project/folder {});
Notes: The dontCheck
is useful when your change breaks the tests, but you want to test it out before refactoring tests. The callCabal2nix
needs the full path to the project folder, not the full path to the cabal file. Also, this will generate new haddocks for the project that you can view in hoogle. If you don’t want the haddocks you can wrap the whole call in self.haskell.lib.dontHaddock (...)
Alternative shellFor
So for we have shown how to create an overlay with your custom haskell package set, tweak that package set to have what you need, use that custom set to create derivation for your project and add some build tools/hooks. There is a shortcut you can take, a function called shellFor that lets you create a shell based on a custom package set and lets you add extra tools/options. You can use this to skip creating the overlay and drvWithTools
I create. For now I’ll leave this tutorial as is since overlays are used thoughout nixpkgs, not just Haskell, while the shellFor
concept is only setup for some languages.