Open Source at Bellroy: Supporting Old GHC Versions

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.