When Haskell Dependencies Fail

Dependency hell has been plaguing me quite a bit recently. When trying to add Haskell packages into my shells I am getting failures pretty frequently, and often it’s on really simple dependencies (containers, semigroups, etc) if they have requested a specific version.
Hackage’s build reports used to be a pretty good indication of which GHC version to use, but I’m finding this to be less reliable.
Sometimes I can get things to magically come together by randomly trying a few different GHC version; but this is far from ideal.
Sometimes updating my channels can help (but sometimes breaks others).
When things REALLY don’t want to build I can resort to cloning the repo, at which point things will build fine. Buuut having submodule clones of random repos floating around my projects is a pain in the ass to work with.
Cabal2Nix leads to the same issues sadly.

Any suggestions on resolving this kind of thing?
Am I alone here?
What do y’all do when Haskell packs blow up on you?

Here is the shell I usually spin up for most projects, maybe somebody will notice a wrong turn :

{ nixpkgs ? import <nixpkgs> {}
, compiler ? "ghc863"
}:
# ghc862, ghc861, ghcHEAD, 844, 822
let
  inherit (nixpkgs) pkgs;

  hp = with pkgs.haskell.packages.${compiler}; [
    base network hindent hlint ipprint hscolour fast-tags
    HTTP aeson happy alex split
    # Haskell Packages go here.
  ];

  ghc = pkgs.haskell.packages.${compiler}.ghcWithPackages (ps: with ps; hp);

in
  pkgs.stdenv.mkDerivation {
    name = "my-haskell-environment";
    buildInputs = [ ghc ] ++ hp ++ (with pkgs; [
      gnumake
      # Other build Dependencies go here.
    ]);
  shellHook = ''
    eval $(egrep ^export ${ghc}/bin/ghc)
    ghc --version
  '';
}

It’s generally best to use the default GHC version in nixpkgs, not a version of your choice. This is because the haskell package set is roughly based on Stackage, which requires specific GHC versions. So pkgs.haskellPackages is generally better than pkgs.haskell.packages.ghcXYZ unless you know you need the new version and are willing to wade through dependency bounds issues.

That said, many of the issues that crop up when using other GHC versions can be solved by extending the haskell package set and modifying packages or replacing them outright. Here’s how my typical haskell project is set up:

{ pkgs ? import <nixpkgs> {} }:

# packageSourceOverrides is a convenience function
# making it easier to update dependencies to new Hackage versions
# or custom source tree.
(haskellPackages.extend (haskell.lib.packageSourceOverrides {
  # Replace the pinned lens version with 4.17 from Hackage.
  lens = "4.17";

  # Add / replace foo with a git checkout.
  # cabal2nix will be called automatically for you
  foo = builtins.fetchGit {
    url = https://github.com/Foo/foo.git;
    rev = "...";
  };

  # builtins.fetchGit ./. is an easy way to get a clean tree
  # from your project. It will even included uncommitted changes,
  # but only if they're in tracked files so build dirs like ./dist
  # aren't sucked into the nix store unnecessarily.
  my-local-pkg = builtins.fetchGit ./.;
})).extend (self: super: {
  # We can extend again to add manual overrides
  # that can't be done with packageSourceOverrides

  # doJailbreak is a very common one. This line replaces bar with
  # a modified version that strips the package of its dependency
  # bounds, allowing us to use it with newer packages than it
  # anticipated.
  bar = haskell.lib.doJailbreak super.bar;
})

To build my package, I can do nix-build -A my-local-pkg, and to enter a nix-shell for incremental building, I can either do nix-shell -A my-local-pkg.env or add this shell.nix file:

(import ./. {}).my-local-pkg.env

If you’re working on multiple local cabal packages, you can get incrementalism with a cabal.project file and by replacing env with shellFor

# cabal.project
packages:
  ./foo
  ./bar
# shell.nix
(import ./. {}).shellFor (p: [p.my-local-pkg1 p.my-local-pkg2])

Now nix-shell --run 'cabal new-build all' can build both packages incrementally.

5 Likes

If this is Haskell-specific, then I recommend looking into a tool like vernix. It lets you pin specific versions of Haskell libraries which will get grabbed straight from Hackage, or easily use your own forks if you are waiting for a package maintainer to merge a patch by specifying a repo location. You can do this in pure Nix, but it gets verbose quickly if you have lots of packages and projects.

1 Like