Open Source at Bellroy: Supporting Old GHC Versions

Jack Kelly profile image Jack Kelly 2025-03-19

There has been some good discussion on the Haskell.org Discourse recently about how many GHC versions it’s reasonable for a maintainer to support. Some maintainers minimise their maintenance burden by rotating out support for anything older than the three most recent GHC major releases; others drop old GHC versions with extreme reluctance.

Bellroy’s primary business is making and selling good things. To help the rest of the business make and sell these good things, Bellroy’s Tech Team builds and maintains software. We are always grateful for the massive ecosystem of open-source software that we build upon, and to the community of maintainers who keep it alive. Bellroy talks a lot about using business as a force for good; in the open-source realm, that means trying to “pay forward” the gifts we’ve built upon. Concretely, this means trying to work upstream-first wherever it makes sense, and releasing useful open-source packages of our own.

This puts us in a challenging position. We want our open-source releases to be broadly useful, and at the same time we cannot take on an unlimited maintenance burden. Tech Team plans and executes its work using a variant of the Shape Up process. We work in eight-week cycles, spending six weeks on project work followed by two weeks of “cool down”. Routine dependency updates and open-source maintenance all have to fit into the cool-down period.

We have an internal document describing how to maintain our open-source packages, and thought we’d share our current thinking on GHCs, library bounds and related issues. This is not the final word from us or even a general recommendation. It’s what we’re doing at the moment, it might change, and it is most definitely not an SLA.

Which GHCs to target?

Within those constraints, which GHCs do we support for an open-source release? First and foremost, we must maintain support for whatever GHC major release is used in our internal Haskell monorepo. Currently, that’s GHC 9.6.

We also aim to officially support any compiler marked “suitable for use” on the GHC GitLab’s wiki, but that’s less of a sure thing. We use Nix for developer shells, and for our open-source libraries, we need to keep the amount of Nix wrangling manageable. These packages use a Nix Flake that only provides GHC, and leaves package selection to the cabal command. If our package’s dependencies haven’t yet updated, or the newest GHC isn’t yet in nixpkgs, we might not be able to support a GHC recently marked “suitable for use”. In such instances, we offer help where we can, but usually have to leave the GHC upgrades for a future project cycle.

When we support a GHC version, we list it in the tested-with field of the package’s .cabal file. The get-tested tool reads this field and lets us run a GitHub Actions matrix of all the GHC versions we support.

Which old versions to remove?

Supporting multiple GHC versions sometimes means adding compatibility cruft to a package. The two most common forms of this are CPP directives in Haskell sources and if impl(ghc >=x.y) conditions in .cabal files. Such cruft might be necessary to support occasional major releases marked “suitable for use”, but can accumulate over a long range of GHC releases. We don’t want the maintenance burden of a package to grow without bound, but we also don’t want to needlessly remove support for older versions when it doesn’t cost additional effort.

The compromise we’ve settled on is this: we don’t remove old GHC versions from tested-with until supporting them requires more code than just supporting the “suitable for use” versions.

Which Haskell dialect to use?

The GHC202X language editions are extremely convenient. We use them throughout our monorepo, but open-source releases use Haskell2010 and explicit {-# LANGUAGE #-} pragmas. This is mostly to make things easier for the emerging MicroHs compiler, and to avoid raising the GHC lower bound just for a little convenience.

For similar reasons, we avoid convenience extensions such as -XBlockArguments (GHC 8.6.1) and -XImportQualifiedPost (GHC 8.10.1). These extensions only add alternative syntax, and although they’re generally old enough to not really factor into GHC bounds, it doesn’t seem worth excluding old versions or alternate compilers by requiring them.

What library bounds to use for dependencies?

Setting library bounds correctly still feels like a bit of an art. One helpful resource is the GHC wiki: its table of boot library versions, lists the version of base and other fundamental libraries (e.g., text) that each GHC release ships with. We set base bounds to admit every GHC version listed in tested-with, and ensure that other boot libraries have bounds which admit the library versions bundled with each GHC.

For other dependencies, we start with the version we actually used, and bound it above by the next major version (which is when the Package Version Policy allows the next breaking change). The cabal outdated command then warns us about new package versions we need to accommodate. If we have to make code changes, we cut a new release; otherwise we can revise the bounds on Hackage.

As with old GHCs, we can’t spend unlimited effort supporting expansive bounds, but for important libraries undergoing major change (e.g., the aeson-2.0 transition), we do try to support both versions at least for a while.

What dependencies are worth taking on?

We try to depend on the minimal set of libraries necessary to get the job done, because dragging in half of Hackage to solve a simple problem feels impolite. Concretely, this mostly means avoiding custom prelude packages and convenient-but-heavy libraries like string-interpolate (which needs haskell-src-exts), and using the microlens-* family of lens libraries instead of lens. In many cases, the generic-lens and generic-optics packages remove the need for library authors to provide lenses at all.

Conclusion

We currently maintain a small handful of open-source Haskell libraries and have more packages awaiting spin-out. Our experience so far is that these principles strike a good balance between maintenance effort and supporting a fair range of compilers and library versions. Dependency update days have generally been smooth, and we expect these processes to scale reasonably well as we release more software to Hackage. We’re looking forward to announcing those new packages when they’re ready.

And if we have to come up with something else, we’ll probably write about that too.