Skip to content

Commit f858f25

Browse files
mpickeringalt-romes
andcommitted
feature: Private Dependencies
This commit introduces so-called "private dependencies". High-level Overview ~~~~~~~~~~~~~~~~~~~ Since its inception, Cabal has enforced the restriction that a library must only link against one version of each package it depends on. This ensures that all of the dependencies in the build plan work together. In your application you use different libraries together, so it’s of paramount importance that they all agree on what `Text` means or what a `ByteString` is. However, sometimes it’s desirable to allow multiple versions of the same library into a build plan. In this case, it’s desirable to allow a library author to specify a private dependency with the promise that its existence will not leak from the interface of the library which uses it. The two main use cases of private dependencies are: - Writing benchmarks and testsuites for your library which test new versions of your library against old versions. - Writing libraries which can communicate with processes built against a range of different library versions (such as cabal-install calling ./Setup). A user specifies a private dependency in their cabal file using `private-build-depends`. The specification starts with the name of the private dependency scope and then contains a list of normal dependency specifications which dictates what is included in that private scope: private-build-depends: TEXT1 with (text == 1.2.*), TEXT2 with (text == 2.*) Each private scope is then solved independently of all other scopes. In this example the TEXT1 scope can choose a version of text in the 1.2.x range and the TEXT2 scope can choose a version of text in the 2.* range. Private scopes do not apply transitively, so the dependencies of text will be solved in the normal top-level scope. If your program contains a value of type Bool, that comes from the base package, which text depends on, because the scopes are not applied transitively the same Bool value can be passed to functions from the TEXT1 scope and TEXT2 scope. Dependencies introduced privately can be imported into modules in the project by prefixing the name of the private scope to an exposed module name. import qualified TEXT1.Data.Text as T1 import qualified TEXT2.Data.Text as T2 Closure of Private Scopes ~~~~~~~~~~~~~~~~~~~~~~~~~ Private dependency scopes can contain multiple packages. Packages in the same scope are solved together. For example, if two packages are tightly coupled and you need to use compatible versions with each other, then you can list them in the same private scope. Such packages will then be solved together, but independently of other packages. Private scopes must be closed. A scope is closed if, whenever we have a dependency chain P1 -> Q -> P2, in which P1 and P2 are both in a given private scope S, then Q also belongs to the private scope S. The solver checks this property, but doesn’t implicitly add packages into a private scope. Implementation ~~~~~~~~~~~~~~ To implement private dependencies we changed * Cabal-syntax to introduce the new `private-build-depends: ALIAS (packages, in, private, scope)` syntax. See the new type `Dependencies` and changes in `Distribution.Types.Dependency`. * cabal-install-solver now considers both public and private dependencies of a given package (see e.g. `solverPkgLibDeps`), has a new constructor `PrivateScope` in `ConstraintScope` for goals in a private scope, and there's a new `Qualifier` for packages introduced in private scopes (see also [Namespace vs Qualifier refactor] below), to solve them separately from packages introduced by `build-depends`. * cabal-install-solver needs to check that the private-scope closure property holds (the closure of the packages in a private scope is in the private scope) (see `Distribution.Solver.Modular.PrivateScopeClosure`). We check that the closure holds by looking at the reverse dependency map while traversing down the tree, at every node: For every package in a private scope, traverse up the reverse dependency map until a package in the same private scope is found. If one exists, and if along the way up any package was not in the same private scope as the packages in the two ends, we fail. * cabal-install understands plans with private dependencies and has a new `UserQualifier` to support constrainting packages in private scopes using the `--constraint` flag. Example: `--constraint=private.pkg-a.TEXT01:text == 1.2.*` * Cabal the library uses the ghc module-renaming mechanism (also used by Backpack) to rename modules from the packages in a private scope to prefix them with the private scope alias. It also ensures `cabal check` fails if there exist the package has private dependencies, as it is currently an experimental feature which we don't necessarily want to allow in hackage yet -- e.g. how will haddock render private dependencies? Namespace vs Qualifier refactor ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We also refactored the `Namespace` vs `Qualifier` types in the solver, clarifying their interaction such that: * A package goal with an indepedent namespace is fully solved indepently from other namespaces, i.e. all the dependency goals introduced by a goal in a given namespace are also solved in that namespace. * In contrast, a package goal with a qualifier is shallow-solved separately from other goals in the same namespace. The dependency goals introduced by it will be solved unqualified (`QualTopLevel`) in that namespace. For example, goal `pkg-a == 0.1` in `DefaultNamespace+QualTopLevel`, and goal `pkg-a == 0.2` in the same namespace but with `QualAlias A2 ...` can be solved together and yield a different version of pkg-a for each of the goals, however, the dependencies of both will be solved together -- if they both dependend on `base`, we'd have to find a single solution. If `pkg-a == 0.2` was in an `Independent` namespace, we could still solve the two goals with two versions of `pkg-a`, but we could also pick different versions for all the subdependencies of `pkg-a == 0.2`. Besides Namespace vs Qualifier being a welcome refactor that facilitates implementing private dependencies, it also fixes haskell#9466 and helps with haskell#9467. --- Co-authored-by: Rodrigo Mesquita <[email protected]>
1 parent 00835c0 commit f858f25

