Our projects have been suffering from an unfortunate Nix ergonomics issue that’s been occupying my mind for a very long time now and flakes + recursive-nix are the ideal solution conceptually, but don’t work together in practice. Let me explain:
We use the excellent haskell.nix
infrastructure to wrap our Haskell projects with Nix. You drop the flake into the repo and it just builds your project with the same dependency set and build options as those used by the native Haskell tooling, the dream interface which keeps Nix out of the way of anyone that wants to work with Haskell tooling only.
A haskell.nix
-built derivation takes 5-10 seconds to evaluate even if the result is already in the store. Even worse, it involves IFD and the intermediate dependencies of this IFD is almost a GB. Neither of this is too bad for a single Haskell project, since your IFD dependencies get fetched the first time you build, and you nix build
occasionally, staying in your nix develop
most of the time, building with Haskell tooling incrementally.
The trouble starts when you want to use these haskell.nix
-built executables downstream. Say you have 5 Haskell executables from 5 different projects that you want to tie in together in a NixOS configuration. Then you have to wait for 5-10 seconds for each project, making it painful to iterate on your downstream flake. What’s even worse is that the one GB IFD dependencies are often also duplicated for each project (because they tend to depend on different versions of haskell.nix
), which means everyone that tries to build your downstream flake ends up fetching a 5GB closure just to evaluate the flake.
You can avoid the IFD in haskell.nix
by “materializing”, but that involves bloating your repo with very large generated sources, which spoils the whole “Nix stays out of the way unless you use it” aspect, but worse, it interacts very badly with flakes’ habit of copying the repo to the nix store for every build.
I’ve been thinking about a good solution to this whole thing and every solution (including doing nothing) has very unpleasant trade-offs one way or another. One solution that would have been perfect all-round and is also conceptually very clean is combining Nix flakes with recursive-nix! When you think about it, the two is actually a match made in heaven. Flakes’ purity and reproducibility mean that building a flake output inside a recursive-nix derivation is perfectly well-behaved.
So, imagine that I could write a recursive-nix derivation that essentially builds nix build ${self}
(please disregard, for the sake of the argument, the fact that the inputs to my flake can be different from my flake.lock
) and expose it as an output of my flake. As long as this derivation is in my binary cache, any downstream users of my flake would just fetch the resulting store path without having to evaluate pretty much anything, completely side-stepping the long evaluation times and the IFD dependencies.
It’s very unfortunate that this theoretically sound application of flakes + recursive-nix doesn’t work in practice today. The build of the recursive-nix derivation fails since it doesn’t have access to the internet (nor even the /nix folder). Flakes’ sandboxing essentially kills recursive-nix while they could’ve been the best buddies in the world.
I think the same perfect solution could be achieved if Nix flake evaluation cache would be extended to the Nix evaluation time. Particularly if we had remote Nix evaluation caches like the current binary caches. But it seems to me like getting recursive-nix to work with flakes is a much simpler undertaking and it reuses the whole existing infrastructure for caching.