I was in a meeting today and I was pointed to a few places where users expressed surprise that somelib ^>= 1.2.3
is not completely identical to a combination of somelib >= 1.2.3
and somelib < 1.3
. I remember when the operator was first introduced, it took me a bit of time to assimilate the documentation for it, so I figured I’d try to describe it in a bit of a different way, together which my understanding of the context.
The documentation for the build-depends
field begins by describing the general syntax for library dependency version constraints, given using the >
, >=
, ==
, <=
, and <
operators combined using &&
and ||
.
Much of the tooling around Cabal is intended to support a declarative workflow where a package states the requirements that it places on its dependencies, and then a solver identifies a set of specific versions that satisfies all requirements. This led to the development of the Package Versioning Policy, a forerunner of the now-widespread semver. One of the PVP’s requirements is that packages have upper bounds - rather than specifying somelib >= 2.4.3
, one must specify somelib >=2.4.3 && < 2.5
.
This specification of upper bounds has a key limitation: there’s no way to tell the difference between “2.5 doesn’t exist yet, so we’d better protect our users for when it comes out” and “I just tested with 2.5, and they changed that thing I depend on in such a way that I can’t work with both 2.4.x and 2.5.x at the same time”. Let’s call these two scenarios “conservative” and “definite” upper bounds. Without knowing the difference, build tools have to treat all upper bounds as if they are definite to avoid doing the wrong thing.
It’s nice for the build tool to know whether an upper bound is conservative or definite, but there was no way to state this in .cabal
files until around 2017. For the sake of backwards compatibility, <
constraints are still treated as if they are definite. But since 2017, somelib ^>= 3.4.2
is a way to describe a conservative upper bound on a major version. In other words, somelib ^>= 3.4.2
is usually treated the same as somelib >= 3.4.2 && < 3.5
, as the PVP would require. However, it additionally indicates that the upper bound of 3.5
is a conservative one, rather than a definite one - it’s got to be there for the sake of reliably constructing build plans, but if we do have to try bumping an upper bound, that one would be the first to try.
cabal-install
uses this in a project configuration file to allow upper bounds to be ignored selectively. In particular, in cabal.project
, the line:
allow-newer: somelib
means to ignore all upper bounds for somelib
, while
allow-newer: ^somelib
ignores only the conservative upper bounds, making it more likely that a build will succeed.
All this information is in the manual, but I heard that the way it was written could be confusing, and the ReadTheDocs search doesn’t allow searching for infix operators like ^>=
. If you gained in understanding from this post, then there’s an opportunity to volunteer here: you have my full permission and encouragement to adapt any part of it to text in the manual for the .cabal
file format and contribute it upstream! Just having learned something is the best time to write it down for others.