blog/content/posts/nix-hls-for-deps.md
2022-10-01 22:15:14 -07:00

162 lines
5.6 KiB
Markdown

+++
date = "2022-07-21"
draft = false
path = "/blog/nix-hls-for-deps"
tags = ["haskell", "nix"]
title = "Setting up dev environments with Haskell Language Server and Nix"
+++
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) {#yolo-method}
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:
* Clone persistent and esqueleto into subdirectories under the app (no need to
make them submodules)
* Put something like the following into `cabal.project` or
`cabal.project.local`:
```cabalproject
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][submodules-oops]).
[submodules-oops]: https://github.com/NixOS/nix/pull/5284
```sh
$ 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
```
[flake template]: https://github.com/lf-/flake-templates/tree/main/haskell
Make a `cabal.project` so cabal considers it a multi package project properly:
```cabal
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`:
```nix
{
# ...
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 {};
}
);
};
};
}
```