Skip to content

Commit 319beb7

Browse files
committed
Fixes #163: pkg-config uses a more general version scheme
- #163 Also resolves #5138 Introduce PkgconfigVersion and PkgconfigVersionRange - `PkgconfigVersion` is compared with `rpmvercmp` - `PkgconfigVersionRange` is subset of `VersionRange` - with `cabal-version` before 3.0 it's parsed like `VersionRange`, where version digits are arbitrary integral (leading spaces allowed) - starting from cabal spec 3.0 `== x.y.*` and `^>=` (and set `{ .. }`) are disallowed. Yet, the version literals syntax is relaxed to accept alphanumerical + `-.` strings. E.g. openssl's `1.1.0h` is accepted. Lax `PkgconfigVersion` parser is also used to parse `pkg-config --modversion` output.
1 parent 2c29dd8 commit 319beb7

24 files changed

+2102
-74
lines changed

Cabal/Cabal.cabal

+14
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ extra-source-files:
5757
tests/ParserTests/errors/leading-comma-2c.errors
5858
tests/ParserTests/errors/leading-comma.cabal
5959
tests/ParserTests/errors/leading-comma.errors
60+
tests/ParserTests/errors/libpq1.cabal
61+
tests/ParserTests/errors/libpq1.errors
62+
tests/ParserTests/errors/libpq2.cabal
63+
tests/ParserTests/errors/libpq2.errors
6064
tests/ParserTests/errors/mixin-1.cabal
6165
tests/ParserTests/errors/mixin-1.errors
6266
tests/ParserTests/errors/mixin-2.cabal
@@ -156,6 +160,12 @@ extra-source-files:
156160
tests/ParserTests/regressions/leading-comma.cabal
157161
tests/ParserTests/regressions/leading-comma.expr
158162
tests/ParserTests/regressions/leading-comma.format
163+
tests/ParserTests/regressions/libpq1.cabal
164+
tests/ParserTests/regressions/libpq1.expr
165+
tests/ParserTests/regressions/libpq1.format
166+
tests/ParserTests/regressions/libpq2.cabal
167+
tests/ParserTests/regressions/libpq2.expr
168+
tests/ParserTests/regressions/libpq2.format
159169
tests/ParserTests/regressions/mixin-1.cabal
160170
tests/ParserTests/regressions/mixin-1.expr
161171
tests/ParserTests/regressions/mixin-1.format
@@ -218,6 +228,7 @@ extra-source-files:
218228
tests/ParserTests/warnings/unknownsection.cabal
219229
tests/ParserTests/warnings/utf8.cabal
220230
tests/ParserTests/warnings/versiontag.cabal
231+
tests/cbits/rpmvercmp.c
221232
tests/hackage/check.sh
222233
tests/hackage/download.sh
223234
tests/hackage/unpack.sh
@@ -402,6 +413,8 @@ library
402413
Distribution.Types.PackageName
403414
Distribution.Types.PackageName.Magic
404415
Distribution.Types.PkgconfigName
416+
Distribution.Types.PkgconfigVersion
417+
Distribution.Types.PkgconfigVersionRange
405418
Distribution.Types.UnqualComponentName
406419
Distribution.Types.IncludeRenaming
407420
Distribution.Types.Mixin
@@ -563,6 +576,7 @@ test-suite unit-tests
563576
UnitTests.Distribution.Utils.NubList
564577
UnitTests.Distribution.Utils.ShortText
565578
UnitTests.Distribution.Version
579+
UnitTests.Distribution.PkgconfigVersion
566580
main-is: UnitTests.hs
567581
build-depends:
568582
array,

Cabal/Distribution/Simple/Configure.hs

