Skip to content

Commit

Permalink
Implement Caret-style version range operator
Browse files Browse the repository at this point in the history
This implements a new syntactic sugar: The version range operator
`^>=` which is equivalent to `>=` intersected with an
automatically inferred major upper bound.

This new syntax is only allowed for `cabal-version: >=2.0`, and
allows to describe the most common use of version
bounds more conveniently:

    build-depends: foo ^>= 1.2.3.4,
                   bar ^>= 1

The declaration above is exactly equivalent to

    build-depends: foo >= 1.2.3.4 && < 1.3,
                   bar >= 1 && < 1.1

The `^`-symbol was chosen because it can serve as a mnemonic when the
`>` sign is rotated and interpreted as "less than upper bound"

Moreover, `^` appears to become a quasi-standard to denote morally
equivalent operator that way in other language ecosystems which similiar
to Haskell have adopted semantic versioning:

 - Node: https://nodesource.com/blog/semver-tilde-and-caret/
 - Bower: https://bower.io/docs/api/#install
 - PHP: https://getcomposer.org/doc/articles/versions.md#caret

Ruby, on the other hand, uses a Tilde operator (`~>`) for that
purpose (but with a less robust semantic):

 - https://blog.codeship.com/optimists-guide-pessimistic-library-versioning

And Python is currently planing to use an `~=` operator:

 - https://www.python.org/dev/peps/pep-0440/#compatible-release
  • Loading branch information
hvr committed Sep 1, 2016
1 parent 118c709 commit ae63fb8
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 6 deletions.
42 changes: 42 additions & 0 deletions Cabal/Distribution/PackageDescription/Check.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,18 @@ checkCabalVersion pkg =
[ display (Dependency name (eliminateWildcardSyntax versionRange))
| Dependency name versionRange <- depsUsingWildcardSyntax ]

-- check use of "build-depends: foo ^>= 1.2.3" syntax
, checkVersion [2,0] (not (null depsUsingMajorBoundSyntax)) $
PackageDistInexcusable $
"The package uses major bounded version syntax in the "
++ "'build-depends' field: "
++ commaSep (map display depsUsingMajorBoundSyntax)
++ ". To use this new syntax the package need to specify at least "
++ "'cabal-version: >= 2.0'. Alternatively, if broader compatibility "
++ "is important then use: " ++ commaSep
[ display (Dependency name (eliminateMajorBoundSyntax versionRange))
| Dependency name versionRange <- depsUsingMajorBoundSyntax ]

-- check use of "tested-with: GHC (>= 1.0 && < 1.4) || >=1.8 " syntax
, checkVersion [1,8] (not (null testedWithVersionRangeExpressions)) $
PackageDistInexcusable $
Expand Down Expand Up @@ -1195,6 +1207,7 @@ checkCabalVersion pkg =
(\_ -> True) -- >=
(\_ -> False)
(\_ _ -> False)
(\_ _ -> False)
(\_ _ -> False) (\_ _ -> False)
id)
(specVersionRaw pkg)
Expand All @@ -1212,12 +1225,16 @@ checkCabalVersion pkg =
(const 1) (const 1)
(const 1) (const 1)
(const (const 1))
(const (const 1))
(+) (+)
(const 3) -- uses new ()'s syntax

depsUsingWildcardSyntax = [ dep | dep@(Dependency _ vr) <- buildDepends pkg
, usesWildcardSyntax vr ]

depsUsingMajorBoundSyntax = [ dep | dep@(Dependency _ vr) <- buildDepends pkg
, usesMajorBoundSyntax vr ]

-- TODO: If the user writes build-depends: foo with (), this is
-- indistinguishable from build-depends: foo, so there won't be an
-- error even though there should be
Expand All @@ -1238,16 +1255,40 @@ checkCabalVersion pkg =
(const False) (const False)
(const False) (const False)
(\_ _ -> True) -- the wildcard case
(\_ _ -> False)
(||) (||) id

-- NB: this eliminates both, WildcardVersion and MajorBoundVersion
-- because when WildcardVersion is not support, neither is MajorBoundVersion
eliminateWildcardSyntax =
foldVersionRange'
anyVersion thisVersion
laterVersion earlierVersion
orLaterVersion orEarlierVersion
(\v v' -> intersectVersionRanges (orLaterVersion v) (earlierVersion v'))
(\v v' -> intersectVersionRanges (orLaterVersion v) (earlierVersion v'))
intersectVersionRanges unionVersionRanges id

usesMajorBoundSyntax :: VersionRange -> Bool
usesMajorBoundSyntax =
foldVersionRange'
False (const False)
(const False) (const False)
(const False) (const False)
(\_ _ -> False)
(\_ _ -> True) -- MajorBoundVersion
(||) (||) id

eliminateMajorBoundSyntax =
foldVersionRange'
anyVersion thisVersion
laterVersion earlierVersion
orLaterVersion orEarlierVersion
(\v _ -> withinVersion v)
(\v v' -> intersectVersionRanges (orLaterVersion v) (earlierVersion v'))
intersectVersionRanges unionVersionRanges id


