-
Notifications
You must be signed in to change notification settings - Fork 703
/
Copy pathPreference.hs
481 lines (436 loc) · 20.6 KB
/
Preference.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
{-# LANGUAGE ScopedTypeVariables #-}
-- | Reordering or pruning the tree in order to prefer or make certain choices.
module Distribution.Solver.Modular.Preference
( avoidReinstalls
, deferSetupChoices
, deferWeakFlagChoices
, enforceManualFlags
, enforcePackageConstraints
, enforceSingleInstanceRestriction
, firstGoal
, preferBaseGoalChoice
, preferLinked
, preferPackagePreferences
, preferReallyEasyGoalChoices
, requireInstalled
, onlyConstrained
, sortGoals
, pruneAfterFirstSuccess
) where
import Prelude ()
import Distribution.Solver.Compat.Prelude
import Data.Function (on)
import qualified Data.List as L
import qualified Data.Map as M
import Control.Monad.Reader hiding (sequence)
import Data.Traversable (sequence)
import Distribution.PackageDescription (lookupFlagAssignment, unFlagAssignment) -- from Cabal
import Distribution.Solver.Types.Flag
import Distribution.Solver.Types.InstalledPreference
import Distribution.Solver.Types.LabeledPackageConstraint
import Distribution.Solver.Types.OptionalStanza
import Distribution.Solver.Types.PackageConstraint
import Distribution.Solver.Types.PackagePath
import Distribution.Solver.Types.PackagePreferences
import Distribution.Solver.Types.Variable
import Distribution.Solver.Modular.Dependency
import Distribution.Solver.Modular.Flag
import Distribution.Solver.Modular.Package
import qualified Distribution.Solver.Modular.PSQ as P
import Distribution.Solver.Modular.Tree
import Distribution.Solver.Modular.Version
import qualified Distribution.Solver.Modular.ConflictSet as CS
import qualified Distribution.Solver.Modular.WeightedPSQ as W
-- | Update the weights of children under 'PChoice' nodes. 'addWeights' takes a
-- list of weight-calculating functions in order to avoid sorting the package
-- choices multiple times. Each function takes the package name, sorted list of
-- children's versions, and package option. 'addWeights' prepends the new
-- weights to the existing weights, which gives precedence to preferences that
-- are applied later.
addWeights :: [PN -> [Ver] -> POption -> Weight] -> Tree d c -> Tree d c
addWeights fs = trav go
where
go :: TreeF d c (Tree d c) -> TreeF d c (Tree d c)
go (PChoiceF qpn@(Q _ pn) rdm x cs) =
let sortedVersions = L.sortBy (flip compare) $ L.map version (W.keys cs)
weights k = [f pn sortedVersions k | f <- fs]
elemsToWhnf :: [a] -> ()
elemsToWhnf = foldr seq ()
in PChoiceF qpn rdm x
-- Evaluate the children's versions before evaluating any of the
-- subtrees, so that 'sortedVersions' doesn't hold onto all of the
-- subtrees (referenced by cs) and cause a space leak.
(elemsToWhnf sortedVersions `seq`
W.mapWeightsWithKey (\k w -> weights k ++ w) cs)
go x = x
addWeight :: (PN -> [Ver] -> POption -> Weight) -> Tree d c -> Tree d c
addWeight f = addWeights [f]
version :: POption -> Ver
version (POption (I v _) _) = v
-- | Prefer to link packages whenever possible.
preferLinked :: Tree d c -> Tree d c
preferLinked = addWeight (const (const linked))
where
linked (POption _ Nothing) = 1
linked (POption _ (Just _)) = 0
-- Works by setting weights on choice nodes. Also applies stanza preferences.
preferPackagePreferences :: (PN -> PackagePreferences) -> Tree d c -> Tree d c
preferPackagePreferences pcs =
preferPackageStanzaPreferences pcs .
addWeights [
\pn _ opt -> preferred pn opt
-- Note that we always rank installed before uninstalled, and later
-- versions before earlier, but we can change the priority of the
-- two orderings.
, \pn vs opt -> case preference pn of
PreferInstalled -> installed opt
PreferLatest -> latest vs opt
, \pn vs opt -> case preference pn of
PreferInstalled -> latest vs opt
PreferLatest -> installed opt
]
where
-- Prefer packages with higher version numbers over packages with
-- lower version numbers.
latest :: [Ver] -> POption -> Weight
latest sortedVersions opt =
let l = length sortedVersions
index = fromMaybe l $ L.findIndex (<= version opt) sortedVersions
in fromIntegral index / fromIntegral l
preference :: PN -> InstalledPreference
preference pn =
let PackagePreferences _ ipref _ = pcs pn
in ipref
-- | Prefer versions satisfying more preferred version ranges.
preferred :: PN -> POption -> Weight
preferred pn opt =
let PackagePreferences vrs _ _ = pcs pn
in fromIntegral . negate . L.length $
L.filter (flip checkVR (version opt)) vrs
-- Prefer installed packages over non-installed packages.
installed :: POption -> Weight
installed (POption (I _ (Inst _)) _) = 0
installed _ = 1
-- | Traversal that tries to establish package stanza enable\/disable
-- preferences. Works by reordering the branches of stanza choices.
preferPackageStanzaPreferences :: (PN -> PackagePreferences) -> Tree d c -> Tree d c
preferPackageStanzaPreferences pcs = trav go
where
go (SChoiceF qsn@(SN (Q pp pn) s) rdm gr _tr ts)
| primaryPP pp && enableStanzaPref pn s =
-- move True case first to try enabling the stanza
let ts' = W.mapWeightsWithKey (\k w -> weight k : w) ts
weight k = if k then 0 else 1
-- defer the choice by setting it to weak
in SChoiceF qsn rdm gr (WeakOrTrivial True) ts'
go x = x
enableStanzaPref :: PN -> OptionalStanza -> Bool
enableStanzaPref pn s =
let PackagePreferences _ _ spref = pcs pn
in s `elem` spref
-- | Helper function that tries to enforce a single package constraint on a
-- given instance for a P-node. Translates the constraint into a
-- tree-transformer that either leaves the subtree untouched, or replaces it
-- with an appropriate failure node.
processPackageConstraintP :: forall d c. QPN
-> ConflictSet
-> I
-> LabeledPackageConstraint
-> Tree d c
-> Tree d c
processPackageConstraintP qpn c i (LabeledPackageConstraint (PackageConstraint scope prop) src) r =
if constraintScopeMatches scope qpn
then go i prop
else r
where
go :: I -> PackageProperty -> Tree d c
go (I v _) (PackagePropertyVersion vr)
| checkVR vr v = r
| otherwise = Fail c (GlobalConstraintVersion vr src)
go _ PackagePropertyInstalled
| instI i = r
| otherwise = Fail c (GlobalConstraintInstalled src)
go _ PackagePropertySource
| not (instI i) = r
| otherwise = Fail c (GlobalConstraintSource src)
go _ _ = r
-- | Helper function that tries to enforce a single package constraint on a
-- given flag setting for an F-node. Translates the constraint into a
-- tree-transformer that either leaves the subtree untouched, or replaces it
-- with an appropriate failure node.
processPackageConstraintF :: forall d c. QPN
-> Flag
-> ConflictSet
-> Bool
-> LabeledPackageConstraint
-> Tree d c
-> Tree d c
processPackageConstraintF qpn f c b' (LabeledPackageConstraint (PackageConstraint scope prop) src) r =
if constraintScopeMatches scope qpn
then go prop
else r
where
go :: PackageProperty -> Tree d c
go (PackagePropertyFlags fa) =
case lookupFlagAssignment f fa of
Nothing -> r
Just b | b == b' -> r
| otherwise -> Fail c (GlobalConstraintFlag src)
go _ = r
-- | Helper function that tries to enforce a single package constraint on a
-- given flag setting for an F-node. Translates the constraint into a
-- tree-transformer that either leaves the subtree untouched, or replaces it
-- with an appropriate failure node.
processPackageConstraintS :: forall d c. QPN
-> OptionalStanza
-> ConflictSet
-> Bool
-> LabeledPackageConstraint
-> Tree d c
-> Tree d c
processPackageConstraintS qpn s c b' (LabeledPackageConstraint (PackageConstraint scope prop) src) r =
if constraintScopeMatches scope qpn
then go prop
else r
where
go :: PackageProperty -> Tree d c
go (PackagePropertyStanzas ss) =
if not b' && s `elem` ss then Fail c (GlobalConstraintFlag src)
else r
go _ = r
-- | Traversal that tries to establish various kinds of user constraints. Works
-- by selectively disabling choices that have been ruled out by global user
-- constraints.
enforcePackageConstraints :: M.Map PN [LabeledPackageConstraint]
-> Tree d c
-> Tree d c
enforcePackageConstraints pcs = trav go
where
go (PChoiceF qpn@(Q _ pn) rdm gr ts) =
let c = varToConflictSet (P qpn)
-- compose the transformation functions for each of the relevant constraint
g = \ (POption i _) -> foldl (\ h pc -> h . processPackageConstraintP qpn c i pc)
id
(M.findWithDefault [] pn pcs)
in PChoiceF qpn rdm gr (W.mapWithKey g ts)
go (FChoiceF qfn@(FN qpn@(Q _ pn) f) rdm gr tr m d ts) =
let c = varToConflictSet (F qfn)
-- compose the transformation functions for each of the relevant constraint
g = \ b -> foldl (\ h pc -> h . processPackageConstraintF qpn f c b pc)
id
(M.findWithDefault [] pn pcs)
in FChoiceF qfn rdm gr tr m d (W.mapWithKey g ts)
go (SChoiceF qsn@(SN qpn@(Q _ pn) f) rdm gr tr ts) =
let c = varToConflictSet (S qsn)
-- compose the transformation functions for each of the relevant constraint
g = \ b -> foldl (\ h pc -> h . processPackageConstraintS qpn f c b pc)
id
(M.findWithDefault [] pn pcs)
in SChoiceF qsn rdm gr tr (W.mapWithKey g ts)
go x = x
-- | Transformation that tries to enforce the rule that manual flags can only be
-- set by the user.
--
-- If there are no constraints on a manual flag, this function prunes all but
-- the default value. If there are constraints, then the flag is allowed to have
-- the values specified by the constraints. Note that the type used for flag
-- values doesn't need to be Bool.
--
-- This function makes an exception for the case where there are multiple goals
-- for a single package (with different qualifiers), and flag constraints for
-- manual flag x only apply to some of those goals. In that case, we allow the
-- unconstrained goals to use the default value for x OR any of the values in
-- the constraints on x (even though the constraints don't apply), in order to
-- allow the unconstrained goals to be linked to the constrained goals. See
-- https://github.com/haskell/cabal/issues/4299. Removing the single instance
-- restriction (SIR) would also fix #4299, so we may want to remove this
-- exception and only let the user toggle manual flags if we remove the SIR.
--
-- This function does not enforce any of the constraints, since that is done by
-- 'enforcePackageConstraints'.
enforceManualFlags :: M.Map PN [LabeledPackageConstraint] -> Tree d c -> Tree d c
enforceManualFlags pcs = trav go
where
go (FChoiceF qfn@(FN (Q _ pn) fn) rdm gr tr Manual d ts) =
FChoiceF qfn rdm gr tr Manual d $
let -- A list of all values specified by constraints on 'fn'.
-- We ignore the constraint scope in order to handle issue #4299.
flagConstraintValues :: [Bool]
flagConstraintValues =
[ flagVal
| let lpcs = M.findWithDefault [] pn pcs
, (LabeledPackageConstraint (PackageConstraint _ (PackagePropertyFlags fa)) _) <- lpcs
, (fn', flagVal) <- unFlagAssignment fa
, fn' == fn ]
-- Prune flag values that are not the default and do not match any
-- of the constraints.
restrictToggling :: Eq a => a -> [a] -> a -> Tree d c -> Tree d c
restrictToggling flagDefault constraintVals flagVal r =
if flagVal `elem` constraintVals || flagVal == flagDefault
then r
else Fail (varToConflictSet (F qfn)) ManualFlag
in W.mapWithKey (restrictToggling d flagConstraintValues) ts
go x = x
-- | Require installed packages.
requireInstalled :: (PN -> Bool) -> Tree d c -> Tree d c
requireInstalled p = trav go
where
go (PChoiceF v@(Q _ pn) rdm gr cs)
| p pn = PChoiceF v rdm gr (W.mapWithKey installed cs)
| otherwise = PChoiceF v rdm gr cs
where
installed (POption (I _ (Inst _)) _) x = x
installed _ _ = Fail (varToConflictSet (P v)) CannotInstall
go x = x
-- | Avoid reinstalls.
--
-- This is a tricky strategy. If a package version is installed already and the
-- same version is available from a repo, the repo version will never be chosen.
-- This would result in a reinstall (either destructively, or potentially,
-- shadowing). The old instance won't be visible or even present anymore, but
-- other packages might have depended on it.
--
-- TODO: It would be better to actually check the reverse dependencies of installed
-- packages. If they're not depended on, then reinstalling should be fine. Even if
-- they are, perhaps this should just result in trying to reinstall those other
-- packages as well. However, doing this all neatly in one pass would require to
-- change the builder, or at least to change the goal set after building.
avoidReinstalls :: (PN -> Bool) -> Tree d c -> Tree d c
avoidReinstalls p = trav go
where
go (PChoiceF qpn@(Q _ pn) rdm gr cs)
| p pn = PChoiceF qpn rdm gr disableReinstalls
| otherwise = PChoiceF qpn rdm gr cs
where
disableReinstalls =
let installed = [ v | (_, POption (I v (Inst _)) _, _) <- W.toList cs ]
in W.mapWithKey (notReinstall installed) cs
notReinstall vs (POption (I v InRepo) _) _ | v `elem` vs =
Fail (varToConflictSet (P qpn)) CannotReinstall
notReinstall _ _ x =
x
go x = x
-- | Require all packages to be mentioned in a constraint or as a goal.
onlyConstrained :: (PN -> Bool) -> Tree d QGoalReason -> Tree d QGoalReason
onlyConstrained p = trav go
where
go (PChoiceF v@(Q _ pn) _ gr _) | not (p pn)
= FailF (varToConflictSet (P v) `CS.union` goalReasonToCS gr) NotExplicit
go x
= x
-- | Sort all goals using the provided function.
sortGoals :: (Variable QPN -> Variable QPN -> Ordering) -> Tree d c -> Tree d c
sortGoals variableOrder = trav go
where
go (GoalChoiceF rdm xs) = GoalChoiceF rdm (P.sortByKeys goalOrder xs)
go x = x
goalOrder :: Goal QPN -> Goal QPN -> Ordering
goalOrder = variableOrder `on` (varToVariable . goalToVar)
varToVariable :: Var QPN -> Variable QPN
varToVariable (P qpn) = PackageVar qpn
varToVariable (F (FN qpn fn)) = FlagVar qpn fn
varToVariable (S (SN qpn stanza)) = StanzaVar qpn stanza
-- | Reduce the branching degree of the search tree by removing all choices
-- after the first successful choice at each level. The returned tree is the
-- minimal subtree containing the path to the first backjump.
pruneAfterFirstSuccess :: Tree d c -> Tree d c
pruneAfterFirstSuccess = trav go
where
go (PChoiceF qpn rdm gr ts) = PChoiceF qpn rdm gr (W.takeUntil active ts)
go (FChoiceF qfn rdm gr w m d ts) = FChoiceF qfn rdm gr w m d (W.takeUntil active ts)
go (SChoiceF qsn rdm gr w ts) = SChoiceF qsn rdm gr w (W.takeUntil active ts)
go x = x
-- | Always choose the first goal in the list next, abandoning all
-- other choices.
--
-- This is unnecessary for the default search strategy, because
-- it descends only into the first goal choice anyway,
-- but may still make sense to just reduce the tree size a bit.
firstGoal :: Tree d c -> Tree d c
firstGoal = trav go
where
go (GoalChoiceF rdm xs) = GoalChoiceF rdm (P.firstOnly xs)
go x = x
-- Note that we keep empty choice nodes, because they mean success.
-- | Transformation that tries to make a decision on base as early as
-- possible by pruning all other goals when base is available. In nearly
-- all cases, there's a single choice for the base package. Also, fixing
-- base early should lead to better error messages.
preferBaseGoalChoice :: Tree d c -> Tree d c
preferBaseGoalChoice = trav go
where
go (GoalChoiceF rdm xs) = GoalChoiceF rdm (P.filterIfAnyByKeys isBase xs)
go x = x
isBase :: Goal QPN -> Bool
isBase (Goal (P (Q _pp pn)) _) = unPN pn == "base"
isBase _ = False
-- | Deal with setup dependencies after regular dependencies, so that we can
-- will link setup dependencies against package dependencies when possible
deferSetupChoices :: Tree d c -> Tree d c
deferSetupChoices = trav go
where
go (GoalChoiceF rdm xs) = GoalChoiceF rdm (P.preferByKeys noSetup xs)
go x = x
noSetup :: Goal QPN -> Bool
noSetup (Goal (P (Q (PackagePath _ns (QualSetup _)) _)) _) = False
noSetup _ = True
-- | Transformation that tries to avoid making weak flag choices early.
-- Weak flags are trivial flags (not influencing dependencies) or such
-- flags that are explicitly declared to be weak in the index.
deferWeakFlagChoices :: Tree d c -> Tree d c
deferWeakFlagChoices = trav go
where
go (GoalChoiceF rdm xs) = GoalChoiceF rdm (P.prefer noWeakFlag (P.prefer noWeakStanza xs))
go x = x
noWeakStanza :: Tree d c -> Bool
noWeakStanza (SChoice _ _ _ (WeakOrTrivial True) _) = False
noWeakStanza _ = True
noWeakFlag :: Tree d c -> Bool
noWeakFlag (FChoice _ _ _ (WeakOrTrivial True) _ _ _) = False
noWeakFlag _ = True
-- | Transformation that prefers goals with lower branching degrees.
--
-- When a goal choice node has at least one goal with zero or one children, this
-- function prunes all other goals. This transformation can help the solver find
-- a solution in fewer steps by allowing it to backtrack sooner when it is
-- exploring a subtree with no solutions. However, each step is more expensive.
preferReallyEasyGoalChoices :: Tree d c -> Tree d c
preferReallyEasyGoalChoices = trav go
where
go (GoalChoiceF rdm xs) = GoalChoiceF rdm (P.filterIfAny zeroOrOneChoices xs)
go x = x
-- | Monad used internally in enforceSingleInstanceRestriction
--
-- For each package instance we record the goal for which we picked a concrete
-- instance. The SIR means that for any package instance there can only be one.
type EnforceSIR = Reader (Map (PI PN) QPN)
-- | Enforce ghc's single instance restriction
--
-- From the solver's perspective, this means that for any package instance
-- (that is, package name + package version) there can be at most one qualified
-- goal resolving to that instance (there may be other goals _linking_ to that
-- instance however).
enforceSingleInstanceRestriction :: Tree d c -> Tree d c
enforceSingleInstanceRestriction = (`runReader` M.empty) . cata go
where
go :: TreeF d c (EnforceSIR (Tree d c)) -> EnforceSIR (Tree d c)
-- We just verify package choices.
go (PChoiceF qpn rdm gr cs) =
PChoice qpn rdm gr <$> sequence (W.mapWithKey (goP qpn) cs)
go _otherwise =
innM _otherwise
-- The check proper
goP :: QPN -> POption -> EnforceSIR (Tree d c) -> EnforceSIR (Tree d c)
goP qpn@(Q _ pn) (POption i linkedTo) r = do
let inst = PI pn i
env <- ask
case (linkedTo, M.lookup inst env) of
(Just _, _) ->
-- For linked nodes we don't check anything
r
(Nothing, Nothing) ->
-- Not linked, not already used
local (M.insert inst qpn) r
(Nothing, Just qpn') -> do
-- Not linked, already used. This is an error
return $ Fail (CS.union (varToConflictSet (P qpn)) (varToConflictSet (P qpn'))) MultipleInstances