The ^>= operator in .cabal files

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.

33 Likes

Hmm, I think I’m going to suggest to purge this requirement from PVP, especially since it knows nothing about the caret operator.

This seems like a non-sensible requirement for people testing their packages in a rolling-release fashion.

This is also a thing:

allow-newer: ^all

https://cabal.readthedocs.io/en/stable/cabal-project.html#cfg-field-allow-newer

6 Likes

Why? It seems like a fairly reasonable requirement to have.

2 Likes

It’s something reasonable to suggest, but it’s an unreasonable requirement.

A user might build their package every day against latest hackage, get notification of build failures and fix them the same day.

That’s a possible workflow. Something that’s even possible to implement holistically for hackage… if we allowed ourselves to have visions for this platform again.

Can the caret operator help here? Yes, but it still is a different signal and a different strategy. I believe both are valid (no upper bound vs ^>= upper bound).

1 Like

Exactly! We need soft upper bounds (“conservative” — I have no evidence it works with larger versions) and hard upper bounds (“definite” – I have evidence it does not work with larger versions).
However, I think the caret operator is the wrong solution, since it ties in a lower bound, muddling the clear concepts. Rather, we should have a new operator for just soft upper bounds, like <? 2.5. These soft upper bounds can then ignored by tooling to test relaxation of upper bounds.

12 Likes

Yes, that seems like a natural and straightforward approach. I always found ^>= baffling, and can never remember what it means for longer than 5 minutes after the most recent time someone explained it to me.

6 Likes

The PVP states:

When publishing a Cabal package, you SHALL ensure that your dependencies in the build-depends field are accurate. This means specifying not only lower bounds, but also upper bounds on every dependency.

At some point in the future, Hackage may refuse to accept packages that do not follow this convention. The aim is that before this happens, we will put in place tool support that makes it easier to follow the convention and less painful when dependencies are updated.

However, I think this is something Distribution.PackageDescription.Check.checkPackage does not warn about (except for the base package and the library stanza).

For this strategy to work, wouldn’t someone have to build every released version of their package at this daily cadence to ensure that that released version, which might get selected by some build plan, wouldn’t be the one to break due to failure to set upper bounds?

Oh, I’ve never known about what ^>= actually does :sweat_smile:

This is good to know, definitely. I’ll adjust my package dependencies accordingly :thinking:

I very much support a soft upper bounds operator, not least because it will finally let us compose ^>= 1.1 && ^>= 1.2 into a single interval :slight_smile:

For consistency’s sake, tho it may confuse some people, I’d be open to <^ instead of <?.

As I recall from the hubbub years ago about ^>= is that the maintainer would declare the lower bound then some other Hackage process would test each version of the dependency (in some sort of matrix build) and automatically set the upper bound when breakage would occur.

It already does warn on missing upper bounds for all dependencies on master.

I can only speculate that the rationale was "^>= 0.4 is shorter and easier to write than >= 0.4 && <? 0.5", but the latter sure seems better from a sense-making perspective.

What about conservative lower bounds? “>=? 0.4 && <? 0.5” makes sense, but I’m not even sure how I’d do it with carets.

2 Likes

May I ask what is meant by "all dependencies on master"? Stack’s sdist command makes use of checkPackage, and, in simple tests, I could not provoke it into warning about missing upper bounds (except for base). (EDIT: If that is a bug in Stack, I’ll raise an issue and see if I can fix it - but I can’t see that Stack filters out any of the warnings it receives, for example.)

Sorry, I was not clear.

I meant: on Cabal head, cabal check (and so, checkPackage) will check for all dependencies upper bounds (exceptions for benchmarks, tests). This feature was introduced in #8361.

Users will find it in cabal-install/Cabal from version 3.10.1.0.

Since warings were typed, third party tools can easily filter what they are not interested in.

1 Like

Is the ^>= not redundant with the principle of PVP itself ?
What I mean is, we know that >=2.5 hasn’t been tested with versions that don’t exist yet, so in a way
it should never be written because I can not guarantee that this will work with every single new release coming in the future. >=2.5 is therefore only what I call optimistic.

On the other hand we know that if the package follows PVP then version 3 should probably break everything, so in the pessimistic scenario >=2.5 equals to >=2.5 && <3.

To summarize given >=2.5 and package version 3, the optimistic option is till work and the pessimistic in won’t. That seems pretty the same as ^>=2.5 to me.

3 Likes

Would you be interested in improving the manual so that it would convey this to you accurately? The text as written made immediate sense to me, but this is not a universal experience, so it would be good to make it more accessible. As someone who just learned it, you’re perfectly positioned to update the docs!

>=2.5 will cause a build failure by default if your codebase is not working with 2.6. ^>=2.5 will not cause a build failure by default, unless a user has set allow-newer: ^all or similar.

So these are quite different in practice.

1 Like

It’s only because tools decide to treat them differently even their semantic is quite the same IMHO.

1 Like

Yeah, that might be a good indication that it doesn’t belong in the PVP spec after all.

1 Like