cabal-install-1.24.0.0: Distribution/Client/Dependency/Modular/Preference.hs
{-# LANGUAGE CPP #-}
module Distribution.Client.Dependency.Modular.Preference
( avoidReinstalls
, deferSetupChoices
, deferWeakFlagChoices
, enforceManualFlags
, enforcePackageConstraints
, enforceSingleInstanceRestriction
, firstGoal
, preferBaseGoalChoice
, preferEasyGoalChoices
, preferLinked
, preferPackagePreferences
, preferReallyEasyGoalChoices
, requireInstalled
) where
-- Reordering or pruning the tree in order to prefer or make certain choices.
import qualified Data.List as L
import qualified Data.Map as M
#if !MIN_VERSION_base(4,8,0)
import Data.Monoid
import Control.Applicative
#endif
import Prelude hiding (sequence)
import Control.Monad.Reader hiding (sequence)
import Data.Map (Map)
import Data.Traversable (sequence)
import Distribution.Client.Dependency.Types
( PackageConstraint(..), LabeledPackageConstraint(..), ConstraintSource(..)
, PackagePreferences(..), InstalledPreference(..) )
import Distribution.Client.Types
( OptionalStanza(..) )
import Distribution.Client.Dependency.Modular.Dependency
import Distribution.Client.Dependency.Modular.Flag
import Distribution.Client.Dependency.Modular.Package
import qualified Distribution.Client.Dependency.Modular.PSQ as P
import Distribution.Client.Dependency.Modular.Tree
import Distribution.Client.Dependency.Modular.Version
import qualified Distribution.Client.Dependency.Modular.ConflictSet as CS
-- | Generic abstraction for strategies that just rearrange the package order.
-- Only packages that match the given predicate are reordered.
packageOrderFor :: (PN -> Bool) -> (PN -> I -> I -> Ordering) -> Tree a -> Tree a
packageOrderFor p cmp' = trav go
where
go (PChoiceF v@(Q _ pn) r cs)
| p pn = PChoiceF v r (P.sortByKeys (flip (cmp pn)) cs)
| otherwise = PChoiceF v r cs
go x = x
cmp :: PN -> POption -> POption -> Ordering
cmp pn (POption i _) (POption i' _) = cmp' pn i i'
-- | Prefer to link packages whenever possible
preferLinked :: Tree a -> Tree a
preferLinked = trav go
where
go (PChoiceF qn a cs) = PChoiceF qn a (P.sortByKeys cmp cs)
go x = x
cmp (POption _ linkedTo) (POption _ linkedTo') = cmpL linkedTo linkedTo'
cmpL Nothing Nothing = EQ
cmpL Nothing (Just _) = GT
cmpL (Just _) Nothing = LT
cmpL (Just _) (Just _) = EQ
-- | Ordering that treats versions satisfying more preferred ranges as greater
-- than versions satisfying less preferred ranges.
preferredVersionsOrdering :: [VR] -> Ver -> Ver -> Ordering
preferredVersionsOrdering vrs v1 v2 = compare (check v1) (check v2)
where
check v = Prelude.length . Prelude.filter (==True) .
Prelude.map (flip checkVR v) $ vrs
-- | Traversal that tries to establish package preferences (not constraints).
-- Works by reordering choice nodes. Also applies stanza preferences.
preferPackagePreferences :: (PN -> PackagePreferences) -> Tree a -> Tree a
preferPackagePreferences pcs = preferPackageStanzaPreferences pcs
. packageOrderFor (const True) preference
where
preference pn i1@(I v1 _) i2@(I v2 _) =
let PackagePreferences vrs ipref _ = pcs pn
in preferredVersionsOrdering vrs v1 v2 `mappend` -- combines lexically
locationsOrdering ipref i1 i2
-- Note that we always rank installed before uninstalled, and later
-- versions before earlier, but we can change the priority of the
-- two orderings.
locationsOrdering PreferInstalled v1 v2 =
preferInstalledOrdering v1 v2 `mappend` preferLatestOrdering v1 v2
locationsOrdering PreferLatest v1 v2 =
preferLatestOrdering v1 v2 `mappend` preferInstalledOrdering v1 v2
-- | Ordering that treats installed instances as greater than uninstalled ones.
preferInstalledOrdering :: I -> I -> Ordering
preferInstalledOrdering (I _ (Inst _)) (I _ (Inst _)) = EQ
preferInstalledOrdering (I _ (Inst _)) _ = GT
preferInstalledOrdering _ (I _ (Inst _)) = LT
preferInstalledOrdering _ _ = EQ
-- | Compare instances by their version numbers.
preferLatestOrdering :: I -> I -> Ordering
preferLatestOrdering (I v1 _) (I v2 _) = compare v1 v2
-- | Traversal that tries to establish package stanza enable\/disable
-- preferences. Works by reordering the branches of stanza choices.
preferPackageStanzaPreferences :: (PN -> PackagePreferences) -> Tree a -> Tree a
preferPackageStanzaPreferences pcs = trav go
where
go (SChoiceF qsn@(SN (PI (Q pp pn) _) s) gr _tr ts) | primaryPP pp =
let PackagePreferences _ _ spref = pcs pn
enableStanzaPref = s `elem` spref
-- move True case first to try enabling the stanza
ts' | enableStanzaPref = P.sortByKeys (flip compare) ts
| otherwise = ts
in SChoiceF qsn gr True ts' -- True: now weak choice
go x = x
-- | 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 :: PP
-> ConflictSet QPN
-> I
-> LabeledPackageConstraint
-> Tree a
-> Tree a
processPackageConstraintP pp _ _ (LabeledPackageConstraint _ src) r
| src == ConstraintSourceUserTarget && not (primaryPP pp) = r
-- the constraints arising from targets, like "foo-1.0" only apply to
-- the main packages in the solution, they don't constrain setup deps
processPackageConstraintP _ c i (LabeledPackageConstraint pc src) r = go i pc
where
go (I v _) (PackageConstraintVersion _ vr)
| checkVR vr v = r
| otherwise = Fail c (GlobalConstraintVersion vr src)
go _ (PackageConstraintInstalled _)
| instI i = r
| otherwise = Fail c (GlobalConstraintInstalled src)
go _ (PackageConstraintSource _)
| 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 :: Flag
-> ConflictSet QPN
-> Bool
-> LabeledPackageConstraint
-> Tree a
-> Tree a
processPackageConstraintF f c b' (LabeledPackageConstraint pc src) r = go pc
where
go (PackageConstraintFlags _ fa) =
case L.lookup 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 :: OptionalStanza
-> ConflictSet QPN
-> Bool
-> LabeledPackageConstraint
-> Tree a
-> Tree a
processPackageConstraintS s c b' (LabeledPackageConstraint pc src) r = go pc
where
go (PackageConstraintStanzas _ 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 QGoalReason
-> Tree QGoalReason
enforcePackageConstraints pcs = trav go
where
go (PChoiceF qpn@(Q pp pn) 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 pp c i pc) id
(M.findWithDefault [] pn pcs)
in PChoiceF qpn gr (P.mapWithKey g ts)
go (FChoiceF qfn@(FN (PI (Q _ pn) _) f) gr tr m ts) =
let c = varToConflictSet (F qfn)
-- compose the transformation functions for each of the relevant constraint
g = \ b -> foldl (\ h pc -> h . processPackageConstraintF f c b pc) id
(M.findWithDefault [] pn pcs)
in FChoiceF qfn gr tr m (P.mapWithKey g ts)
go (SChoiceF qsn@(SN (PI (Q _ pn) _) f) 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 f c b pc) id
(M.findWithDefault [] pn pcs)
in SChoiceF qsn gr tr (P.mapWithKey g ts)
go x = x
-- | Transformation that tries to enforce manual flags. Manual flags
-- can only be re-set explicitly by the user. This transformation should
-- be run after user preferences have been enforced. For manual flags,
-- it checks if a user choice has been made. If not, it disables all but
-- the first choice.
enforceManualFlags :: Tree QGoalReason -> Tree QGoalReason
enforceManualFlags = trav go
where
go (FChoiceF qfn gr tr True ts) = FChoiceF qfn gr tr True $
let c = varToConflictSet (F qfn)
in case span isDisabled (P.toList ts) of
([], y : ys) -> P.fromList (y : L.map (\ (b, _) -> (b, Fail c ManualFlag)) ys)
_ -> ts -- something has been manually selected, leave things alone
where
isDisabled (_, Fail _ (GlobalConstraintFlag _)) = True
isDisabled _ = False
go x = x
-- | Require installed packages.
requireInstalled :: (PN -> Bool) -> Tree QGoalReason -> Tree QGoalReason
requireInstalled p = trav go
where
go (PChoiceF v@(Q _ pn) gr cs)
| p pn = PChoiceF v gr (P.mapWithKey installed cs)
| otherwise = PChoiceF v 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 QGoalReason -> Tree QGoalReason
avoidReinstalls p = trav go
where
go (PChoiceF qpn@(Q _ pn) gr cs)
| p pn = PChoiceF qpn gr disableReinstalls
| otherwise = PChoiceF qpn gr cs
where
disableReinstalls =
let installed = [ v | (POption (I v (Inst _)) _, _) <- P.toList cs ]
in P.mapWithKey (notReinstall installed) cs
notReinstall vs (POption (I v InRepo) _) _ | v `elem` vs =
Fail (varToConflictSet (P qpn)) CannotReinstall
notReinstall _ _ x =
x
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 a -> Tree a
firstGoal = trav go
where
go (GoalChoiceF xs) = GoalChoiceF (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. 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 a -> Tree a
preferBaseGoalChoice = trav go
where
go (GoalChoiceF xs) = GoalChoiceF (P.preferByKeys isBase xs)
go x = x
isBase :: OpenGoal comp -> Bool
isBase (OpenGoal (Simple (Dep (Q _pp pn) _) _) _) | unPN pn == "base" = True
isBase _ = False
-- | Deal with setup dependencies after regular dependencies, so that we can
-- will link setup depencencies against package dependencies when possible
deferSetupChoices :: Tree a -> Tree a
deferSetupChoices = trav go
where
go (GoalChoiceF xs) = GoalChoiceF (P.preferByKeys noSetup xs)
go x = x
noSetup :: OpenGoal comp -> Bool
noSetup (OpenGoal (Simple (Dep (Q (PP _ns (Setup _)) _) _) _) _) = 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 a -> Tree a
deferWeakFlagChoices = trav go
where
go (GoalChoiceF xs) = GoalChoiceF (P.prefer noWeakStanza (P.prefer noWeakFlag xs))
go x = x
noWeakStanza :: Tree a -> Bool
noWeakStanza (SChoice _ _ True _) = False
noWeakStanza _ = True
noWeakFlag :: Tree a -> Bool
noWeakFlag (FChoice _ _ True _ _) = False
noWeakFlag _ = True
-- | Transformation that sorts choice nodes so that
-- child nodes with a small branching degree are preferred.
--
-- Only approximates the number of choices in the branches.
-- In particular, we try to take any goal immediately if it has
-- a branching degree of 0 (guaranteed failure) or 1 (no other
-- choice possible).
--
-- Returns at most one choice.
--
preferEasyGoalChoices :: Tree a -> Tree a
preferEasyGoalChoices = trav go
where
go (GoalChoiceF xs) = GoalChoiceF (P.dminimumBy dchoices xs)
-- (a different implementation that seems slower):
-- GoalChoiceF (P.firstOnly (P.preferOrElse zeroOrOneChoices (P.minimumBy choices) xs))
go x = x
-- | A variant of 'preferEasyGoalChoices' that just keeps the
-- ones with a branching degree of 0 or 1. Note that unlike
-- 'preferEasyGoalChoices', this may return more than one
-- choice.
--
preferReallyEasyGoalChoices :: Tree a -> Tree a
preferReallyEasyGoalChoices = trav go
where
go (GoalChoiceF xs) = GoalChoiceF (P.prefer 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 QGoalReason -> Tree QGoalReason
enforceSingleInstanceRestriction = (`runReader` M.empty) . cata go
where
go :: TreeF QGoalReason (EnforceSIR (Tree QGoalReason)) -> EnforceSIR (Tree QGoalReason)
-- We just verify package choices.
go (PChoiceF qpn gr cs) =
PChoice qpn gr <$> sequence (P.mapWithKey (goP qpn) cs)
go _otherwise =
innM _otherwise
-- The check proper
goP :: QPN -> POption -> EnforceSIR (Tree QGoalReason) -> EnforceSIR (Tree QGoalReason)
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