hs

Towards a Common Haskell Development Environment

One of the projects we have been pursuing at the Haskell foundation is a unified Haskell installer. Hitherto things have been a little fragmented when it comes to Haskell installers, not covering Windows very uniformly and, of course, requiring different installers depending upon whether you want to work with stack or cabal-install. (I distinguish between the Cabal API used by both stack and the cabal-install application, using the package names to keep everything straight.)

This is really unfortunate for many reasons.

  • Poor Windows coverage restricts access to Haskell, setting up a viscous cycle whereby the poorer the Windows tooling, the smaller the pool of Windows Haskell developers available to improve or even maintain it.

  • It reinforces a tendency to silo developers with those using Posix/Windows or stack/cabal-install, contributing to a growing incomprehension of each other’s problems, even for those that actively want to see more uniformity in development environments. It is difficult to overstate the problems this causes.

  • Even for those that are prepared to build their own unified development environment it is not currently practical to do so based on the commonly used tools — unless you are prepared to patch the tools and/or install wrapper scripts (see below) as the current tools will install toolchains without regard those that are currently installed. It is of course easy enough to install one along side the other for a specific use but they will manage their own duplicate toolchains without constant intervention.

  • Writing satisfactory Haskell introductory tutorials is nearly impossible to do. You have to either pick one of the build systems and cover that properly, most likely on Posix, or try to to cover everything in detail which will immediately restrict your audience, or abstract away from the details and your audience will get culled when it comes to trying things out.

This list isn’t comprehensive and you can probably add to it.

How to fix?

[Update 2021-07-14: I should have made clear in the original posting that what follows is merely my own technical proposal for integrating cabal-install and stack and not in any way a Haskell Foundation adopted proposal.]

Given the above, ideally, all Haskell installers like stack and ghcup should install both cabal-install and stack in such a way that they will use a toolchain that either installs, and while allowing the developer to control which tool-chain installer gets invoked regardless of whether you are building with stack or cabal-install from a clean checkout. (Currently only stack will auto-install a missing toolchain but there is no reason why cabal-install could not trigger an installation with the right hooks.)

Cross-installing the build tools is almost trivial (ghcup is already unofficially doing it) — getting them to manage the toolchains coherently thereafter is the hard part.

A pragmatic approach could be taken as stack and ghcup each use entirely straightforward and stable organisations of their installed toolchains. Before each is about to install another toolchain they could check whether the other had already installed it and link it in rather than downloading and installing another GHC bindist. Perhaps predictably, nobody was keen on this solution, as they would be relying on undocumented interfaces going forward, clearly risking problems down the line.

I have come to see this is correct — we should build proper interfaces. It also opens up the opportunity to give the developer full control of which system manages the toolchain installations.

hs: a tooolchain installation broker

Whenever cabal-install or stack wish to locate a toolchain (say, ghc-8.10.4), and they have been configured to do so, they will call out to a new hs broker service:

$ hs whereis ghc-8.10.4
/Users/chris/.stack/programs/x86_64-osx/ghc-8.10.4

(Here we are execing hs, but an internal hs library call is also available.)

hs responds with the location of the root of the installed bindist if it is already installed (in the case above, we clearly have a stack managed toolchain).

If the toolchain is not installed then, depending upon how hs is configured, it will yield an error indicating that no such toolchain could be located, or it will auto-download-and-install the toolchain with the designated default toolchain installer, or it will ask the developer if they would like to download and install the required GHC bindist.

The build manager can specify this behaviour explicitly.

$ hs whereis --install     ghc-8.10.4
$ hs whereis --no-install  ghc-8.10.4
$ hs whereis --ask-install ghc-8.10.4

hs: a PATH interface for cabal-install

Traditionally, cabal-install uses the PATH to resolve the location of the tools. If ghc-pkg for GHC 8.10.4 is required then ghc-pkg-8.10.4 will be execed. Exploiting this we can install a bunch of wrapper scripts on the PATH containing variants of the following:

#!/usr/bin/env bash
hs run ghc-pkg-8.10.4 -- "$@"

Instead of yielding the location of the bindist hs run ghc-pkg-8.10.4 runs the command directly passing through all of the arguments after the -- without further interpretation. The wrapper scripts can rely on the configured installation behaviour or specify it explicitly.

Of course, for this to work these wrapper scripts (in ~/.hs/bin by default) must be on the path ahead of anything else that might have ghc-pkg-8.10.4 and friends.

configuring hs and listing installations

The installation manager priorities, the default install mode and default toolchain can be configured or displayed (by specifying no argument) as follows:

# ghcup to carry out installations requested from the build tools
$ hs use ghcup stack
# ask before installing any missing toolchains
$ hs use-install-mode ask-install
# specify the deafult toolchain when no version specified
$ hs use-compiler 8.10.4

To list the installation in your development environment:

$ stack list
8.6.5      stack /Users/chris/.stack/programs/x86_64-osx/ghc-8.6.5
8.8.3      stack /Users/chris/.stack/programs/x86_64-osx/ghc-8.8.3
8.8.4      stack /Users/chris/.stack/programs/x86_64-osx/ghc-8.8.4
8.10.4     stack /Users/chris/.stack/programs/x86_64-osx/ghc-8.10.4
8.10.5     ghcup /Users/chris/.ghcup/ghc/8.10.5
9.0.1      ghcup /Users/chris/.ghcup/ghc/9.0.1

configuring the build tools

Currently stack will use its internal GHC installations while cabal-install will rely on the path. It would be great if their global configurations would allow the developer or the bootstrap Haskell installer to configure them to defer to specified broker as follows:

installer-broker: hs

This would be bigger departure for cabal-install but it should be straightforward for cabal-install to make a call out to the broker when the need for a sandboxed toolchain is discovered with a configured broker, at which point cabal-install could internally put the toolchain located by the broker on its path, thereby – when a broker is configured – relieving the developer of the need to manage the PATH to capture the relevant toolchain.

Both build systems would have converged on the same behaviour when it comes to toolchain management.

a working prototype

I have hs written, a patch for stack and wrappers for cabal-install, yielding a unified system in which all of the toolchains are made available to both systems as installations are demanded and added by stack or ghcup according to the hs configuration. (This is Unix-only for now – Windows coverage should be straightforward and is in progress.)


Got an issue with any of this? Please share or drop me a line (see below).