+5-4
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import Distribution.Simple.LocalBuildInfo
8181
import Distribution.Types.ExeDependency
8282
import Distribution.Types.LegacyExeDependency
8383
import Distribution.Types.PkgconfigDependency
84+
import Distribution.Types.PkgconfigVersionRange
8485
import Distribution.Types.LocalBuildInfo
8586
import Distribution.Types.LibraryName
8687
import Distribution.Types.ComponentRequestedSpec
@@ -1559,8 +1560,8 @@ configurePkgconfigPackages verbosity pkg_descr progdb enabled
15591560
`catchExit` (\_ -> die' verbosity notFound)
15601561
case simpleParsec version of
15611562
Nothing -> die' verbosity "parsing output of pkg-config --modversion failed"
1562-
Just v | not (withinRange v range) -> die' verbosity (badVersion v)
1563-
| otherwise -> info verbosity (depSatisfied v)
1563+
Just v | not (withinPkgconfigVersionRange v range) -> die' verbosity (badVersion v)
1564+
| otherwise -> info verbosity (depSatisfied v)
15641565
where
15651566
notFound = "The pkg-config package '" ++ pkg ++ "'"
15661567
++ versionRequirement
@@ -1573,8 +1574,8 @@ configurePkgconfigPackages verbosity pkg_descr progdb enabled
15731574
++ ": using version " ++ prettyShow v
15741575

15751576
versionRequirement
1576-
| isAnyVersion range = ""
1577-
| otherwise = " version " ++ prettyShow range
1577+
| isAnyPkgconfigVersion range = ""
1578+
| otherwise = " version " ++ prettyShow range
15781579

15791580
pkg = unPkgconfigName pkgn
15801581

Cabal/Distribution/Types/PkgconfigDependency.hs

+3-4
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ module Distribution.Types.PkgconfigDependency
77
import Distribution.Compat.Prelude
88
import Prelude ()
99

10-
import Distribution.Version (VersionRange, anyVersion)
11-
1210
import Distribution.Types.PkgconfigName
11+
import Distribution.Types.PkgconfigVersionRange
1312

1413
import Distribution.Parsec
1514
import Distribution.Pretty
@@ -22,7 +21,7 @@ import Text.PrettyPrint ((<+>))
2221
-- @since 2.0.0.2
2322
data PkgconfigDependency = PkgconfigDependency
2423
PkgconfigName
25-
VersionRange
24+
PkgconfigVersionRange
2625
deriving (Generic, Read, Show, Eq, Typeable, Data)
2726

2827
instance Binary PkgconfigDependency
@@ -36,5 +35,5 @@ instance Parsec PkgconfigDependency where
3635
parsec = do
3736
name <- parsec
3837
P.spaces
39-
verRange <- parsec <|> pure anyVersion
38+
verRange <- parsec <|> pure anyPkgconfigVersion
4039
pure $ PkgconfigDependency name verRange
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
{-# LANGUAGE DeriveDataTypeable #-}
2+
{-# LANGUAGE DeriveGeneric #-}
3+
-- @since 3.0
4+
module Distribution.Types.PkgconfigVersion (
5+
PkgconfigVersion (..),
6+
rpmvercmp,
7+
) where
8+
9+
import Distribution.Compat.Prelude
10+
import Prelude ()
11+
12+
import Distribution.Parsec
13+
import Distribution.Pretty
14+
import Distribution.Utils.Generic (isAsciiAlphaNum)
15+
16+
import qualified Data.ByteString as BS
17+
import qualified Data.ByteString.Char8 as BS8
18+
import qualified Distribution.Compat.CharParsing as P
19+
import qualified Text.PrettyPrint as PP
20+
21+
-- | @pkg-config@ versions.
22+
--
23+
-- In fact, this can be arbitrary 'BS.ByteString',
24+
-- but 'Parsec' instance is a little pickier.
25+
--
26+
-- @since 3.0
27+
newtype PkgconfigVersion = PkgconfigVersion BS.ByteString
28+
deriving (Generic, Read, Show, Typeable, Data)
29+
30+
instance Eq PkgconfigVersion where
31+
PkgconfigVersion a == PkgconfigVersion b = rpmvercmp a b == EQ
32+
33+
instance Ord PkgconfigVersion where
34+
PkgconfigVersion a `compare` PkgconfigVersion b = rpmvercmp a b
35+
36+
instance Binary PkgconfigVersion
37+
instance NFData PkgconfigVersion where rnf = genericRnf
38+
39+
instance Pretty PkgconfigVersion where
40+
pretty (PkgconfigVersion bs) = PP.text (BS8.unpack bs)
41+
42+
instance Parsec PkgconfigVersion where
43+
parsec = PkgconfigVersion . BS8.pack <$> P.munch1 predicate where
44+
predicate c = isAsciiAlphaNum c || c == '.' || c == '-'
45+
46+
-------------------------------------------------------------------------------
47+
-- rmpvercmp - pure Haskell implementation
48+
-------------------------------------------------------------------------------
49+
50+
-- | Compare two version strings as @pkg-config@ would compare them.
51+
--
52+
-- @since 3.0
53+
rpmvercmp :: BS.ByteString -> BS.ByteString -> Ordering
54+
rpmvercmp a b = go0 (BS.unpack a) (BS.unpack b)
55+
where
56+
go0 :: [Word8] -> [Word8] -> Ordering
57+
go0 xs ys = go1 (dropNonAlnum8 xs) (dropNonAlnum8 ys)
58+
59+
go1 :: [Word8] -> [Word8] -> Ordering
60+
go1 [] [] = EQ
61+
go1 [] _ = LT
62+
go1 _ [] = GT
63+
go1 xs@(x:_) ys
64+
| isDigit8 x =
65+
let (xs1, xs2) = span isDigit8 xs
66+
(ys1, ys2) = span isDigit8 ys
67+
-- numeric segments are always newer than alpha segments
68+
in if null ys1
69+
then GT
70+
else compareInt xs1 ys1 <> go0 xs2 ys2
71+
72+
-- isAlpha
73+
| otherwise =
74+
let (xs1, xs2) = span isAlpha8 xs
75+
(ys1, ys2) = span isAlpha8 ys
76+
in if null ys1
77+
then LT
78+
else compareStr xs1 ys1 <> go0 xs2 ys2
79+
80+
-- compare as numbers
81+
compareInt :: [Word8] -> [Word8] -> Ordering
82+
compareInt xs ys =
83+
-- whichever number has more digits wins
84+
compare (length xs') (length ys') <>
85+
-- equal length: use per character compare, "strcmp"
86+
compare xs' ys'
87+
where
88+
-- drop leading zeros
89+
xs' = dropWhile (== 0x30) xs
90+
ys' = dropWhile (== 0x30) ys
91+
92+
-- strcmp
93+
compareStr :: [Word8] -> [Word8] -> Ordering
94+
compareStr = compare
95+
96+
dropNonAlnum8 :: [Word8] -> [Word8]
97+
dropNonAlnum8 = dropWhile (\w -> not (isDigit8 w || isAlpha8 w))
98+
99+
isDigit8 :: Word8 -> Bool
100+
isDigit8 w = 0x30 <= w && w <= 0x39
101+
102+
isAlpha8 :: Word8 -> Bool
103+
isAlpha8 w = (0x41 <= w && w <= 0x5A) || (0x61 <= w && w <= 0x7A)
104+
105+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
{-# LANGUAGE DeriveDataTypeable #-}
2+
{-# LANGUAGE DeriveGeneric #-}
3+
module Distribution.Types.PkgconfigVersionRange (
4+
PkgconfigVersionRange (..),
5+
anyPkgconfigVersion,
6+
isAnyPkgconfigVersion,
7+
withinPkgconfigVersionRange,
8+
) where
9+
10+
import Distribution.Compat.Prelude
11+
import Prelude ()
12+
13+
import Distribution.CabalSpecVersion
14+
import Distribution.Parsec
15+
import Distribution.Pretty
16+
import Distribution.Types.PkgconfigVersion
17+
import Distribution.Types.Version
18+
import Distribution.Types.VersionRange
19+
20+
import qualified Data.ByteString.Char8 as BS8
21+
import qualified Distribution.Compat.CharParsing as P
22+
import qualified Text.PrettyPrint as PP
23+
24+
-- | @since 3.0
25+
data PkgconfigVersionRange
26+
= PcAnyVersion
27+
| PcThisVersion PkgconfigVersion -- = version
28+
| PcLaterVersion PkgconfigVersion -- > version (NB. not >=)
29+
| PcEarlierVersion PkgconfigVersion -- < version
30+
| PcOrLaterVersion PkgconfigVersion -- >= version
31+
| PcOrEarlierVersion PkgconfigVersion -- =< version
32+
| PcUnionVersionRanges PkgconfigVersionRange PkgconfigVersionRange
33+
| PcIntersectVersionRanges PkgconfigVersionRange PkgconfigVersionRange
34+
deriving (Generic, Read, Show, Eq, Typeable, Data)
35+
36+
instance Binary PkgconfigVersionRange
37+
instance NFData PkgconfigVersionRange where rnf = genericRnf
38+
39+
instance Pretty PkgconfigVersionRange where
40+
pretty = pp 0 where
41+
pp :: Int -> PkgconfigVersionRange -> PP.Doc
42+
pp _ PcAnyVersion = PP.text "-any"
43+
pp _ (PcThisVersion v) = PP.text "==" <<>> pretty v
44+
pp _ (PcLaterVersion v) = PP.text ">" <<>> pretty v
45+
pp _ (PcEarlierVersion v) = PP.text "<" <<>> pretty v
46+
pp _ (PcOrLaterVersion v) = PP.text ">=" <<>> pretty v
47+
pp _ (PcOrEarlierVersion v) = PP.text "<=" <<>> pretty v
48+
49+
pp d (PcUnionVersionRanges v u) = parens (d >= 1) $
50+
pp 1 v PP.<+> PP.text "||" PP.<+> pp 0 u
51+
pp d (PcIntersectVersionRanges v u) = parens (d >= 2) $
52+
pp 2 v PP.<+> PP.text "&&" PP.<+> pp 1 u
53+
54+
parens True = PP.parens
55+
parens False = id
56+
57+
instance Parsec PkgconfigVersionRange where
58+
-- note: the wildcard is used in some places, e.g
59+
-- http://hackage.haskell.org/package/bindings-libzip-0.10.1/bindings-libzip.cabal
60+
--
61+
-- however, in the presense of alphanumerics etc. lax version parser,
62+
-- wildcard is ill-specified
63+
64+
parsec = do
65+
csv <- askCabalSpecVersion
66+
if csv >= CabalSpecV3_0
67+
then pkgconfigParser
68+
else versionRangeToPkgconfigVersionRange <$> versionRangeParser P.integral
69+
70+
-- "modern" parser of @pkg-config@ package versions.
71+
pkgconfigParser :: CabalParsing m => m PkgconfigVersionRange
72+
pkgconfigParser = P.spaces >> expr where
73+
-- every parser here eats trailing space
74+
expr = do
75+
ts <- term `P.sepBy` (P.string "||" >> P.spaces)
76+
return $ foldr1 PcUnionVersionRanges ts
77+
78+
term = do
79+
fs <- factor `P.sepBy` (P.string "&&" >> P.spaces)
80+
return $ foldr1 PcIntersectVersionRanges fs
81+
82+
factor = parens expr <|> prim
83+
84+
prim = do
85+
op <- P.munch1 (`elem` "<>=^-") P.<?> "operator"
86+
case op of
87+
"-" -> anyPkgconfigVersion <$ (P.string "any" *> P.spaces)
88+
89+
"==" -> afterOp PcThisVersion
90+
">" -> afterOp PcLaterVersion
91+
"<" -> afterOp PcEarlierVersion
92+
">=" -> afterOp PcOrLaterVersion
93+
"<=" -> afterOp PcOrEarlierVersion
94+
95+
_ -> P.unexpected $ "Unknown version operator " ++ show op
96+
97+
afterOp f = do
98+
P.spaces
99+
v <- parsec
100+
P.spaces
101+
return (f v)
102+
103+
parens = P.between
104+
((P.char '(' P.<?> "opening paren") >> P.spaces)
105+
(P.char ')' >> P.spaces)
106+
107+
anyPkgconfigVersion :: PkgconfigVersionRange
108+
anyPkgconfigVersion = PcAnyVersion
109+
110+
-- | TODO: this is not precise, but used only to prettify output.
111+
isAnyPkgconfigVersion :: PkgconfigVersionRange -> Bool
112+
isAnyPkgconfigVersion = (== PcAnyVersion)
113+
114+
withinPkgconfigVersionRange :: PkgconfigVersion -> PkgconfigVersionRange -> Bool
115+
withinPkgconfigVersionRange v = go where
116+
go PcAnyVersion = True
117+
go (PcThisVersion u) = v == u
118+
go (PcLaterVersion u) = v > u
119+
go (PcEarlierVersion u) = v < u
120+
go (PcOrLaterVersion u) = v >= u
121+
go (PcOrEarlierVersion u) = v <= u
122+
go (PcUnionVersionRanges a b) = go a || go b
123+
go (PcIntersectVersionRanges a b) = go a && go b
124+
125+
-------------------------------------------------------------------------------
126+
-- Conversion
127+
-------------------------------------------------------------------------------
128+
129+
versionToPkgconfigVersion :: Version -> PkgconfigVersion
130+
versionToPkgconfigVersion = PkgconfigVersion . BS8.pack . prettyShow
131+
132+
versionRangeToPkgconfigVersionRange :: VersionRange -> PkgconfigVersionRange
133+
versionRangeToPkgconfigVersionRange = foldVersionRange
134+
anyPkgconfigVersion
135+
(PcThisVersion . versionToPkgconfigVersion)
136+
(PcLaterVersion . versionToPkgconfigVersion)
137+
(PcEarlierVersion . versionToPkgconfigVersion)
138+
PcUnionVersionRanges
139+
PcIntersectVersionRanges

Cabal/doc/file-format-changelog.rst

+5-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ relative to the respective preceding *published* version.
5858
* New set-notation syntax for ``==`` and ``^>=`` operators, see
5959
:pkg-field:`build-depends` field documentation for examples.
6060

61-
* Allow more whitespace in :pkg-field: `mixins` field
61+
* Allow more whitespace in :pkg-field:`mixins` field
62+
63+
* Wildcards are disallowed in :pkg-field:`pkgconfig-depends`,
64+
Yet the pkgconfig format is relaxed to accept e.g. versions like ``1.1.0h``.
65+
6266

6367
``cabal-version: 2.4``
6468
----------------------

Cabal/tests/ParserTests.hs

+4
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ errorTests = testGroup "errors"
119119
, errorTest "undefined-flag.cabal"
120120
, errorTest "mixin-1.cabal"
121121
, errorTest "mixin-2.cabal"
122+
, errorTest "libpq1.cabal"
123+
, errorTest "libpq2.cabal"
122124
]
123125

124126
errorTest :: FilePath -> TestTree
@@ -170,6 +172,8 @@ regressionTests = testGroup "regressions"
170172
, regressionTest "mixin-1.cabal"
171173
, regressionTest "mixin-2.cabal"
172174
, regressionTest "mixin-3.cabal"
175+
, regressionTest "libpq1.cabal"
176+
, regressionTest "libpq2.cabal"
173177
]
174178

175179
regressionTest :: FilePath -> TestTree

0 commit comments

Comments
 (0)