Simple per-project setup

TLDR: Could we have a per-project file that GHCup and editor plugins could use as a single source of truth for the expected toolchain version?


I’ve been trying to find a simple setup for GHC, Cabal, HLS with VSCode support. I want it to be useful for beginners and intermediate Haskell developers. I have something that I find acceptable but I would like to improve it while keeping it simple. This setup is used by students.

I want a setup that is able to specify which version of GHC, cabal, HLS use
per project. This, I think, would help intermediate people to have multiple projects and not need to worry about environment setup that much.

So far I am suggesting students a setup that only needs to have GHCup and VSCode with a minimal config. Thanks to GHCup they not even need to install GHC, HLS, cabal manually.

This is the VSCode configuration for haskell.haskell we use.

{
    "haskell.manageHLS": "GHCup",
    "haskell.upgradeGHCup": false,
    "haskell.toolchain": {
        "ghc": "recommended",
        "hls": "recommended",
        "cabal": "recommended",
        "stack": null
    },
    "editor.formatOnSave": true
}

A Makefile, given in a template offers convenient wrappers to run, repl or test the code. Again, it only requires GHCup to be installed.

GHC_VERSION=recommended
CABAL_VERSION=recommended
GHCUP_ENV=ghcup run --ghc $(GHC_VERSION) --cabal $(CABAL_VERSION)

.PHONY: run
run:
	$(GHCUP_ENV) -- cabal run

.PHONY: test
test:
	$(GHCUP_ENV) -- cabal test --test-show-details=direct

.PHONY: repl
repl:
	$(GHCUP_ENV) -- cabal repl 

.PHONY: clean
clean:
	rm -rf dist-newstyle
	rm -rf bin

One of the challenges is that there is no single source of truth for what are the expected version of the toolchain. What is the recommended version could change any time during the course, not ideal. It’s easy to pin the version in the Makefile.

GHC_VERSION=9.6.7
CABAL_VERSION=3.12.1.0

But the editor might use a different version.

The workaround for this in VSCode would be to setup a profile with custom settings or use workspaces. So we would be left out of the simple “open the directory” use case.

I think that having some .ghcup or maybe more agnostic .haskell file where the expected versions of GHC, Cabal, HLS could be stated would be beneficial.

VSCode haskell.haskell plugin could pick that information. A ghcup run could do the same. Other tools could also benefit from a simpler declaration.

I am relaying heavily on ghcup. I know it’s not the only option to setup an
environment and there are many supporters of using Nix to have a full environment setup per project. But I believe ghcup is better for beginners and even intermediate.

  • Would you think is beneficial this per project ability to setup Haskell tooling?
  • Do you think there is value on designing it beyond GHC up so other tools can leverage it?

I am also aware of asdf but that is yet another dependency to impose, I would like to minimize the setup needed.

Thanks!

5 Likes

I think that usually the GHC version is the only one that changes across projects. You can set that in the cabal.project file, for example like this:

-- cabal.project
with-compiler: ghc-9.6.7

As for Cabal, I’ve never run into breaking changes, so I usually just stick to recommended or upgrade to a newer version if I want new features, but I always just set it globally.

Finally for HLS, I believe the Haskell vscode extension finds and installs a good version automatically so I also don’t configure this manually.

1 Like

I could see the cabal.project as the source of truth. But that will not download the ghc itself currently. It could be that GHCup (and through it, the VSCode haskell.haskell plugin) could be extended to download it & use it.

HLS is only needed for the editor, so if there is a good heuristic to get the right HLS version for the picked GHC I would be happy.

I agree that cabal should be less problematic if version don’t match. But because I am thinking of all the toolchain as a whole I was thinking towards another layer: independent of cabal itself.

Scoping this to download and use the GHC stated by the cabal.project when doing a ghcup run and haskell.haskell could be enough. Maybe with the addition of downloading the recommended cabalversion if none is available. Yet this sounds a bit more messy and I fear if others agree that that should be the behavior of ghcup (maybe behind some flag).

I think it should be easy to ad a ghcup project command or the like that just read the compiler version out of the current project file and ensured it was installed. since multiple compiler versions can be around at once, that alone should suffice.

There’s also an idea floating around the cabal issue tracker of making it possible to add a custom hook which runs ghcup when needed:

(I believe there was also another issue about the specific case of installing ghc versions, but I couldn’t find it quickly)

Checking how the vscode extension works it seems to prefer latest version of each tool instead of recommended, which seems not ideal for beginners. I recall last year that I specifically pointed choose recommended because of some bug in latest hls (I think it was something in the formatter).

