A problem I sometimes have is dealing with a bunch of independently maintained but very interdependent libraries, for which I would like proper IDE functionality cross-project. Another problem I sometimes have is that I want to make a contribution to some library but setting up an IDE for it is a pain in the neck.

YOLO method (putting it all in a workspace with your app and not telling Nix)

Often it is easiest to partially bypass Nix while doing development on dependencies. This means that Nix provides the compiler but the dependency you're working on and everything below it is then owned by Cabal.

This is achieved by adding the dependencies into packages: inside cabal.project or cabal.project.local. Then they are part of the workspace, and cabal can do partial recompilation on them as desired.

For instance, if your application depends on persistent and esqueleto and you want to hack on persistent, you would have to do the following:

packages:
    esqueleto
    persistent/persistent
    persistent/persistent-*

Sometimes, there will be an error that says that some files are missing. This is because Cabal decided to try to rebuild the Nix version of the package in question, and could not find source code. The solution to this is to clone the package with such an error and add it to cabal.project as before.

You can set up HLS by running gen-hie (from the implicit-hie package) and then put the results into hie.yaml.

Sometimes HLS won't have loaded up the dependency yet, for some reason and might have some weird import errors. This is solved by opening a file from that dependency.

This is kind of a hack, but this method works really really well for large codebases as it preserves the ability to do incremental builds on changes of dependencies, while also mostly using dependencies from Nix caches.

Standalone workspace

It's possible to create a workspace to work on two or more libraries. This leverages Nix relatively nicely: pulls in dependencies as expected, and so on.

Let's do this to Persistent and Esqueleto, two database libraries. As I later learned, this was rather annoying due to having a large number of packages.

First, make a directory to do this in, and clone the dependencies into this directory. It's important this not be a git repo, because Nix gets confused with nested git repos, and it would be silly to have to set up submodules to do a little hacking (apparently they don't work at all anyway).

$ mkdir deps && cd deps
$ gh repo clone yesodweb/persistent
$ gh repo clone bitemyapp/esqueleto

Then initialize the flake with just a flake.nix with nix flake init, with a flake template:

$ nix flake init -t github:lf-/flake-templates#haskell.flakeNix

Make a cabal.project so cabal considers it a multi package project properly:

packages:
    esqueleto
    persistent/persistent
    persistent/persistent-*

Edit the flake.nix to add both of the packages in the workspace to the overlay and to the packages of the devShell:

{
  # ...
  outputs = { self, nixpkgs, flake-utils }:
    let
      # ...
      out = system:
        let
          # ...
        in
          {
            # ...
            devShells.default =
              let
                haskellPackages = pkgs.haskell.packages.${ghcVer};
              in
                haskellPackages.shellFor {
                  packages = p: with pkgs.haskell.packages.${ghcVer}; [
                    persistent
                    esqueleto
                    persistent-qq
                    persistent-template
                    persistent-mysql
                    persistent-test
                    persistent-postgresql
                    persistent-sqlite
                  ];
                  withHoogle = true;
                  buildInputs = with haskellPackages; [
                    haskell-language-server
                    fourmolu
                    ghcid
                    cabal-install
                  ];
                };
          };
    in
      flake-utils.lib.eachDefaultSystem out // {
        # this stuff is *not* per-system
        overlays = {
          default = makeHaskellOverlay (
            prev: hfinal: hprev:
              let
                hlib = prev.haskell.lib;
                # this uses overrideSrc because of funniness with the deps of
                # persistent-sqlite
                makeLocal = n: hlib.overrideSrc hprev.${n} { src = ./persistent + "/${n}"; };
              in
                {
                  persistent = makeLocal "persistent";
                  persistent-test = makeLocal "persistent-test";
                  persistent-sqlite = makeLocal "persistent-sqlite";
                  persistent-postgresql = makeLocal "persistent-postgresql";
                  persistent-mysql = makeLocal "persistent-mysql";
                  persistent-qq = makeLocal "persistent-qq";
                  persistent-template = makeLocal "persistent-template";
                  esqueleto = hprev.callCabal2nix "esqueleto" ./esqueleto/esqueleto.cabal {};
                }
          );
        };
      };
}