Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Why is haskell.nix so complex (OR, how can we reduce it)? #1855

Closed
L-as opened this issue Feb 21, 2023 · 10 comments
Closed

Why is haskell.nix so complex (OR, how can we reduce it)? #1855

L-as opened this issue Feb 21, 2023 · 10 comments
Assignees
Labels
bug Something isn't working wontfix

Comments

@L-as
Copy link
Contributor

L-as commented Feb 21, 2023

cloc . --exclude-dir=materialized gives

     739 text files.
     571 unique files.                                          
     171 files ignored.

github.com/AlDanial/cloc v 1.96  T=0.79 s (723.5 files/s, 129753.4 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
diff                           140           2235          10095          49948
Nix                            247           1634           5573          19161
Bourne Shell                     6            161            438           3333
Haskell                         77            472            295           2691
Markdown                        43            823              0           2512
JSON                            14              0              0           2271
YAML                            22             77            135            421
XML                             14             13              0             58
TOML                             3              0              0             17
C/C++ Header                     2              6              2             11
C                                1              3              0              4
JavaScript                       2              1              7              4
-------------------------------------------------------------------------------
SUM:                           571           5425          16545          80431
-------------------------------------------------------------------------------

haskell.nix is a Nix framework only for Haskell, yet it's so complex. ~20k lines of Nix, ~50k lines of patches, etc.
There's a reason for every single line of code (obviously), yet, looking at it from the big picture, it's not clear
why we need so much code to compile Haskell using GHC.
The patches are obviously not needed, every single patch that is still necessary should be upstreamed.
Perhaps there was a culture of working around upstream rather than attempting to fix upstream?
Without being deeply intimate with most of the codebase, could haskell.nix be simpler if we

  1. Tried to fix the issues in GHC/Cabal that make it hard to build, and
  2. Drop everything legacy?

Other projects for reference:

My motivation is that it's quite frustrating to deal with building Haskell code. There's no good solution really. The above two projects don't fix the problem either, they use the code from Nixpkgs which is quite complex too.
If we think about the problem from first principles, we want to be able to compile Haskell modules, that depends on other Haskell modules (perhaps from the same project or another project), and eventually turn that into either a (shared) library or executable.
That library or executable might need other dependencies, some of which are necessary because of Haskell dependencies (c-bits, pkg-config, etc.). We additionally want the (obvious) ability to cross-compile.
Cross-compilation is mainly complicated because GHC only supports targetting one platform at a time (IIRC) like GCC, etc.. Nixpkgs has also had to workaround this issue resulting in complicated code for GCC, resulting in the build-host-target platform triple, when really it ought to be just build-host.

If rather than using Nix to orchestrate Cabal and GHC, could we not attempt to make Cabal nix-native as also mentioned in NixOS/rfcs#134 (comment) (this might necessarily involve using ca-derivations and dynamic-derivations)?

In the end, I honestly don't think there's much code to reuse from the current haskell.nix, but I doubt anyone is satisfied with the status quo. It is not fun to use haskell.nix, or to work with Haskell, because of the tooling.

@L-as L-as added the bug Something isn't working label Feb 21, 2023
@L-as L-as changed the title Why is haskell.nix so complex (OR, how can we reduce it?) Why is haskell.nix so complex (OR, how can we reduce it)? Feb 21, 2023
@angerman
Copy link
Collaborator

@L-as thanks for starting this discussion. Yes, we do want to slim it down as well. Adding all of nix-tools to haskell.nix, made it probably look even larger; though it's just a organisational optimisation.

The patches are obviously not needed, every single patch that is still necessary should be upstreamed.
Perhaps there was a culture of working around upstream rather than attempting to fix upstream?

that is a valid question, though the answer is definitely not that we have a culture of working around upstream. IOG is in fact very involved in upstream GHC development. If you look at the patches, you'll see most of them are for 8.10, and that's effectively a dead compiler at this point. The likelihood of getting any significant 8.10.8 is virtually zero. Due to the changes between 8.10, and what's currently in HEAD of GHC, it's also non-trivial to upstream 8.10 fixes into HEAD, even though we'd like to it's a non-trivial time investment when 8.10 is (still) our production compiler :-/

We could probably drop a significant amount of logic, if we supported only Hadrian only GHCs, and dropped everything before. That does would make haskell.nix much less useful for a lot of projects still on 8.10. There is a fairly long discussion around GHCs stability here.

A lot of complexity in haskell.nix comes from its ability to cross compile far beyond what stock haskell toolchains can do, due to the power of nixpkgs. If we rip all of that out, we can probably go back to the haskell infra in nixpkgs but would lose the component level granularity.

I've also written something about how I see haskell.nix on reddit a few days ago.

Regarding different granularity (module level), this is what I believe https://github.com/tweag/rules_haskell does for bezel, and what https://github.com/nmattia/snack tried to do.

Am I willing to throw most of haskell.nix away if we can? Absolutely. I don't like complexity or code for that matter. If it can be done simpler, I'm all for it. If nix evolves to allow us to do that. By all means let's do that! I have no emotional attachment to haskell.nix. It's a tool to get a job done. And it does get that job done fairly well, despite its sharp edges.

We can probably improve on a few things in haskell.nix, and especially on the complexity of IFDs. All of this is time intensive. Haskell.nix has been an iterative approach to continuously improve in a somewhat organic way towards new challenges. The most recent being support for custom hackages in the form of CHaP, and now also revision support for that. All of this sadly piles on complexity.

haskell.nix has for the most part tried to defer complexity to other tools (e.g. let cabal solve the build plan; and use cabal's Setup.hs to configure/build packages and components). When possible it strives to not go down the NIH route. The compiler derivations are a slight departure from this, but mostly due to us not wanting to be reliant on nixpkgs providing the compiler we need, and not changing the expressions in ways that break haskell.nix. The idea here is that haskell.nix is orthogonal to nixpkgs.

Lastly, GHC (and cabal), have (sadly) quite a bit of knowledge about linking necessities. The fact that it's already replicated in cabal, and cabal doesn't always just defer to GHC for this, can lead to complications, and replicating that in yet another layer looks (to me), like not an improvement, but making it even more brittle then all this already is.

If you have some ideas where we could easily drop some code/complexity from haskell.nix, please do not hesitate to propose doing that!

@yvan-sraka yvan-sraka self-assigned this Feb 27, 2023
@TravisWhitaker
Copy link
Contributor

A lot of complexity in haskell.nix comes from its ability to cross compile far beyond what stock haskell toolchains can do, due to the power of nixpkgs.

This was the primary motivation for my organization to move from upstream Nixpkgs' Haskell machinery to Haskell.nix. Moritz, Hamish, and all of the other contributors have made a massive investment in ensuring that native support for aarch64, x86-to-aarch64 cross support, and mingw-w64 cross support work well. For whatever reason, upstream Nixpkgs seems to not care as much about this functionality, and I often found it broken and struggled to get any fixes I had to make upstreamed. Haskell.nix's GHC patches are an important part of this improved support, especially since my organization is also begrudgingly still stuck on GHC 8.10.7. I am extremely grateful for this; a lot of my and my colleagues time used to be spent keeping our fork of upstream Nixpkgs' Haskell machinery doing what we wanted, now we just use Haskell.nix and it (mostly) just works.

Another important benefit to mention here is that Haskell.nix provides a very nice (although scantly documented) library of Nix functions for its underlying machinery. This too was very valuable for me, since my organization has a perhaps unique approach to Haskell development. We have almost 100 internal (closed source) Haskell projects used across our company, and they all have to work together. To achieve this, we build everything in the company against a fixed stackage snapshot, to which we add this set of ~100 internal packages (plus a handful of version overrides to make certain hairy upstream packages work). We build all the Hoogle docs for this whole ecosystem, CI pushes it to an internal server so we can query it, and the whole setup provides a Debian-like dependency management model. I can add any other Haskell library made by anyone else in the company to my project without thinking about it; everything will be ABI-compatible because we're all using the same dependency versions. I only had to write about ~1000 Nix LOC to get all this working, using Haskell.nix as a library of sorts.

If we think about the problem from first principles, we want to be able to compile Haskell modules, that depends on other Haskell modules (perhaps from the same project or another project), and eventually turn that into either a (shared) library or executable.
That library or executable might need other dependencies, some of which are necessary because of Haskell dependencies (c-bits, pkg-config, etc.). We additionally want the (obvious) ability to cross-compile.

Doing everything you mention here in a way that's guaranteed to be reproducible is a tall order. In fact, I'm surprised that it only takes ~20,000 Nix LOC to make something that works as well as Haskell.nix does. Upstream Nixpkgs' toolchain bootstrapping machinery is also quite complex, but you're right to say that Haskell.nix is more complex than this. This extra complexity comes from these sources:

  • The Haskell package system (Haskell.nix uses IFD to manage this).
  • The bifurcation in strategies for dealing with the package system (the stackage-way and the cabal-solver-way).
  • Non-retargetability of GHC (I have to build a new GHC to target a new platform, GCC has the same problem as you point out).
  • Template Haskell.

This last point is, in my opinion, the least sane, but it's not a problem with Haskell.nix. A big problem with TH is that it makes an assumption that's very, very difficult for cross-compilation to deal with: TH code is assumed to run on the target platform, not the host platform. Haskell.nix deals with this in an ingenious (or harebrained, depending on your perspective) way: it runs a remote iserv in QEMU. In fact, if I remember correctly, Moritz was the one who made remote iserv possible in GHC in the first place.

I doubt anyone is satisfied with the status quo. It is not fun to use haskell.nix, or to work with Haskell, because of the tooling.

Where should we be looking for inspiration? Who has a toolchain that provides all of the guarantees of Nix, with the convenience of Haskell.nix (I don't have to write any Nix once everything's setup, IFD takes care of everything), but with more fun?

@angerman
Copy link
Collaborator

Just a minor correction: I didn't come up with iserv; the original idea came from @luite (as part of solving TH for GHCJS), and then @simonmar took the idea into GHC in the form of iserv to allow (iirc) primarily loading different flavours into ghci, by delegating to a process.

What I did was only fix some word size assumptions, such that cross word size communication did work; and then build iserv-proxy around it, such that it supports cross compilation better. haskell.nix really only ties this all together with a lot of automation.

Ultimately haskell.nix wouldn't exist without all the contributors over the years who have helped!

@L-as
Copy link
Contributor Author

L-as commented Mar 17, 2023

Where should we be looking for inspiration? Who has a toolchain that provides all of the guarantees of Nix, with the convenience of Haskell.nix (I don't have to write any Nix once everything's setup, IFD takes care of everything), but with more fun?

None at all in the Haskell ecosystem! For what it's worth, having to write Nix code is OK IMO, but it should be easy to understand. A simpler system would fix most of the problems I have I think.
I agree with your points about complexity, and in the end, the fault lies not with haskell.nix, but with the rest of the Haskell ecosystem.

@angerman
Copy link
Collaborator

@L-as here is were we two disagree I guess:

having to write Nix code is OK IMO
I don't think it is. I don't want to write nix, (or any code for that matter at all), it's going to bitrot over time and maintenance becomes a liability. Yes this includes haskell.nix, I don't like the amount of code we have here. Ideally it would all be gone. I'm happy for any ideas how to incrementally remove code from haskell.nix and make it easier!

most of the problems I have I think.
If you have the time, could you make list of the problems you have?

Lastly, yes we try to merge as much stuff upstream, but this is hard for many reasons. Production codebases staying on an effectively End Of Line compiler. And ultimately also just the time to do it :(

Here is what I think haskell.nix should be:

{
    description = "simple haskell.nix flake";

    inputs.haskellNix.url = "github:input-output-hk/haskell.nix";
    inputs.nixpkgs.follows = "haskellNix/nixpkgs-unstable";
    inputs.flake-utils.url = "github:numtide/flake-utils";

    outputs = { self, nixpkgs, flake-utils, haskellNix }: flake-utils.lib.eachDefaultSystem (system:
    let
      pkgs = import nixpkgs { inherit system; inherit (haskellNix) config; overlays = [ haskellNix.overlay ]; };
      myHsPkgs = pkgs.haskell-nix.project { src = ./.; compiler-nix-name = "ghc8107"; };
    in rec {
      packages.myComponent  = myHsPkgs.flake.packages."<pkg>:<type>:<component>";
      hydraJobs = packages;
    });
}

that's about as much nix code as I'm willing to have for a haskell project. Now if we add in packaging, and cross compilation, it will need a bit more. But that's mostly pkgs.pkgsCross.<target>.haskell-nix.project { ... }.

@L-as
Copy link
Contributor Author

L-as commented Mar 19, 2023

Wrt. GHC, if we do the base proposal by Ericson, it would likely be heavily aleviated.

@stale
Copy link

stale bot commented Jul 17, 2023

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Jul 17, 2023
@Ericson2314
Copy link

haskell/cabal#9089 I think will help a lot with this. Cabal doing something similar, even if it is not exactly the same, will excise a lot of bugs. Likewise, I really hope we could build GHC with Haskell.nix without Hadrian too. Such dogfooding would also excise a lot of bugs.

@stale stale bot removed the wontfix label Jul 17, 2023
@andreabedini
Copy link
Member

haskell/cabal#9089 I think will help a lot with this. Cabal doing something similar, even if it is not exactly the same, will excise a lot of bugs. Likewise, I really hope we could build GHC with Haskell.nix without Hadrian too. Such dogfooding would also excise a lot of bugs.

@Ericson2314 Happy to chat in private if you like but I think there's lots to do, both in cabal and in haskell.nix.

Copy link

stale bot commented Nov 15, 2023

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Nov 15, 2023
@stale stale bot closed this as completed Jan 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working wontfix
Projects
None yet
Development

No branches or pull requests

6 participants