We recently had a very large discussion of stack on Reddit, which I thought was great for kicking off some discussion. In that discussion, there was a very active thread about how stack relates to the Package Versioning Policy (aka, the PVP).
The PVP - and in particular its policy on preemptive upper bounds - has been discussed at great length many times in the past, and I have no wish to revisit that discussion here. I also went into some detail on Reddit explaining how stack and Stackage relate to the PVP, and won't repeat those details here (tl;dr: they're orthogonal issues, and upper bounds are neither required nor discouraged by either).
However, the discussion brought up one of my long-held beliefs: "the right way to solve this is with tooling." Specifically: manually keeping track of lots of lower and upper bounds is a hard, tedious process that many people get wrong regularly. One great effort in this direction is the Hackage Matrix Builder project, which helps find overly lax (and, I believe, overly restrictive) bounds on Hackage and report them to authors. I'm announcing an experimental feature I've just added to stack master, and hoping it can help with the initial creation of upper bounds.
The feature itself is quite simple. When you run the
upload commands, there's a new
--pvp-bounds, which can be set to
none (don't modify the cabal file at all),
upper (add upper bounds),
lower bounds), and
both (add both upper and lower
bounds). The default is
none (we shouldn't change an
author's cabal file without permission), but that default can be
overridden in the
stack.yaml file (either for your
project, or in
~/.stack/stack.yaml). The algorithm is
bytestring >= 0.9)
That was a bit abstract, so let's give an example. Support you're using LTS 3.0, which includes aeson-0.8.0.2, attoparsec-0.12.1.6, and text-126.96.36.199. Let's further say that in your cabal file you have the following:
build-depends: aeson >= 0.7 , text < 2 , attoparsec
If you specify
--pvp-bounds both on the command
line, you'll end up with the following changes:
aeson >= 0.7 && < 0.9. Reason: We respect the existing lower bound (>= 0.7), but add in an upper bound based on the version of aeson used in your snapshot. Since we're currently using 0.8.0.2, the next major bump will be 0.9.
text >= 188.8.131.52 && < 2. We respect the existing upper bound (even though it's no in compliance with PVP rules - this allows you as an author to maintain more control when using this feature), but add in a lower bound to the currently used version of text.
attoparsec >= 0.12.1.6 && < 0.13. Since there are no upper or lower bounds, we add both of them in.
Just to round out the feature description:
--pvp-bounds none, your bounds are unmodified
--pvp-bounds loweryou get
--pvp-bounds upperyou get
The motivation behind this approach is simplicity. For users of stack, your versioning work usually comes down to choosing just one number: the LTS Haskell version (or Stackage Nightly date), which is relatively easy to deal with. Managing version ranges of every single dependency is an arduous process, and hard to get right. (How often have you accidentally started relying on a new feature in a minor version bump of a dependency, but forgotten to bump the lower bound?) Now, stack will handle that drudgery for you.
Of course, there are cases where you'll want to tell stack that you know better than it, e.g. "I'm using a subset of the aeson API that's compatible between both 0.7 and 0.8, so I want to override stack's guess at a lower bound." Or, "even though the PVP says text-1.3 may have a breaking change, I've spoken with the author, and I know that the parts I'm using won't be changed until version 2."
This feature should still be considered experimental, but I'm hopeful that it will be an experiment that works, and can make both upper bounds advocates and detractors happy. As a member of the latter group, I'm actually planning on trying out this functionality on some of my packages for future releases.
There are still downsides to this feature, which are worth mentioning:
--reorder-goals --max-backjumps=-1flag to cabal. (Note: when using stack's dependency solving capabilities, it passes in these flags for you automatically.)
Do you like this blog post and need help with industrial Haskell, Rust or DevOps? Contact us.