Cabal is less critical as it should be more stable, current recommended version is 3.12.1.0 (1yr old) while latest is 3.16.0.0.

I see value in pushing for latest to gather feedback earlier, but for beginners I rather stick with recommended.

  • For HLS that could be left as an editor config, which unless declared somewhere it’s left to each person set it up in the editor.
  • For cabal we might need to allow different version or stick to latest unless declared somewhere.

I am not sure about generic pre/post build hooks per package: portability and security will be a pain. I see security was partially addressed in the proposal but interactivity is a concern.

I seems that allowing a ghcup run --ghc project ... that would check with-compiler: ghc-* in the cabal.project would be the less intrusive thing to do.

And a new ghcup project command that could give visibility to that would be handy so that the vscode plugin can say: Need to download ghc-X.Y.Z vscode-haskell/src/hlsBinaries.ts at 5cf4c8fa8f4f117c258aff1ca388a193d5a8e7cb · haskell/vscode-haskell · GitHub

And I don’t see logic in the vscode plugin to choose the HLS version based on the chosen GHC. So if we use a recommended/latest for it it will eventually stop working for some specific GHC. Unless we pin it somewhere.

1 Like

You are specifying Cabal (the tool). If you were to relax that requirement and use Stack, I get good results with VS Code’s Haskell extension and:

    "haskell.manageHLS": "GHCup",
    "haskell.toolchain": {
      "ghc": null,
      "cabal": null,
      "stack": null
    }

GHC is specified, per project, by the Stack project-level configuration file. Stack upgrades itself (stack upgrade) and fetches GHC (and MSYS2, on Windows), as needed. VS Code etc upgrades itself, and uses GHCup to fetch HLS as needed. GHCup upgrades itself (ghcup upgrade). There is no need to specify Stack, VS Code, HLS or GHCup versions in practice because of the emphasis on backwards compatibility: just use the most recent version for bug fixes, awareness of recent GHC versions, etc.

A typical project-level configuration file looks like this:

snapshot: lts-24.9 # Implies GHC 9.10.2

(You can, of course, use GHCup to fetch Stack. However, I am often upgrading to the master branch version of Stack and prefer to use Stack to fetch Stack.)

2 Likes

This has been proposed before, but today I don’t think ghcup should have intimate knowledge of how cabal works (and I have almost zero appetite to depend on Cabals chaotic API).

I believe that there is a hierarchy in a healthy toolchain and certain tools should know nothing about each other.

This can be solved in cabal, by implementing shell hooks:

We have implemented something like this in stack years ago and it works fine.

3 Likes

Can you be more precise?

This works on all platforms, e.g. see:

This hook is optionally installed through ghcup (the installer asks you for it). It is not much different from git hooks. We can do the same for cabal.

Separation of concerns makes sense. I do tend to worry “generic” hooks that interact with the build process are too open to all sorts of uses – although having them configured per-box rather than per-package could help.

Perhaps a simpler still approach would be to use cabal’s existing extensible command structure to add an external cabal setup-compiler command that parses the project file and invokes ghcup with the desired ghc version.

1 Like

Maybe it’s a me problem, but I prefer to use cabal. I find it simpler and more similar to other package manager.

I see that what I am saying is a scoped version of 'ghcup satisfy' command · Issue #109 · haskell/ghcup-hs · GitHub. Wouldn’t at least parsing the cabal.project file to find a with-compiler: ghc-* be desired?

To honor the hierarchy in a toolchain it seems that having a .ghcup in the project would be the best approach to keep things within the realm of ghcup.

(I just read that you consider ghcup obsolete after so much work, so first of all thanks for all the effort, honestly. FWIW I do think there is still value in ghcup for lots of people)

So the idea would be to allow each project to use cabal shell hooks (that doesn’t exist today) to download additional toolchain.

I see that as a bit more complicated than having declarative information of the ghc (and maybe hls) wanted. The project template ends up being more flexible but also more complex. It could work, but is it reasonable to expect that that effort could move forward?