File tree

196 files changed

+4488
-2416
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

196 files changed

+4488
-2416
lines changed

Cabal-QuickCheck/src/Test/QuickCheck/Instances/Cabal.hs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,23 @@ instance Arbitrary Dependency where
208208
| (pn', vr', lb') <- shrink (pn, vr, lb)
209209
]
210210

211+
-------------------------------------------------------------------------------
212+
-- Private Dependency
213+
-------------------------------------------------------------------------------
214+
215+
instance Arbitrary PrivateAlias where
216+
arbitrary = PrivateAlias <$> arbitrary
217+
shrink (PrivateAlias al) = PrivateAlias <$> shrink al
218+
instance Arbitrary PrivateDependency where
219+
arbitrary = PrivateDependency
220+
<$> arbitrary
221+
<*> arbitrary
222+
223+
shrink (PrivateDependency al dps) =
224+
[ PrivateDependency al' dps'
225+
| (al', dps') <- shrink (al, dps)
226+
]
227+
211228
-------------------------------------------------------------------------------
212229
-- PackageVersionConstraint
213230
-------------------------------------------------------------------------------

Cabal-described/src/Distribution/Described.hs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ import Distribution.Types.AbiDependency (AbiDependency)
7676
import Distribution.Types.AbiHash (AbiHash)
7777
import Distribution.Types.BenchmarkType (BenchmarkType)
7878
import Distribution.Types.BuildType (BuildType)
79-
import Distribution.Types.Dependency (Dependency)
79+
import Distribution.Types.Dependency (Dependency, PrivateAlias(..), PrivateDependency)
8080
import Distribution.Types.ExecutableScope (ExecutableScope)
8181
import Distribution.Types.ExeDependency (ExeDependency)
8282
import Distribution.Types.ExposedModule (ExposedModule)
@@ -391,6 +391,19 @@ instance Described Dependency where
391391
where
392392
vr = RENamed "version-range" (describe (Proxy :: Proxy VersionRange))
393393

394+
instance Described PrivateDependency where
395+
describe _ = REAppend
396+
[ RENamed "alias" (describe (Proxy :: Proxy PrivateAlias))
397+
, RESpaces1
398+
, "with"
399+
, RESpaces1
400+
, reChar '('
401+
, RESpaces
402+
, REMunch reSpacedComma (describe (Proxy :: Proxy Dependency))
403+
, RESpaces
404+
, reChar ')'
405+
]
406+
394407
instance Described ExecutableScope where
395408
describe _ = REUnion ["public","private"]
396409

