# Caching for incremental changes (including \`ghc-nix\`)

**URL:** <https://discourse.haskell.org/t/caching-for-incremental-changes-including-ghc-nix/13606>\
**Category:** Learn\
**Created:** [January 27, 2026, 7:55am UTC](https://discourse.haskell.org/t/caching-for-incremental-changes-including-ghc-nix/13606 "2026-01-27T07:55:02Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![clinton](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/clinton/32/4111_2.png) [@clinton](https://discourse.haskell.org/u/clinton)\
**Post date:** [January 27, 2026, 7:55am UTC](https://discourse.haskell.org/t/caching-for-incremental-changes-including-ghc-nix/13606/1 "2026-01-27T07:55:02Z")

</div>

At my previous Haskell role, we had a workflow where our CI/Production builds were using Nix (using `cabal2nix`), but the issue is that Nix rebuilds things on the package level, so if one file in a package changes it must rebuild the entire package. This is too slow to be practical for building changes during development.

So for development, I had the following setup:

1. A `cabal.project` file with quite strict warning and error options (like `-Wall -Werror`)
2. A second `.project` file, say `cabal-dev.project`, which `import:`ed `cabal.project`, but with a looser set of warnings/errors, in particular, turning off warnings/errors for stylistic things, like unused imports, and also turned off optimisation (i.e. `-O0`) to speed up builds.
3. A third `.project` file, which was called `cabal-hls.project`, which imported from `cabal-dev.project`, but had an even looser set of warnings, and did things like turn on [`-fdefer-typed-holes`](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/defer_type_errors.html#ghc-flag-fdefer-typed-holes) and even [`-fdefer-type-errors`](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/defer_type_errors.html#ghc-flag-fdefer-type-errors). I found the issue with the HLS language server with a standard set of build options that minor issues in some modules (like an unused import) would actually break downstream HLS processing on files that imported a file with an issue, so keeping the compile warnings very loose for HLS would allow edited interactivity to continue even if some files had minor compile issues.

So, this set up worked okay, but there’s a few issues:

1. `cabal` caches the last build, but if any of the settings change, that cache is unused and overridden. I actually solved that by setting up scripts to build these various versions of `cabal.project` in different directories, but…
2. If I change my branch/commit, it would reuse any cached build results for unchanged files, but rebuild anything changed. But then if I change back to the previous branch, it’s build results would then need to be rebuilt even though they were built minutes ago. I could perhaps improve this by caching the build results in different directories based on commit id also, but that would mean whenever I created a new commit I’d need to rebuild everything from scratch which is probably worse.
3. `cabal2nix` doesn’t look at `cabal.project` files but instead just looks at individual `.cabal` files. So one has to replicate your project based settings in Nix and keep these in sync with the `cabal.project` settings. Also whilst we tried to ensure the same package versions were being used by pinning [`index-state:`](https://cabal.readthedocs.io/en/3.4/cabal-project.html#cfg-field-index-state) in `cabal.project` to a similar time/date as our `flake.lock`, there could sometimes still be slight mismatches.

I understand [`haskell.nix`](https://input-output-hk.github.io/haskell.nix/) fixes problem 3, by building the Nix derivations directly from the `cabal` build plan, but that still leaves the issues with having reasonably fast builds of small incremental changes.

I quite like how Nix solves issues 1 and 2. There’s no need for scripts to put builds in separate directories/hacks based on git commit id, it just reuses what previous results are reusable, no matter how many times you’ve change build options/git branches. The only problem I see that it does this at the granularity of entire packages, not per file.

It really looks like [`ghc-nix`](https://github.com/matthewbauer/ghc-nix) is the solution I’m looking for. It just solves everything, fast incremental builds, no difference between development and CI build processes etc. But it hasn’t been updated in over three years now, and comes with a big warning _“this project is still in very early days, so it’s not too easy to use… yet.”_.

The other alternative I’ve found is [`Snack`](https://github.com/nmattia/snack) which seems to be a similar offering, but working as a `cabal`/`stack` replacement instead of a `GHC` wrapper. But `Snack` has been unmaintained for six years.

I was just wondering if there has been any recent work in this space? I quite like the benefits that Nix gives particularly regarding reproducibility and transparent caching, but it’s just too slow in a development workflow when the smallest build unit is at the package, not module level. Or should I just dive into `ghc-nix` even though it hasn’t had any updates for a few years? Is anyone using it for current projects on a modern version of GHC?

---

<div class="post-metadata">

**Author:** ![Ericson2314](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/ericson2314/32/1026_2.png) [@Ericson2314](https://discourse.haskell.org/u/Ericson2314)\
**Post date:** [January 27, 2026, 3:56pm UTC](https://discourse.haskell.org/t/caching-for-incremental-changes-including-ghc-nix/13606/2 "2026-01-27T15:56:54Z")

</div>

There has been more recent work in this space! Please check out [GitHub - obsidiansystems/sandstone: Fine-grained Haskell builds with Nix's dynamic derivations](https://github.com/obsidiansystems/sandstone/) (slides: [sandstone/planet-nix-2025.pdf at slides · obsidiansystems/sandstone · GitHub](https://github.com/obsidiansystems/sandstone/blob/slides/planet-nix-2025.pdf) ), a demonstration my coworkers and I made about a year ago.

It too has not seen more work in the past year, but I have been work on the unstable features of Nix that is uses behind the scenes. Progress should be continuing on that front too, until everything that Sandstone needs is quite production ready. At that point, I would like to take Sandstone and integrate it into a an alternative backend for Cabal’s new ``build-type: Hooks`` that works under the same principles — when Sam Derbyshire (no discourse account?) worked on that, he and I had some conversations where I specifically mentioned some things that would help with this eventually happening.

---

<div class="post-metadata">

**Author:** ![enobayram](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/enobayram/32/1815_2.png) [@enobayram](https://discourse.haskell.org/u/enobayram)\
**Post date:** [January 27, 2026, 8:07pm UTC](https://discourse.haskell.org/t/caching-for-incremental-changes-including-ghc-nix/13606/3 "2026-01-27T20:07:17Z")

</div>

I would honestly recommend against fighting with the Haskell-Nix tooling in order to get a `nix build` behavior that’s ultimately sub-optimal for local development. Even if you get `nix build` to only recompile files that got changed and their dependencies, that will still be orders of magnitude slower than dropping down to a multiple home units `ghci` session that just loads the module you’re working on plus the unit test module that tests it. That setup allows you to iterate much much faster than a `nix build` that would recompile only the changed files (and their dependencies), because:

1. Loading a module into `ghci` is MUCH faster than compiling the same module, even with -O0
2. IME, by only recompiling what’s changed, you’ll still typically recompile half of the project on average, because you’re only pruning modules below you in the dependency tree and not those above you. This is in contrast to the app/lib module + test module `:reload` workflow I mentioned above, where, if you’re careful with your module dependency tree, you bypass most of the dependencies below AND above you and only reload the modules between your app/lib module and the test module.

So, my recommendation is to keep your Haskell-Nix setup boring and easy to maintain over time and instead channel your local development efficiency efforts to setting up a great REPL environment and REPL-driven workflows.

---

<div class="post-metadata">

**Author:** ![clinton](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/clinton/32/4111_2.png) [@clinton](https://discourse.haskell.org/u/clinton)\
**Post date:** [January 28, 2026, 6:23am UTC](https://discourse.haskell.org/t/caching-for-incremental-changes-including-ghc-nix/13606/4 "2026-01-28T06:23:47Z")

</div>

Thinking out loud here somewhat, but I both want reproducibility and really want my local dev and “prod” builds using the same package set.

I guess I could just wrap `cabal build` or `stack build` in one big nix derivation, but that has the issue that any change to the code would result in requiring a full rebuild, including of any hackage dependencies.

Perhaps I could solve this though using `haskell.nix`, which apparently makes the nix build plan based on a cabal/stack one, and they both have the ability to have pins so perhaps that’s solves the out of sync issues?

So going back to GHCI, one of my main issues is of course I’d like my editor to use HLS, and at least with what I’ve found with the VSCode plugin, it just calls `cabal` on the command line. I have previously done hacks to ensure it calls cabal with some parameters (namely, the “HLS” style `.project` file I mentioned above), but yes, there’s no need for HLS to actually compile the code, interpreting and typechecking is all that’s needed. Is there anyway to force `cabal` to use GHCI instead of GHC, or is there another way I should go about getting HLS to work quickly and responsively particularly on a significant codebase?

---

<div class="post-metadata">

**Author:** ![enobayram](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/enobayram/32/1815_2.png) [@enobayram](https://discourse.haskell.org/u/enobayram)\
**Post date:** [January 28, 2026, 7:09am UTC](https://discourse.haskell.org/t/caching-for-incremental-changes-including-ghc-nix/13606/5 "2026-01-28T07:09:38Z")

</div>

I’m not sure what sort of project size we’re talking about, but IME, HLS was doing fine in projects that had \>50k LoC. I’m not sure what exactly it does under the hood, but I’m pretty sure it doesn’t just call cabal to build the project, it probably calls it to get the compiler flags and the dependency tree.

IME, HLS struggles the most when you open two files simultaneously with too may modules in between on the dependency tree. I.e. you’re working on the `Utils` module that almost every other module depends on, and you have the `Main` module open at the side, so that means HLS has to reload your entire project every time you make any change in `Utils`. I always try to avoid doing that. You can easily put yourself into this situation if you have a `TestUtils` module that all of your tests use and that `TestUtils` depends on a large chunk of your app/library modules. Then whenever you’re working on tests together with the module you’re testing, you’ll be waiting for the entire project to reload. Employing a little mechanical sympathy for HLS and being mindful of the shape of your module dependency graph goes a very long way.

My default choice for a Haskell project setup is to go with a `cabal.project` + `haskell.nix`. Specify your GHC version and the hackage snapshot in the `cabal.project` and that will make sure you get the same Haskell packages across your `cabal build/repl` and `nix build`. If your project depends on tricky system dependencies that you get from `nixpkgs`, you can always prepare a `nix develop/shell` environment that brings in just those packages and then run `cabal build` in that shell.

IMO, the ultimate Haskell development experience is when you set up a way to test the code you’re working on in a very fast `ghciwatch` loop. In your editor you have the app module on one panel and the test module in the other, with very few dependencies in between and in a terminal you run `ghciwatch` to run the tests whenever you save, so you get feedback within seconds of saving. Set this up from day one, make sure it stays smooth and neutralize with a taser anyone that tries to take that away from you.

---

<div class="post-metadata">

**Author:** ![alexfmpe](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/alexfmpe/32/5150_2.png) [@alexfmpe](https://discourse.haskell.org/u/alexfmpe)\
**Post date:** [January 30, 2026, 3:31am UTC](https://discourse.haskell.org/t/caching-for-incremental-changes-including-ghc-nix/13606/6 "2026-01-30T03:31:33Z")

</div>

> So going back to GHCI, one of my main issues is of course I’d like my editor to use HLS, and at least with what I’ve found with the VSCode plugin, it just calls `cabal` on the command line

IIUC it calls `cabal repl` which will use GHCi under the hood.

> another way I should go about getting HLS to work quickly and responsively particularly on a significant codebase

Generally making typechecking faster should help, probably also want to enable multi-repl. See [Cheaper: producing a program with less developer time](https://discourse.haskell.org/t/cheaper-producing-a-program-with-less-developer-time/13225)

> really want my local dev and “prod” builds using the same package set.

You can easily get a nix-shell which will build all your dependencies with nix (exactly same derivations as your prod build) so that only your actual project, where you’ll be iterating, is built with cabal/ghci, for fast iteration.

You just need your `shell.nix` to consist of a call to `shellFor`. Throw your own local packages into `packages`, then cabal/ghcid/etc into `nativeBuildInputs`. You can also throw HLS in there, but you’ll want to run `code .` inside the shell and tell VSCode to use $PATH instead of installing HLS itself.

Example usage:

> <https://github.com/alexfmpe/obenstein/blob/90ee3345a95eb157278d8d52c5929c06ed9a8057/project.nix#L64>

shellFor definition:

> <https://github.com/NixOS/nixpkgs/blob/6308c3b21396534d8aaeac46179c14c439a89b8a/pkgs/development/haskell-modules/make-package-set.nix#L461>