Continue with that, you need to trust scripts author or have a way of trusting the scripts (as described in Addition of generic pre build and post build hooks. · Issue #9892 · haskell/cabal · GitHub and following comments, there are some nuances on the UX). By having declarative information it means you need to trust that cabal or ghcup will do the right thing, I find that trust easier.

Regarding portability, all the examples are bash scripts. How that works on Windows?

And having hooks/scripts on individual packages, without some trust mechanism is calling for troubles. Not so much for the top level hooks/scripts at the project, but for nested dependencies is a big no. IIUC Addition of generic pre build and post build hooks. · Issue #9892 · haskell/cabal · GitHub wants to add cabal hooks for nested packages, I have suffered the same feature in other package managers of other languages that support this, and how I wish that feature was opt-in only.

Seems simpler and isolated, but how it could work in the full picture?

The user has ghcup and the project checked out. I can see a Makefile or script downloading the external setup-compiler. Should the VSCode extension do the same? relay on this external command when the GHCup option is not set or given the "project" value for "ghc" ? If that’s the config it sounds it should be responsible of ghcup to do something with it.


Thanks for all the context and links of past discussions.

What’s the difference of trusting cabal or ghcup? If we want to be pedantic, then scripts are in fact much more trustworthy (if you understand shell well), because you can read what is actually executed. You can’t do that with Haskell.

The hook in question is also super short, because it basically defers all the work to the ghcup binary, which is doing the actual work.

My idea of shell hooks is that it’s something entirely controlled by the builder, not the package author. You decide whether hooks run at all and if so, which ones.

Did you just got bitten by my April Fool?

At the end of the day it is executing arbitrary code unless you review it, yes.

If the script is located in the project and short, it can be reviewed. But scripts grow in complexity with time. Keeping it up to date in each project with best practices becomes a burden.

Regarding scripts in arbitrary packages, they are harder to review because, if wanted to, you need to know which version will be used, then review it, then install it. Or some variation on that. Even if you review it the supply chain could have been compromised: a new release of some package is injected with malicious code. Unless safeguards are put it requires diligence to avoid such problems. I think that ghcup getting compromised and released is much less possible than an arbitrary package, this is the root of the reason I find it easier to trust ghcup (or whatever official tool rather than arbitrary packages).

So for me it’s maintenance at each project and trust the benefit of pushing it to ghcup itself.

So by the builder means local to the project. Right? The project template becomes more complex (maintenance point), but it removes the trust issue to some degree. Beginners & intermediate will need to vet a more complex project template as opposed to trusting the official tooling and a declarative value of which ghc version is wanted.

:sweat_smile: Oh! Well, it’s definitely best if you are engaged with ghcup.


Another aspect on to build this inside cabal is that then the user will need to first download cabal and then download the rest of the tools. If ghcup is able to determine all the tools they can be download in parallel.

The stack install hook has not grown in years (because again: it’s mainly just calling ghcup). I don’t consider this a valid argument against the proposed solution.

I proposed no such thing.

Again: ghcup (or rather its bootstrap script) could install said hooks. This is already the case for stack installed via ghcup. The end user would simply acknowledge that option during the installation process. There isn’t much maintenance involved.

VSCode could have an option to also install those hooks.

Ah, you are talking about global scripts installed, not per project. I missed that, sorry.

From Addition of generic pre build and post build hooks. · Issue #9892 · haskell/cabal · GitHub I understood that the proposal shifted to per-project hook.

If it has been working good for stack for many years then it could work for cabal also, yet we need to wait until cabal global hooks lands.

Meanwhile having a cabal-ghcup-setup-compiler external command as suggested before is something I could use, yet that command would depend on Cabal library (which is not ideal) to parse the cabal.project file. But I see little to no chance to get that into the vscode plugin so it defeats the purpose.

If the goal is vscode doing this, just adding it to the vscode plugin directly seems the easiest path. Genuinely its a one-liner and doesn’t require parsing a file at all, just a robust regex.

I have to insist on using Nix and flakes, and then using direnv to automatically set up everything. This is a far more general solution.

Here is one of my projects that uses standalone Haskell files and also cabal projects: GitHub - emlautarom1/HaskellSnippets: Code snippets with some Haskell experiments . Opening the directory in VSCode automatically sets up GHC, cabal and LSP. The downside of any Nix based solution is that it does not work on Windows.

Other similar solutions are devbox ( Haskell | Jetify Docs ), mise-en-place (https://mise.jdx.dev/), or devenv (https://devenv.sh/).

For a more drastic approach you can use DevContainers (https://containers.dev/)

So basically not viable for most beginners.

1 Like

It’s a bit more than parsing.

The project file format allows imports (they may be local files or URLs), so you need to fetch them. There’s also conditionals now that need to be resolved, see 2.1. Conditionals and imports.

I had a look at the third party package cabal-install-parsers and it doesn’t seem that it handles those cases.

Only imports would need to be dealt with. Compilers can be set from within imports, but they cannot be set in conditional blocks (as conditionals themselves test on compiler).