@@ -446,6 +459,9 @@ instance Described ModuleName where
446459
describe _ = REMunch1 (reChar '.') component where
447460
component = RECharSet csUpper <> REMunch reEps (REUnion [RECharSet csAlphaNum, RECharSet (fromString "_'")])
448461

462+
instance Described PrivateAlias where
463+
describe _ = describe (Proxy :: Proxy ModuleName)
464+
449465
instance Described ModuleReexport where
450466
describe _ = RETodo
451467

Cabal-syntax/Cabal-syntax.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ library
135135
Distribution.Types.Condition
136136
Distribution.Types.ConfVar
137137
Distribution.Types.Dependency
138+
Distribution.Types.Dependency.Lens
138139
Distribution.Types.DependencyMap
139140
Distribution.Types.ExeDependency
140141
Distribution.Types.Executable

Cabal-syntax/src/Distribution/ModuleName.hs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module Distribution.ModuleName
1818
( ModuleName
1919
, fromString
2020
, fromComponents
21+
, combineModuleName
2122
, components
2223
, toFilePath
2324
, main
@@ -99,12 +100,6 @@ validModuleComponent (c : cs) = isUpper c && all validModuleChar cs
99100
instance IsString ModuleName where
100101
fromString = ModuleName . toShortText
101102

102-
-- | Construct a 'ModuleName' from valid module components, i.e. parts
103-
-- separated by dots.
104-
fromComponents :: [String] -> ModuleName
105-
fromComponents comps = fromString (intercalate "." comps)
106-
{-# DEPRECATED fromComponents "Exists for cabal-install only" #-}
107-
108103
-- | The module name @Main@.
109104
main :: ModuleName
110105
main = ModuleName (fromString "Main")
@@ -119,6 +114,19 @@ components mn = split (unModuleName mn)
119114
(chunk, []) -> chunk : []
120115
(chunk, _ : rest) -> chunk : split rest
121116

117+
-- | Construct a 'ModuleName' from valid module components, i.e. parts
118+
-- separated by dots.
119+
--
120+
-- Inverse of 'components', i.e. @fromComponents (components x) = x@
121+
fromComponents :: [String] -> ModuleName
122+
fromComponents comps = fromString (intercalate "." comps)
123+
{-# DEPRECATED fromComponents "Exists for cabal-install only" #-}
124+
125+
-- | Append one valid module name onto another valid module name
126+
-- This is used when adding the module suffix to private dependencies
127+
combineModuleName :: ModuleName -> ModuleName -> ModuleName
128+
combineModuleName mn1 mn2 = fromComponents (components mn1 ++ components mn2)
129+
122130
-- | Convert a module name to a file path, but without any file extension.
123131
-- For example:
124132
--

Cabal-syntax/src/Distribution/PackageDescription/Configuration.hs

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ module Distribution.PackageDescription.Configuration
3131
, mapTreeConstrs
3232
, transformAllBuildInfos
3333
, transformAllBuildDepends
34-
, transformAllBuildDependsN
3534
, simplifyWithSysParams
3635
) where
3736

@@ -63,6 +62,7 @@ import Distribution.Version
6362

6463
import qualified Data.Map.Lazy as Map
6564
import Data.Tree (Tree (Node))
65+
import qualified Distribution.Types.Dependency.Lens as L
6666

6767
------------------------------------------------------------------------------
6868

@@ -189,10 +189,10 @@ resolveWithFlags
189189
-- ^ Compiler information
190190
-> [PackageVersionConstraint]
191191
-- ^ Additional constraints
192-
-> [CondTree ConfVar [Dependency] PDTagged]
193-
-> ([Dependency] -> DepTestRslt [Dependency])
192+
-> [CondTree ConfVar Dependencies PDTagged]
193+
-> (Dependencies -> DepTestRslt Dependencies)
194194
-- ^ Dependency test function.
195-
-> Either [Dependency] (TargetSet PDTagged, FlagAssignment)
195+
-> Either Dependencies (TargetSet PDTagged, FlagAssignment)
196196
-- ^ Either the missing dependencies (error case), or a pair of
197197
-- (set of build targets with dependencies, chosen flag assignments)
198198
resolveWithFlags dom enabled os arch impl constrs trees checkDeps =
@@ -324,7 +324,7 @@ extractConditions f gpkg =
324324
]
325325

326326
-- | A map of package constraints that combines version ranges using 'unionVersionRanges'.
327-
newtype DepMapUnion = DepMapUnion {unDepMapUnion :: Map PackageName (VersionRange, NonEmptySet LibraryName)}
327+
newtype DepMapUnion = DepMapUnion {unDepMapUnion :: Map (PackageName, IsPrivate) (VersionRange, NonEmptySet LibraryName)}
328328

329329
instance Semigroup DepMapUnion where
330330
DepMapUnion x <> DepMapUnion y =
@@ -337,12 +337,22 @@ unionVersionRanges'
337337
-> (VersionRange, NonEmptySet LibraryName)
338338
unionVersionRanges' (vr, cs) (vr', cs') = (unionVersionRanges vr vr', cs <> cs')
339339

340-
toDepMapUnion :: [Dependency] -> DepMapUnion
340+
toDepMapUnion :: Dependencies -> DepMapUnion
341341
toDepMapUnion ds =
342-
DepMapUnion $ Map.fromListWith unionVersionRanges' [(p, (vr, cs)) | Dependency p vr cs <- ds]
342+
DepMapUnion $
343+
Map.fromListWith
344+
unionVersionRanges'
345+
( [((p, Public), (vr, cs)) | Dependency p vr cs <- publicDependencies ds]
346+
++ [((p, Private (private_alias d)), (vr, cs)) | d <- privateDependencies ds, Dependency p vr cs <- private_depends d]
347+
)
343348

344-
fromDepMapUnion :: DepMapUnion -> [Dependency]
345-
fromDepMapUnion m = [Dependency p vr cs | (p, (vr, cs)) <- Map.toList (unDepMapUnion m)]
349+
fromDepMapUnion :: DepMapUnion -> Dependencies
350+
fromDepMapUnion m =
351+
Dependencies
352+
[Dependency p vr cs | ((p, Public), (vr, cs)) <- Map.toList (unDepMapUnion m)]
353+
[PrivateDependency alias deps | (alias, deps) <- Map.toList priv_deps]
354+
where
355+
priv_deps = Map.fromListWith (++) [(sn, [Dependency p vr cs]) | ((p, Private sn), (vr, cs)) <- Map.toList (unDepMapUnion m)]
346356

347357
freeVars :: CondTree ConfVar c a -> [FlagName]
348358
freeVars t = [f | PackageFlag f <- freeVars' t]
@@ -400,8 +410,9 @@ flattenTaggedTargets (TargetSet targets) = foldr untag (Nothing, []) targets
400410
| otherwise -> (mb_lib, (n, redoBD c) : comps)
401411
(PDNull, x) -> x -- actually this should not happen, but let's be liberal
402412
where
413+
deps = fromDepMap depMap
403414
redoBD :: L.HasBuildInfo a => a -> a
404-
redoBD = set L.targetBuildDepends $ fromDepMap depMap
415+
redoBD = set L.targetPrivateBuildDepends (privateDependencies deps) . set L.targetBuildDepends (publicDependencies deps)
405416

406417
------------------------------------------------------------------------------
407418
-- Convert GenericPackageDescription to PackageDescription
@@ -453,7 +464,7 @@ finalizePD
453464
:: FlagAssignment
454465
-- ^ Explicitly specified flag assignments
455466
-> ComponentRequestedSpec
456-
-> (Dependency -> Bool)
467+
-> (Maybe PrivateAlias -> Dependency -> Bool)
457468
-- ^ Is a given dependency satisfiable from the set of
458469
-- available packages? If this is unknown then use
459470
-- True.
@@ -465,7 +476,7 @@ finalizePD
465476
-- ^ Additional constraints
466477
-> GenericPackageDescription
467478
-> Either
468-
[Dependency]
479+
Dependencies
469480
(PackageDescription, FlagAssignment)
470481
-- ^ Either missing dependencies or the resolved package
471482
-- description along with the flag assignments chosen.
@@ -526,8 +537,11 @@ finalizePD
526537
| otherwise -> [b, not b]
527538
-- flagDefaults = map (\(n,x:_) -> (n,x)) flagChoices
528539
check ds =
529-
let missingDeps = filter (not . satisfyDep) ds
530-
in if null missingDeps
540+
let missingDeps =
541+
Dependencies
542+
(filter (not . satisfyDep Nothing) (publicDependencies ds))
543+
(mapMaybe (\(PrivateDependency priv pds) -> case filter (not . satisfyDep (Just priv)) pds of [] -> Nothing; pds' -> Just (PrivateDependency priv pds')) (privateDependencies ds))
544+
in if null (publicDependencies missingDeps) && null (privateDependencies missingDeps)
531545
then DepOk
532546
else MissingDeps missingDeps
533547

@@ -652,19 +666,9 @@ transformAllBuildDepends
652666
-> GenericPackageDescription
653667
-> GenericPackageDescription
654668
transformAllBuildDepends f =
655-
over (L.traverseBuildInfos . L.targetBuildDepends . traverse) f
669+
over (L.traverseBuildInfos . L.targetPrivateBuildDepends . traverse . L.private_depends . traverse) f
670+
. over (L.traverseBuildInfos . L.targetBuildDepends . traverse) f
656671
. over (L.packageDescription . L.setupBuildInfo . traverse . L.setupDepends . traverse) f
657672
-- cannot be point-free as normal because of higher rank
658-
. over (\f' -> L.allCondTrees $ traverseCondTreeC f') (map f)
673+
. over (\f' -> L.allCondTrees $ traverseCondTreeC f') (mapDependencies f)
659674

660-
-- | Walk a 'GenericPackageDescription' and apply @f@ to all nested
661-
-- @build-depends@ fields.
662-
transformAllBuildDependsN
663-
:: ([Dependency] -> [Dependency])
664-
-> GenericPackageDescription
665-
-> GenericPackageDescription
666-
transformAllBuildDependsN f =
667-
over (L.traverseBuildInfos . L.targetBuildDepends) f
668-
. over (L.packageDescription . L.setupBuildInfo . traverse . L.setupDepends) f
669-
-- cannot be point-free as normal because of higher rank
670-
. over (\f' -> L.allCondTrees $ traverseCondTreeC f') f

Cabal-syntax/src/Distribution/PackageDescription/FieldGrammar.hs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ libraryFieldGrammar
171171
, c (List CommaFSep (Identity LegacyExeDependency) LegacyExeDependency)
172172
, c (List CommaFSep (Identity PkgconfigDependency) PkgconfigDependency)
173173
, c (List CommaVCat (Identity Dependency) Dependency)
174+
, c (List CommaVCat (Identity PrivateDependency) PrivateDependency)
174175
, c (List CommaVCat (Identity Mixin) Mixin)
175176
, c (List CommaVCat (Identity ModuleReexport) ModuleReexport)
176177
, c (List FSep (MQuoted Extension) Extension)
@@ -220,6 +221,7 @@ foreignLibFieldGrammar
220221
, c (List CommaFSep (Identity ExeDependency) ExeDependency)
221222
, c (List CommaFSep (Identity LegacyExeDependency) LegacyExeDependency)
222223
, c (List CommaFSep (Identity PkgconfigDependency) PkgconfigDependency)
224+
, c (List CommaVCat (Identity PrivateDependency) PrivateDependency)
223225
, c (List CommaVCat (Identity Dependency) Dependency)
224226
, c (List CommaVCat (Identity Mixin) Mixin)
225227
, c (List FSep (Identity ForeignLibOption) ForeignLibOption)
@@ -260,6 +262,7 @@ executableFieldGrammar
260262
, c (List CommaFSep (Identity LegacyExeDependency) LegacyExeDependency)
261263
, c (List CommaFSep (Identity PkgconfigDependency) PkgconfigDependency)
262264
, c (List CommaVCat (Identity Dependency) Dependency)
265+
, c (List CommaVCat (Identity PrivateDependency) PrivateDependency)
263266
, c (List CommaVCat (Identity Mixin) Mixin)
264267
, c (List FSep (MQuoted Extension) Extension)
265268
, c (List FSep (MQuoted Language) Language)
@@ -336,6 +339,7 @@ testSuiteFieldGrammar
336339
, c (List CommaFSep (Identity PkgconfigDependency) PkgconfigDependency)
337340
, c (List CommaFSep Token String)
338341
, c (List CommaVCat (Identity Dependency) Dependency)
342+
, c (List CommaVCat (Identity PrivateDependency) PrivateDependency)
339343
, c (List CommaVCat (Identity Mixin) Mixin)
340344
, c (List FSep (MQuoted Extension) Extension)
341345
, c (List FSep (MQuoted Language) Language)
@@ -480,6 +484,7 @@ benchmarkFieldGrammar
480484
, c (List CommaFSep (Identity LegacyExeDependency) LegacyExeDependency)
481485
, c (List CommaFSep (Identity PkgconfigDependency) PkgconfigDependency)
482486
, c (List CommaVCat (Identity Dependency) Dependency)
487+
, c (List CommaVCat (Identity PrivateDependency) PrivateDependency)
483488
, c (List CommaVCat (Identity Mixin) Mixin)
484489
, c (List FSep (MQuoted Extension) Extension)
485490
, c (List FSep (MQuoted Language) Language)
@@ -582,6 +587,7 @@ buildInfoFieldGrammar
582587
, c (List CommaFSep (Identity LegacyExeDependency) LegacyExeDependency)
583588
, c (List CommaFSep (Identity PkgconfigDependency) PkgconfigDependency)
584589
, c (List CommaVCat (Identity Dependency) Dependency)
590+
, c (List CommaVCat (Identity PrivateDependency) PrivateDependency)
585591
, c (List CommaVCat (Identity Mixin) Mixin)
586592
, c (List FSep (MQuoted Extension) Extension)
587593
, c (List FSep (MQuoted Language) Language)
@@ -676,6 +682,7 @@ buildInfoFieldGrammar =
676682
<*> pure mempty -- static-options ???
677683
<*> prefixedFields "x-" L.customFieldsBI
678684
<*> monoidalFieldAla "build-depends" formatDependencyList L.targetBuildDepends
685+
<*> monoidalFieldAla "private-build-depends" formatPrivateDependencyList L.targetPrivateBuildDepends
679686
<*> monoidalFieldAla "mixins" formatMixinList L.mixins
680687
^^^ availableSince CabalSpecV2_0 []
681688
{-# SPECIALIZE buildInfoFieldGrammar :: ParsecFieldGrammar' BuildInfo #-}
@@ -800,6 +807,9 @@ setupBInfoFieldGrammar def =
800807
formatDependencyList :: [Dependency] -> List CommaVCat (Identity Dependency) Dependency
801808
formatDependencyList = alaList CommaVCat
802809

810+
formatPrivateDependencyList :: [PrivateDependency] -> List CommaVCat (Identity PrivateDependency) PrivateDependency
811+
formatPrivateDependencyList = alaList CommaVCat
812+
803813
formatMixinList :: [Mixin] -> List CommaVCat (Identity Mixin) Mixin
804814
formatMixinList = alaList CommaVCat
805815

0 commit comments

Comments
 (0)