compatLicenses = [ GPL Nothing, LGPL Nothing, AGPL Nothing, BSD3, BSD4
, PublicDomain, AllRightsReserved
, UnspecifiedLicense, OtherLicense ]
Expand Down Expand Up @@ -1319,6 +1360,7 @@ displayRawVersionRange =
(\v -> (Disp.text ">=" <<>> disp v , 0))
(\v -> (Disp.text "<=" <<>> disp v , 0))
(\v _ -> (Disp.text "==" <<>> dispWild v , 0))
(\v _ -> (Disp.text "^>=" <<>> disp v , 0))
(\(r1, p1) (r2, p2) ->
(punct 2 p1 r1 <+> Disp.text "||" <+> punct 2 p2 r2 , 2))
(\(r1, p1) (r2, p2) ->
Expand Down
41 changes: 39 additions & 2 deletions Cabal/Distribution/Version.hs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ module Distribution.Version (
differenceVersionRanges,
invertVersionRange,
withinVersion,
majorBoundVersion,
betweenVersionsInclusive,

-- ** Inspection
Expand Down Expand Up @@ -125,6 +126,7 @@ data VersionRange
| LaterVersion Version -- > version (NB. not >=)
| EarlierVersion Version -- < version
| WildcardVersion Version -- == ver.* (same as >= ver && < ver+1)
| MajorBoundVersion Version -- @^>= ver@ (same as >= ver && < MAJ(ver)+1)
| UnionVersionRanges VersionRange VersionRange
| IntersectVersionRanges VersionRange VersionRange
| VersionRangeParens VersionRange -- just '(exp)' parentheses syntax
Expand Down Expand Up @@ -274,8 +276,17 @@ invertVersionRange =
withinVersion :: Version -> VersionRange
withinVersion = WildcardVersion

-- | The version range @>= v1 && <= v2@.
-- | The version range @^>= v@.
--
-- For example, for version @1.2.3.4@, the version range @^>= 1.2.3.4@ is the same as
-- @>= 1.2.3.4 && < 1.3@.
--
-- Note that @^>= 1@ is equivalent to @>= 1 && < 1.1@.
--
-- @since 2.0@
majorBoundVersion :: Version -> VersionRange
majorBoundVersion = MajorBoundVersion

-- In practice this is not very useful because we normally use inclusive lower
-- bounds and exclusive upper bounds.
--
Expand Down Expand Up @@ -334,6 +345,7 @@ foldVersionRange anyv this later earlier union intersect = fold
fold (LaterVersion v) = later v
fold (EarlierVersion v) = earlier v
fold (WildcardVersion v) = fold (wildcard v)
fold (MajorBoundVersion v) = fold (majorBound v)
fold (UnionVersionRanges v1 v2) = union (fold v1) (fold v2)
fold (IntersectVersionRanges v1 v2) = intersect (fold v1) (fold v2)
fold (VersionRangeParens v) = fold v
Expand All @@ -342,6 +354,10 @@ foldVersionRange anyv this later earlier union intersect = fold
(orLaterVersion v)
(earlierVersion (wildcardUpperBound v))

majorBound v = intersectVersionRanges
(orLaterVersion v)
(earlierVersion (majorUpperBound v))

-- | An extended variant of 'foldVersionRange' that also provides a view of the
-- expression in which the syntactic sugar @\">= v\"@, @\"<= v\"@ and @\"==
-- v.*\"@ is presented explicitly rather than in terms of the other basic
Expand All @@ -358,12 +374,18 @@ foldVersionRange' :: a -- ^ @\"-any\"@ version
-- inclusive lower bound and the
-- exclusive upper bounds of the
-- range defined by the wildcard.
-> (Version -> Version -> a) -- ^ @\"^>= v\"@ major upper bound
-- The function is passed the
-- inclusive lower bound and the
-- exclusive major upper bounds
-- of the range defined by this
-- operator.
-> (a -> a -> a) -- ^ @\"_ || _\"@ union
-> (a -> a -> a) -- ^ @\"_ && _\"@ intersection
-> (a -> a) -- ^ @\"(_)\"@ parentheses
-> VersionRange -> a
foldVersionRange' anyv this later earlier orLater orEarlier
wildcard union intersect parens = fold
wildcard major union intersect parens = fold
where
fold AnyVersion = anyv
fold (ThisVersion v) = this v
Expand All @@ -380,6 +402,7 @@ foldVersionRange' anyv this later earlier orLater orEarlier
(ThisVersion v')) | v==v' = orEarlier v

fold (WildcardVersion v) = wildcard v (wildcardUpperBound v)
fold (MajorBoundVersion v) = major v (majorUpperBound v)
fold (UnionVersionRanges v1 v2) = union (fold v1) (fold v2)
fold (IntersectVersionRanges v1 v2) = intersect (fold v1) (fold v2)
fold (VersionRangeParens v) = parens (fold v)
Expand Down Expand Up @@ -501,6 +524,18 @@ isWildcardRange (Version branch1 _) (Version branch2 _) = check branch1 branch2
check (n:ns) (m:ms) | n == m = check ns ms
check _ _ = False

-- | Compute next greater major version to be used as upper bound
--
-- Example: @0.4.1@ produces the version @0.5@ which then can be used
-- to construct a range @>= 0.4.1 && < 0.5@
majorUpperBound :: Version -> Version
majorUpperBound version = version { versionBranch = upperBound }
where
upperBound = case versionBranch version of
[] -> [0,1] -- should not happen
[m1] -> [m1,1] -- e.g. version '1'
(m1:m2:_) -> [m1,m2+1]

------------------
-- Intervals view
--
Expand Down Expand Up @@ -802,6 +837,7 @@ instance Text VersionRange where
(\v -> (Disp.text ">=" <<>> disp v , 0))
(\v -> (Disp.text "<=" <<>> disp v , 0))
(\v _ -> (Disp.text "==" <<>> dispWild v , 0))
(\v _ -> (Disp.text "^>=" <<>> disp v , 0))
(\(r1, p1) (r2, p2) ->
(punct 2 p1 r1 <+> Disp.text "||" <+> punct 2 p2 r2 , 2))
(\(r1, p1) (r2, p2) ->
Expand Down Expand Up @@ -867,6 +903,7 @@ instance Text VersionRange where
("<=", orEarlierVersion),
(">", LaterVersion),
(">=", orLaterVersion),
("^>=", MajorBoundVersion),
("==", ThisVersion) ]

-- | Does the version range have an upper bound?
Expand Down
1 change: 1 addition & 0 deletions Cabal/changelog
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
* Internal 'build-tools' dependencies are now added to PATH
upon invocation of GHC, so that they can be conveniently
used via `-pgmF`. (#1541)
* Add support for new caret-style version range operator `^>=` (#3705)

1.24.0.0 Ryan Thomas <[email protected]> March 2016
* Support GHC 8.
Expand Down
26 changes: 22 additions & 4 deletions Cabal/doc/developing-packages.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -1337,21 +1337,39 @@ for these fields.
library
build-depends:
base >= 2,
foo >= 1.2 && < 1.3,
foo >= 1.2.3 && < 1.3,
bar
~~~~~~~~~~~~~~~~

Dependencies like `foo >= 1.2 && < 1.3` turn out to be very common
Dependencies like `foo >= 1.2.3 && < 1.3` turn out to be very common
because it is recommended practise for package versions to
correspond to API versions. As of Cabal 1.6, there is a special
syntax to support this use:
correspond to API versions (see [PVP][]).

Since Cabal 1.6, there is a special wildcard syntax to help with such ranges

~~~~~~~~~~~~~~~~
build-depends: foo ==1.2.*
~~~~~~~~~~~~~~~~

It is only syntactic sugar. It is exactly equivalent to `foo >= 1.2 && < 1.3`.

Starting with Cabal 2.0, there's a new syntactic sugar to
support [PVP][]-style major upper bounds conveniently, and is
inspired by similiar syntactic sugar found in other language
ecosystems where it's often called the "Caret" operator:

~~~~~~~~~~~~~~~~
build-depends: foo ^>= 1.2.3.4,
bar ^>= 1
~~~~~~~~~~~~~~~~

The declaration above is exactly equivalent to

~~~~~~~~~~~~~~~~
build-depends: foo >= 1.2.3.4 && < 1.3,
bar >= 1 && < 1.1
~~~~~~~~~~~~~~~~

Note: Prior to Cabal 1.8, `build-depends` specified in each section
were global to all sections. This was unintentional, but some packages
were written to depend on it, so if you need your `build-depends` to
Expand Down
3 changes: 3 additions & 0 deletions Cabal/tests/UnitTests/Distribution/Version.hs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ prop_foldVersionRange' range =
laterVersion earlierVersion
orLaterVersion orEarlierVersion
(\v _ -> withinVersion v)
(\v _ -> majorBoundVersion v)
unionVersionRanges intersectVersionRanges id
range
where
Expand Down Expand Up @@ -655,6 +656,7 @@ prop_parse_disp1 vr =
laterVersion earlierVersion
orLaterVersion orEarlierVersion
(\v _ -> withinVersion v)
(\v _ -> MajorBoundVersion v)
unionVersionRanges intersectVersionRanges id

stripParens :: VersionRange -> VersionRange
Expand Down Expand Up @@ -713,6 +715,7 @@ displayRaw =
(\v -> Disp.text ">=" <> disp v)
(\v -> Disp.text "<=" <> disp v)
(\v _ -> Disp.text "==" <> dispWild v)
(\v _ -> Disp.text "^>=" <> disp v)
(\r1 r2 -> r1 <+> Disp.text "||" <+> r2)
(\r1 r2 -> r1 <+> Disp.text "&&" <+> r2)
(\r -> Disp.parens r) -- parens
Expand Down

0 comments on commit ae63fb8

Please sign in to comment.