Commit a0d80350 authored by Herbert Valerio Riedel's avatar Herbert Valerio Riedel 🕺

Enhance `--allow-{newer,older}` syntax

This extends the capabilities of `--allow-{newer,older}` to allow for more
fine-grained scoping to control more precisely which packages and constraints
a relaxation is applied to. See updated documentation for more details.
parent e2cacc17
......@@ -670,7 +670,7 @@ The following settings control the behavior of the dependency solver:
the flag multiple times.
.. cfg-field:: allow-newer: none, all or list of scoped package names (space or comma separated)
--allow-newer, --allow-newer=[none,all,pkg]
--allow-newer, --allow-newer=[none,all,[scope:][^]pkg]
:synopsis: Lift dependencies upper bound constaints.
:default: ``none``
......@@ -687,11 +687,28 @@ The following settings control the behavior of the dependency solver:
allow-newer: pkg:dep-pkg
This syntax is recommended, as it is often only a single package
If the scope shall be limited to specific releases of ``pkg``, the
extended form as in
::
allow-newer: pkg-1.2.3:dep-pkg, pkg-1.1.2:dep-pkg
can be used to limit the relaxation of dependencies on
``dep-pkg`` by the ``pkg-1.2.3`` and ``pkg-1.1.2`` releases only.
The scoped syntax is recommended, as it is often only a single package
whose upper bound is misbehaving. In this case, the upper bounds of
other packages should still be respected; indeed, relaxing the bound
can break some packages which test the selected version of packages.
The syntax also allows to prefix the dependee package with a
modifier symbol to modify the scope/semantic of the relaxation
transformation in a additional ways. Currently only one modifier
symbol is defined, i.e. ``^`` (i.e. caret) which causes the
relaxation to be applied only to ``^>=`` operators and leave all other
version operators untouched.
However, in some situations (e.g., when attempting to build packages
on a new version of GHC), it is useful to disregard *all*
upper-bounds, with respect to a package or all packages. This can be
......@@ -701,21 +718,32 @@ The following settings control the behavior of the dependency solver:
::
-- Disregard upper bounds involving the dependencies on
-- packages bar, baz and quux
allow-newer: bar, baz, quux
-- packages bar, baz. For quux only, relax
-- 'quux ^>= ...'-style constraints only.
allow-newer: bar, baz, ^quux
-- Disregard all upper bounds when dependency solving
allow-newer: all
For consistency, there is also the explicit wildcard scope syntax
``*`` (or its alphabetic synonym ``all``). Consequently, the first
part of the example above is equivalent to the explicitly scoped
variant:
::
allow-newer: all:bar, *:baz, *:^quux
:cfg-field:`allow-newer` is often used in conjunction with a constraint
(in the cfg-field:`constraints` field) forcing the usage of a specific,
newer version of a package.
The command line variant of this field is ``--allow-newer=bar``. A
The command line variant of this field is e.g. ``--allow-newer=bar``. A
bare ``--allow-newer`` is equivalent to ``--allow-newer=all``.
.. cfg-field:: allow-older: none, all, list of scoped package names (space or comma separated)
--allow-older, --allow-older=[none,all,pkg]
--allow-older, --allow-older=[none,all,[scope:][^]pkg]
:synopsis: Lift dependency lower bound constaints.
:default: ``none``
......
......@@ -73,6 +73,7 @@ import Distribution.Client.Types
( SourcePackageDb(SourcePackageDb)
, UnresolvedPkgLoc, UnresolvedSourcePackage
, AllowNewer(..), AllowOlder(..), RelaxDeps(..), RelaxedDep(..)
, RelaxDepScope(..), RelaxDepMod(..)
)
import Distribution.Client.Dependency.Types
( PreSolver(..), Solver(..)
......@@ -90,10 +91,6 @@ import Distribution.PackageDescription.Configuration
( finalizePD )
import Distribution.Client.PackageUtils
( externalBuildDepends )
import Distribution.Version
( Version, mkVersion
, VersionRange, anyVersion, thisVersion, orLaterVersion, withinRange
, simplifyVersionRange, removeLowerBound, removeUpperBound )
import Distribution.Compiler
( CompilerInfo(..) )
import Distribution.System
......@@ -108,6 +105,7 @@ import Distribution.Text
( display )
import Distribution.Verbosity
( normal, Verbosity )
import Distribution.Version
import qualified Distribution.Compat.Graph as Graph
import Distribution.Solver.Types.ComponentDeps (ComponentDeps)
......@@ -426,24 +424,18 @@ hideInstalledPackagesAllVersions pkgnames params =
-- 'addSourcePackages' won't have upper bounds in dependencies relaxed.
--
removeUpperBounds :: AllowNewer -> DepResolverParams -> DepResolverParams
removeUpperBounds (AllowNewer RelaxDepsNone) params = params
removeUpperBounds (AllowNewer allowNewer) params =
params {
depResolverSourcePkgIndex = sourcePkgIndex'
}
where
sourcePkgIndex' = fmap relaxDeps $ depResolverSourcePkgIndex params
relaxDeps :: UnresolvedSourcePackage -> UnresolvedSourcePackage
relaxDeps srcPkg = srcPkg {
packageDescription = relaxPackageDeps removeUpperBound allowNewer
(packageDescription srcPkg)
}
removeUpperBounds (AllowNewer relDeps) = removeBounds RelaxUpper relDeps
-- | Dual of 'removeUpperBounds'
removeLowerBounds :: AllowOlder -> DepResolverParams -> DepResolverParams
removeLowerBounds (AllowOlder RelaxDepsNone) params = params
removeLowerBounds (AllowOlder allowNewer) params =
removeLowerBounds (AllowOlder relDeps) = removeBounds RelaxLower relDeps
data RelaxKind = RelaxLower | RelaxUpper
-- | Common internal implementation of 'removeLowerBounds'/'removeUpperBounds'
removeBounds :: RelaxKind -> RelaxDeps -> DepResolverParams -> DepResolverParams
removeBounds _relKind RelaxDepsNone params = params -- no-op optimisation
removeBounds relKind relDeps params =
params {
depResolverSourcePkgIndex = sourcePkgIndex'
}
......@@ -452,35 +444,68 @@ removeLowerBounds (AllowOlder allowNewer) params =
relaxDeps :: UnresolvedSourcePackage -> UnresolvedSourcePackage
relaxDeps srcPkg = srcPkg {
packageDescription = relaxPackageDeps removeLowerBound allowNewer
packageDescription = relaxPackageDeps relKind relDeps
(packageDescription srcPkg)
}
-- | Relax the dependencies of this package if needed.
--
-- Helper function used by 'removeLowerBound' and 'removeUpperBounds'
relaxPackageDeps :: (VersionRange -> VersionRange)
-- Helper function used by 'removeBounds'
relaxPackageDeps :: RelaxKind
-> RelaxDeps
-> PD.GenericPackageDescription -> PD.GenericPackageDescription
relaxPackageDeps _ RelaxDepsNone gpd = gpd
relaxPackageDeps vrtrans RelaxDepsAll gpd = PD.transformAllBuildDepends relaxAll gpd
relaxPackageDeps _ RelaxDepsNone gpd = gpd -- subsumed by no-op case in 'removeBounds'
relaxPackageDeps relKind RelaxDepsAll gpd = PD.transformAllBuildDepends relaxAll gpd
where
relaxAll = \(Dependency pkgName verRange) ->
Dependency pkgName (vrtrans verRange)
relaxPackageDeps vrtrans (RelaxDepsSome allowNewerDeps') gpd =
relaxAll (Dependency pkgName verRange) =
Dependency pkgName (removeBound relKind RelaxDepModNone verRange)
relaxPackageDeps relKind (RelaxDepsSome depsToRelax0) gpd =
PD.transformAllBuildDepends relaxSome gpd
where
thisPkgName = packageName gpd
allowNewerDeps = mapMaybe f allowNewerDeps'
f (RelaxedDep p) = Just p
f (RelaxedDepScoped scope p) | scope == thisPkgName = Just p
| otherwise = Nothing
relaxSome = \d@(Dependency depName verRange) ->
if depName `elem` allowNewerDeps
then Dependency depName (vrtrans verRange)
else d
thisPkgId = packageId gpd
depsToRelax = Map.fromList $ mapMaybe f depsToRelax0
f :: RelaxedDep -> Maybe (PackageName,RelaxDepMod)
f (RelaxedDep scope rdm p) = case scope of
RelaxDepScopeAll -> Just (p,rdm)
RelaxDepScopePackage p0
| p0 == thisPkgName -> Just (p,rdm)
| otherwise -> Nothing
RelaxDepScopePackageId p0
| p0 == thisPkgId -> Just (p,rdm)
| otherwise -> Nothing
relaxSome :: Dependency -> Dependency
relaxSome d@(Dependency depName verRange)
| Just relMod <- Map.lookup depName depsToRelax =
Dependency depName (removeBound relKind relMod verRange)
| otherwise = d -- no-op
-- | Internal helper for 'relaxPackageDeps'
removeBound :: RelaxKind -> RelaxDepMod -> VersionRange -> VersionRange
removeBound RelaxLower RelaxDepModNone = removeLowerBound
removeBound RelaxUpper RelaxDepModNone = removeUpperBound
removeBound relKind RelaxDepModCaret =
foldVersionRange'
anyVersion
thisVersion
laterVersion
earlierVersion
orLaterVersion
orEarlierVersion
(\v _ -> withinVersion v)
caretTransformation -- see below
unionVersionRanges
intersectVersionRanges
id
where
-- This function is the interesting part as it defines the meaning
-- of 'RelaxDepModCaret', i.e. to transform only @^>=@ constraints;
caretTransformation l u = case relKind of
RelaxUpper -> orLaterVersion l -- rewrite @^>= x.y.z@ into @>= x.y.z@
RelaxLower -> earlierVersion u -- rewrite @^>= x.y.z@ into @< x.(y+1)@
-- | Supply defaults for packages without explicit Setup dependencies
--
......
......@@ -25,13 +25,13 @@ import Distribution.Client.Compat.Prelude
import Distribution.Package
( Package(..), HasMungedPackageId(..), HasUnitId(..)
, PackageInstalled(..), newSimpleUnitId )
, PackageIdentifier(..), PackageInstalled(..), newSimpleUnitId )
import Distribution.InstalledPackageInfo
( InstalledPackageInfo, installedComponentId, sourceComponentName )
import Distribution.PackageDescription
( FlagAssignment )
import Distribution.Version
( VersionRange )
( VersionRange, nullVersion )
import Distribution.Types.ComponentId
( ComponentId )
import Distribution.Types.MungedPackageId
......@@ -401,6 +401,9 @@ data RelaxDeps =
RelaxDepsNone
-- | Ignore upper bounds in dependencies on the given packages.
--
-- Note that 'RelaxDepsNone' and @RelaxDepsSome []@ are equivalent
-- (TODO: change @[RelaxedDep]@ to @NonEmpty RelaxDep@ or remove 'RelaxDepsNone')
| RelaxDepsSome [RelaxedDep]
-- | Ignore upper bounds in dependencies on all packages.
......@@ -409,20 +412,55 @@ data RelaxDeps =
-- | Dependencies can be relaxed either for all packages in the install plan, or
-- only for some packages.
data RelaxedDep = RelaxedDep PackageName
| RelaxedDepScoped PackageName PackageName
data RelaxedDep = RelaxedDep !RelaxDepScope !RelaxDepMod !PackageName
deriving (Eq, Read, Show, Generic)
-- | Specify the scope of a relaxation, i.e. limit which depending
-- packages are allowed to have their version constraints relaxed.
data RelaxDepScope = RelaxDepScopeAll
-- ^ Apply relaxation in any package
| RelaxDepScopePackage !PackageName
-- ^ Apply relaxation to in all versions of a package
| RelaxDepScopePackageId !PackageId
-- ^ Apply relaxation to a specific version of a package only
deriving (Eq, Read, Show, Generic)
-- | Modifier for dependency relaxation
data RelaxDepMod = RelaxDepModNone -- ^ Default semantics
| RelaxDepModCaret -- ^ Apply relaxation only to @^>=@ constraints
deriving (Eq, Read, Show, Generic)
instance Text RelaxedDep where
disp (RelaxedDep p0) = disp p0
disp (RelaxedDepScoped p0 p1) = disp p0 Disp.<> Disp.colon Disp.<> disp p1
disp (RelaxedDep scope rdmod dep) = case scope of
RelaxDepScopeAll -> modDep
RelaxDepScopePackage p0 -> disp p0 Disp.<> Disp.colon Disp.<> modDep
RelaxDepScopePackageId p0 -> disp p0 Disp.<> Disp.colon Disp.<> modDep
where
modDep = case rdmod of
RelaxDepModNone -> disp dep
RelaxDepModCaret -> Disp.char '^' Disp.<> disp dep
parse = scopedP Parse.<++ normalP
parse = RelaxedDep <$> scopeP <*> modP <*> parse
where
scopedP = RelaxedDepScoped `fmap` parse <* Parse.char ':' <*> parse
normalP = RelaxedDep `fmap` parse
-- "greedy" choices
scopeP = (pure RelaxDepScopeAll <* Parse.char '*' <* Parse.char ':')
Parse.<++ (pure RelaxDepScopeAll <* Parse.string "all:")
Parse.<++ (RelaxDepScopePackageId <$> pidP <* Parse.char ':')
Parse.<++ (RelaxDepScopePackage <$> parse <* Parse.char ':')
Parse.<++ (pure RelaxDepScopeAll)
modP = (pure RelaxDepModCaret <* Parse.char '^')
Parse.<++ (pure RelaxDepModNone)
-- | Stricter 'PackageId' parser which doesn't overlap with 'PackageName' parser
pidP = do
p0 <- parse
when (pkgVersion p0 == nullVersion) Parse.pfail
pure p0
instance Binary RelaxDeps
instance Binary RelaxDepMod
instance Binary RelaxDepScope
instance Binary RelaxedDep
instance Binary AllowNewer
instance Binary AllowOlder
......
......@@ -5,6 +5,8 @@
roll back the index to an earlier state.
* 'cabal new-configure' now backs up the old 'cabal.project.local'
file if it exists (#4460).
* Enhance '--allow-newer' syntax to allow more fine-grained scoping
control (#4575).
2.0.0.0 Ryan Thomas <ryan@ryant.org> May 2017
* Removed the '--root-cmd' parameter of the 'install' command
......
......@@ -16,6 +16,7 @@ import Distribution.PackageDescription hiding (Flag)
import Distribution.Compiler
import Distribution.Version
import Distribution.ParseUtils
import Distribution.Text as Text
import Distribution.Simple.Compiler
import Distribution.Simple.Setup
import Distribution.Simple.InstallDirs
......@@ -63,6 +64,7 @@ tests =
, testGroup "individual parser tests"
[ testProperty "package location" prop_parsePackageLocationTokenQ
, testProperty "RelaxedDep" prop_roundtrip_printparse_RelaxedDep
]
, testGroup "ProjectConfig printing/parsing round trip"
......@@ -239,6 +241,12 @@ prop_parsePackageLocationTokenQ (PackageLocationString str) =
_ -> False
prop_roundtrip_printparse_RelaxedDep :: RelaxedDep -> Bool
prop_roundtrip_printparse_RelaxedDep rdep =
case [ x | (x,"") <- Parse.readP_to_S Text.parse (Text.display rdep) ] of
[rdep'] -> rdep' == rdep
_ -> False
------------------------
-- Arbitrary instances
--
......@@ -776,11 +784,18 @@ instance Arbitrary RelaxDeps where
, pure RelaxDepsAll
]
instance Arbitrary RelaxedDep where
arbitrary = oneof [ RelaxedDep <$> arbitrary
, RelaxedDepScoped <$> arbitrary <*> arbitrary
instance Arbitrary RelaxDepMod where
arbitrary = elements [RelaxDepModNone, RelaxDepModCaret]
instance Arbitrary RelaxDepScope where
arbitrary = oneof [ pure RelaxDepScopeAll
, RelaxDepScopePackage <$> arbitrary
, RelaxDepScopePackageId <$> (PackageIdentifier <$> arbitrary <*> arbitrary)
]
instance Arbitrary RelaxedDep where
arbitrary = RelaxedDep <$> arbitrary <*> arbitrary <*> arbitrary
instance Arbitrary ProfDetailLevel where
arbitrary = elements [ d | (_,_,d) <- knownProfDetailLevels ]
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment