diff --git a/Cabal/doc/nix-local-build.rst b/Cabal/doc/nix-local-build.rst index 363c8245e70..c8584f87e32 100644 --- a/Cabal/doc/nix-local-build.rst +++ b/Cabal/doc/nix-local-build.rst @@ -2073,6 +2073,21 @@ Most users generally won't need these. The command line variant of this field is ``--(no-)count-conflicts``. +.. cfg-field:: minimize-conflict-set: boolean + --minimize-conflict-set + --no-minimize-conflict-set + :synopsis: Try to improve the solver error message when there is no + solution. + + :default: False + + When there is no solution, try to improve the solver error message + by finding a minimal conflict set. This option may increase run + time significantly, so it is off by default. + + The command line variant of this field is + ``--(no-)minimize-conflict-set``. + .. cfg-field:: strong-flags: boolean --strong-flags --no-strong-flags diff --git a/cabal-install/Distribution/Client/Config.hs b/cabal-install/Distribution/Client/Config.hs index 9df76e7225c..8c85cc6fa4d 100644 --- a/cabal-install/Distribution/Client/Config.hs +++ b/cabal-install/Distribution/Client/Config.hs @@ -254,6 +254,7 @@ instance Semigroup SavedConfig where installMaxBackjumps = combine installMaxBackjumps, installReorderGoals = combine installReorderGoals, installCountConflicts = combine installCountConflicts, + installMinimizeConflictSet = combine installMinimizeConflictSet, installIndependentGoals = combine installIndependentGoals, installShadowPkgs = combine installShadowPkgs, installStrongFlags = combine installStrongFlags, diff --git a/cabal-install/Distribution/Client/Dependency.hs b/cabal-install/Distribution/Client/Dependency.hs index d58eb848c43..c6e4c16824f 100644 --- a/cabal-install/Distribution/Client/Dependency.hs +++ b/cabal-install/Distribution/Client/Dependency.hs @@ -47,6 +47,7 @@ module Distribution.Client.Dependency ( setPreferenceDefault, setReorderGoals, setCountConflicts, + setMinimizeConflictSet, setIndependentGoals, setAvoidReinstalls, setShadowPkgs, @@ -158,6 +159,7 @@ data DepResolverParams = DepResolverParams { depResolverSourcePkgIndex :: PackageIndex.PackageIndex UnresolvedSourcePackage, depResolverReorderGoals :: ReorderGoals, depResolverCountConflicts :: CountConflicts, + depResolverMinimizeConflictSet :: MinimizeConflictSet, depResolverIndependentGoals :: IndependentGoals, depResolverAvoidReinstalls :: AvoidReinstalls, depResolverShadowPkgs :: ShadowPkgs, @@ -195,6 +197,7 @@ showDepResolverParams p = ++ "\nstrategy: " ++ show (depResolverPreferenceDefault p) ++ "\nreorder goals: " ++ show (asBool (depResolverReorderGoals p)) ++ "\ncount conflicts: " ++ show (asBool (depResolverCountConflicts p)) + ++ "\nminimize conflict set: " ++ show (asBool (depResolverMinimizeConflictSet p)) ++ "\nindependent goals: " ++ show (asBool (depResolverIndependentGoals p)) ++ "\navoid reinstalls: " ++ show (asBool (depResolverAvoidReinstalls p)) ++ "\nshadow packages: " ++ show (asBool (depResolverShadowPkgs p)) @@ -251,6 +254,7 @@ basicDepResolverParams installedPkgIndex sourcePkgIndex = depResolverSourcePkgIndex = sourcePkgIndex, depResolverReorderGoals = ReorderGoals False, depResolverCountConflicts = CountConflicts True, + depResolverMinimizeConflictSet = MinimizeConflictSet False, depResolverIndependentGoals = IndependentGoals False, depResolverAvoidReinstalls = AvoidReinstalls False, depResolverShadowPkgs = ShadowPkgs False, @@ -306,6 +310,12 @@ setCountConflicts count params = depResolverCountConflicts = count } +setMinimizeConflictSet :: MinimizeConflictSet -> DepResolverParams -> DepResolverParams +setMinimizeConflictSet minimize params = + params { + depResolverMinimizeConflictSet = minimize + } + setIndependentGoals :: IndependentGoals -> DepResolverParams -> DepResolverParams setIndependentGoals indep params = params { @@ -745,8 +755,7 @@ resolveDependencies platform comp pkgConfigDB solver params = Step (showDepResolverParams finalparams) $ fmap (validateSolverResult platform comp indGoals) - $ runSolver solver (SolverConfig reordGoals cntConflicts - indGoals noReinstalls + $ runSolver solver (SolverConfig reordGoals cntConflicts minimize indGoals noReinstalls shadowing strFlags allowBootLibs onlyConstrained_ maxBkjumps enableBj solveExes order verbosity (PruneAfterFirstSuccess False)) platform comp installedPkgIndex sourcePkgIndex @@ -760,6 +769,7 @@ resolveDependencies platform comp pkgConfigDB solver params = sourcePkgIndex reordGoals cntConflicts + minimize indGoals noReinstalls shadowing @@ -1005,9 +1015,10 @@ resolveWithoutDependencies :: DepResolverParams -> Either [ResolveNoDepsError] [UnresolvedSourcePackage] resolveWithoutDependencies (DepResolverParams targets constraints prefs defpref installedPkgIndex sourcePkgIndex - _reorderGoals _countConflicts _indGoals _avoidReinstalls - _shadowing _strFlags _maxBjumps _enableBj - _solveExes _allowBootLibInstalls _onlyConstrained _order _verbosity) = + _reorderGoals _countConflicts _minimizeConflictSet + _indGoals _avoidReinstalls _shadowing _strFlags + _maxBjumps _enableBj _solveExes + _allowBootLibInstalls _onlyConstrained _order _verbosity) = collectEithers $ map selectPackage (Set.toList targets) where selectPackage :: PackageName -> Either ResolveNoDepsError UnresolvedSourcePackage diff --git a/cabal-install/Distribution/Client/Fetch.hs b/cabal-install/Distribution/Client/Fetch.hs index 31d13275e6d..ac0a74a907d 100644 --- a/cabal-install/Distribution/Client/Fetch.hs +++ b/cabal-install/Distribution/Client/Fetch.hs @@ -162,6 +162,8 @@ planPackages verbosity comp platform fetchFlags . setCountConflicts countConflicts + . setMinimizeConflictSet minimizeConflictSet + . setShadowPkgs shadowPkgs . setStrongFlags strongFlags @@ -197,6 +199,7 @@ planPackages verbosity comp platform fetchFlags reorderGoals = fromFlag (fetchReorderGoals fetchFlags) countConflicts = fromFlag (fetchCountConflicts fetchFlags) + minimizeConflictSet = fromFlag (fetchMinimizeConflictSet fetchFlags) independentGoals = fromFlag (fetchIndependentGoals fetchFlags) shadowPkgs = fromFlag (fetchShadowPkgs fetchFlags) strongFlags = fromFlag (fetchStrongFlags fetchFlags) diff --git a/cabal-install/Distribution/Client/Freeze.hs b/cabal-install/Distribution/Client/Freeze.hs index 461568cbee5..3b8b2c193ab 100644 --- a/cabal-install/Distribution/Client/Freeze.hs +++ b/cabal-install/Distribution/Client/Freeze.hs @@ -175,6 +175,8 @@ planPackages verbosity comp platform mSandboxPkgInfo freezeFlags . setCountConflicts countConflicts + . setMinimizeConflictSet minimizeConflictSet + . setShadowPkgs shadowPkgs . setStrongFlags strongFlags @@ -205,6 +207,7 @@ planPackages verbosity comp platform mSandboxPkgInfo freezeFlags reorderGoals = fromFlag (freezeReorderGoals freezeFlags) countConflicts = fromFlag (freezeCountConflicts freezeFlags) + minimizeConflictSet = fromFlag (freezeMinimizeConflictSet freezeFlags) independentGoals = fromFlag (freezeIndependentGoals freezeFlags) shadowPkgs = fromFlag (freezeShadowPkgs freezeFlags) strongFlags = fromFlag (freezeStrongFlags freezeFlags) diff --git a/cabal-install/Distribution/Client/Install.hs b/cabal-install/Distribution/Client/Install.hs index 0d3391a2377..df2c7bb4b7e 100644 --- a/cabal-install/Distribution/Client/Install.hs +++ b/cabal-install/Distribution/Client/Install.hs @@ -389,6 +389,8 @@ planPackages verbosity comp platform mSandboxPkgInfo solver . setCountConflicts countConflicts + . setMinimizeConflictSet minimizeConflictSet + . setAvoidReinstalls avoidReinstalls . setShadowPkgs shadowPkgs @@ -455,6 +457,7 @@ planPackages verbosity comp platform mSandboxPkgInfo solver fromFlag (installReinstall installFlags) reorderGoals = fromFlag (installReorderGoals installFlags) countConflicts = fromFlag (installCountConflicts installFlags) + minimizeConflictSet = fromFlag (installMinimizeConflictSet installFlags) independentGoals = fromFlag (installIndependentGoals installFlags) avoidReinstalls = fromFlag (installAvoidReinstalls installFlags) shadowPkgs = fromFlag (installShadowPkgs installFlags) diff --git a/cabal-install/Distribution/Client/ProjectConfig.hs b/cabal-install/Distribution/Client/ProjectConfig.hs index 8e05e02f067..9417aeb7d05 100644 --- a/cabal-install/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/Distribution/Client/ProjectConfig.hs @@ -238,6 +238,7 @@ resolveSolverSettings ProjectConfig{ | otherwise -> Just n solverSettingReorderGoals = fromFlag projectConfigReorderGoals solverSettingCountConflicts = fromFlag projectConfigCountConflicts + solverSettingMinimizeConflictSet = fromFlag projectConfigMinimizeConflictSet solverSettingStrongFlags = fromFlag projectConfigStrongFlags solverSettingAllowBootLibInstalls = fromFlag projectConfigAllowBootLibInstalls solverSettingOnlyConstrained = fromFlag projectConfigOnlyConstrained @@ -258,6 +259,7 @@ resolveSolverSettings ProjectConfig{ projectConfigMaxBackjumps = Flag defaultMaxBackjumps, projectConfigReorderGoals = Flag (ReorderGoals False), projectConfigCountConflicts = Flag (CountConflicts True), + projectConfigMinimizeConflictSet = Flag (MinimizeConflictSet False), projectConfigStrongFlags = Flag (StrongFlags False), projectConfigAllowBootLibInstalls = Flag (AllowBootLibInstalls False), projectConfigOnlyConstrained = Flag OnlyConstrainedNone, diff --git a/cabal-install/Distribution/Client/ProjectConfig/Legacy.hs b/cabal-install/Distribution/Client/ProjectConfig/Legacy.hs index e031b5c64cf..2a373a5e8e8 100644 --- a/cabal-install/Distribution/Client/ProjectConfig/Legacy.hs +++ b/cabal-install/Distribution/Client/ProjectConfig/Legacy.hs @@ -352,6 +352,7 @@ convertLegacyAllPackageFlags globalFlags configFlags --installUpgradeDeps = projectConfigUpgradeDeps, installReorderGoals = projectConfigReorderGoals, installCountConflicts = projectConfigCountConflicts, + installMinimizeConflictSet = projectConfigMinimizeConflictSet, installPerComponent = projectConfigPerComponent, installIndependentGoals = projectConfigIndependentGoals, --installShadowPkgs = projectConfigShadowPkgs, @@ -577,6 +578,7 @@ convertToLegacySharedConfig installUpgradeDeps = mempty, --projectConfigUpgradeDeps, installReorderGoals = projectConfigReorderGoals, installCountConflicts = projectConfigCountConflicts, + installMinimizeConflictSet = projectConfigMinimizeConflictSet, installIndependentGoals = projectConfigIndependentGoals, installShadowPkgs = mempty, --projectConfigShadowPkgs, installStrongFlags = projectConfigStrongFlags, @@ -954,7 +956,8 @@ legacySharedConfigFieldDescrs = , "remote-build-reporting", "report-planning-failure" , "one-shot", "jobs", "keep-going", "offline", "per-component" -- solver flags: - , "max-backjumps", "reorder-goals", "count-conflicts", "independent-goals" + , "max-backjumps", "reorder-goals", "count-conflicts" + , "minimize-conflict-set", "independent-goals" , "strong-flags" , "allow-boot-library-installs", "reject-unconstrained-dependencies", "index-state" ] . commandOptionsToFields diff --git a/cabal-install/Distribution/Client/ProjectConfig/Types.hs b/cabal-install/Distribution/Client/ProjectConfig/Types.hs index 329c290d5fd..3a17786d95b 100644 --- a/cabal-install/Distribution/Client/ProjectConfig/Types.hs +++ b/cabal-install/Distribution/Client/ProjectConfig/Types.hs @@ -193,6 +193,7 @@ data ProjectConfigShared projectConfigMaxBackjumps :: Flag Int, projectConfigReorderGoals :: Flag ReorderGoals, projectConfigCountConflicts :: Flag CountConflicts, + projectConfigMinimizeConflictSet :: Flag MinimizeConflictSet, projectConfigStrongFlags :: Flag StrongFlags, projectConfigAllowBootLibInstalls :: Flag AllowBootLibInstalls, projectConfigOnlyConstrained :: Flag OnlyConstrained, @@ -383,6 +384,7 @@ data SolverSettings solverSettingMaxBackjumps :: Maybe Int, solverSettingReorderGoals :: ReorderGoals, solverSettingCountConflicts :: CountConflicts, + solverSettingMinimizeConflictSet :: MinimizeConflictSet, solverSettingStrongFlags :: StrongFlags, solverSettingAllowBootLibInstalls :: AllowBootLibInstalls, solverSettingOnlyConstrained :: OnlyConstrained, diff --git a/cabal-install/Distribution/Client/ProjectPlanning.hs b/cabal-install/Distribution/Client/ProjectPlanning.hs index b68f270d358..c0392568a95 100644 --- a/cabal-install/Distribution/Client/ProjectPlanning.hs +++ b/cabal-install/Distribution/Client/ProjectPlanning.hs @@ -951,6 +951,8 @@ planPackages verbosity comp platform solver SolverSettings{..} . setCountConflicts solverSettingCountConflicts + . setMinimizeConflictSet solverSettingMinimizeConflictSet + --TODO: [required eventually] should only be configurable for --custom installs -- . setAvoidReinstalls solverSettingAvoidReinstalls diff --git a/cabal-install/Distribution/Client/Setup.hs b/cabal-install/Distribution/Client/Setup.hs index 6360978f7c4..fa4adee047a 100644 --- a/cabal-install/Distribution/Client/Setup.hs +++ b/cabal-install/Distribution/Client/Setup.hs @@ -973,6 +973,7 @@ data FetchFlags = FetchFlags { fetchMaxBackjumps :: Flag Int, fetchReorderGoals :: Flag ReorderGoals, fetchCountConflicts :: Flag CountConflicts, + fetchMinimizeConflictSet :: Flag MinimizeConflictSet, fetchIndependentGoals :: Flag IndependentGoals, fetchShadowPkgs :: Flag ShadowPkgs, fetchStrongFlags :: Flag StrongFlags, @@ -992,6 +993,7 @@ defaultFetchFlags = FetchFlags { fetchMaxBackjumps = Flag defaultMaxBackjumps, fetchReorderGoals = Flag (ReorderGoals False), fetchCountConflicts = Flag (CountConflicts True), + fetchMinimizeConflictSet = Flag (MinimizeConflictSet False), fetchIndependentGoals = Flag (IndependentGoals False), fetchShadowPkgs = Flag (ShadowPkgs False), fetchStrongFlags = Flag (StrongFlags False), @@ -1053,6 +1055,7 @@ fetchCommand = CommandUI { fetchMaxBackjumps (\v flags -> flags { fetchMaxBackjumps = v }) fetchReorderGoals (\v flags -> flags { fetchReorderGoals = v }) fetchCountConflicts (\v flags -> flags { fetchCountConflicts = v }) + fetchMinimizeConflictSet (\v flags -> flags { fetchMinimizeConflictSet = v }) fetchIndependentGoals (\v flags -> flags { fetchIndependentGoals = v }) fetchShadowPkgs (\v flags -> flags { fetchShadowPkgs = v }) fetchStrongFlags (\v flags -> flags { fetchStrongFlags = v }) @@ -1073,6 +1076,7 @@ data FreezeFlags = FreezeFlags { freezeMaxBackjumps :: Flag Int, freezeReorderGoals :: Flag ReorderGoals, freezeCountConflicts :: Flag CountConflicts, + freezeMinimizeConflictSet :: Flag MinimizeConflictSet, freezeIndependentGoals :: Flag IndependentGoals, freezeShadowPkgs :: Flag ShadowPkgs, freezeStrongFlags :: Flag StrongFlags, @@ -1090,6 +1094,7 @@ defaultFreezeFlags = FreezeFlags { freezeMaxBackjumps = Flag defaultMaxBackjumps, freezeReorderGoals = Flag (ReorderGoals False), freezeCountConflicts = Flag (CountConflicts True), + freezeMinimizeConflictSet = Flag (MinimizeConflictSet False), freezeIndependentGoals = Flag (IndependentGoals False), freezeShadowPkgs = Flag (ShadowPkgs False), freezeStrongFlags = Flag (StrongFlags False), @@ -1142,6 +1147,7 @@ freezeCommand = CommandUI { freezeMaxBackjumps (\v flags -> flags { freezeMaxBackjumps = v }) freezeReorderGoals (\v flags -> flags { freezeReorderGoals = v }) freezeCountConflicts (\v flags -> flags { freezeCountConflicts = v }) + freezeMinimizeConflictSet (\v flags -> flags { freezeMinimizeConflictSet = v }) freezeIndependentGoals (\v flags -> flags { freezeIndependentGoals = v }) freezeShadowPkgs (\v flags -> flags { freezeShadowPkgs = v }) freezeStrongFlags (\v flags -> flags { freezeStrongFlags = v }) @@ -1711,6 +1717,7 @@ data InstallFlags = InstallFlags { installMaxBackjumps :: Flag Int, installReorderGoals :: Flag ReorderGoals, installCountConflicts :: Flag CountConflicts, + installMinimizeConflictSet :: Flag MinimizeConflictSet, installIndependentGoals :: Flag IndependentGoals, installShadowPkgs :: Flag ShadowPkgs, installStrongFlags :: Flag StrongFlags, @@ -1757,6 +1764,7 @@ defaultInstallFlags = InstallFlags { installMaxBackjumps = Flag defaultMaxBackjumps, installReorderGoals = Flag (ReorderGoals False), installCountConflicts = Flag (CountConflicts True), + installMinimizeConflictSet = Flag (MinimizeConflictSet False), installIndependentGoals= Flag (IndependentGoals False), installShadowPkgs = Flag (ShadowPkgs False), installStrongFlags = Flag (StrongFlags False), @@ -1968,6 +1976,7 @@ installOptions showOrParseArgs = installMaxBackjumps (\v flags -> flags { installMaxBackjumps = v }) installReorderGoals (\v flags -> flags { installReorderGoals = v }) installCountConflicts (\v flags -> flags { installCountConflicts = v }) + installMinimizeConflictSet (\v flags -> flags { installMinimizeConflictSet = v }) installIndependentGoals (\v flags -> flags { installIndependentGoals = v }) installShadowPkgs (\v flags -> flags { installShadowPkgs = v }) installStrongFlags (\v flags -> flags { installStrongFlags = v }) @@ -2778,14 +2787,16 @@ optionSolverFlags :: ShowOrParseArgs -> (flags -> Flag Int ) -> (Flag Int -> flags -> flags) -> (flags -> Flag ReorderGoals) -> (Flag ReorderGoals -> flags -> flags) -> (flags -> Flag CountConflicts) -> (Flag CountConflicts -> flags -> flags) + -> (flags -> Flag MinimizeConflictSet) -> (Flag MinimizeConflictSet -> flags -> flags) -> (flags -> Flag IndependentGoals) -> (Flag IndependentGoals -> flags -> flags) -> (flags -> Flag ShadowPkgs) -> (Flag ShadowPkgs -> flags -> flags) -> (flags -> Flag StrongFlags) -> (Flag StrongFlags -> flags -> flags) -> (flags -> Flag AllowBootLibInstalls) -> (Flag AllowBootLibInstalls -> flags -> flags) -> (flags -> Flag OnlyConstrained) -> (Flag OnlyConstrained -> flags -> flags) -> [OptionField flags] -optionSolverFlags showOrParseArgs getmbj setmbj getrg setrg getcc setcc getig setig - getsip setsip getstrfl setstrfl getib setib getoc setoc = +optionSolverFlags showOrParseArgs getmbj setmbj getrg setrg getcc setcc + getmc setmc getig setig getsip setsip getstrfl setstrfl + getib setib getoc setoc = [ option [] ["max-backjumps"] ("Maximum number of backjumps allowed while solving (default: " ++ show defaultMaxBackjumps ++ "). Use a negative number to enable unlimited backtracking. Use 0 to disable backtracking completely.") getmbj setmbj @@ -2801,6 +2812,13 @@ optionSolverFlags showOrParseArgs getmbj setmbj getrg setrg getcc setcc getig se (fmap asBool . getcc) (setcc . fmap CountConflicts) (yesNoOpt showOrParseArgs) + , option [] ["minimize-conflict-set"] + ("When there is no solution, try to improve the error message by finding " + ++ "a minimal conflict set (default: false). May increase run time " + ++ "significantly.") + (fmap asBool . getmc) + (setmc . fmap MinimizeConflictSet) + (yesNoOpt showOrParseArgs) , option [] ["independent-goals"] "Treat several goals on the command line as independent. If several goals depend on the same package, different versions can be chosen." (fmap asBool . getig) diff --git a/cabal-install/Distribution/Solver/Modular.hs b/cabal-install/Distribution/Solver/Modular.hs index 157bd921643..4648ab8a130 100644 --- a/cabal-install/Distribution/Solver/Modular.hs +++ b/cabal-install/Distribution/Solver/Modular.hs @@ -1,3 +1,6 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE ScopedTypeVariables #-} + module Distribution.Solver.Modular ( modularResolver, SolverConfig(..), PruneAfterFirstSuccess(..) ) where @@ -13,7 +16,7 @@ import Prelude () import Distribution.Solver.Compat.Prelude import qualified Data.Map as M -import Data.Set (Set) +import Data.Set (Set, isSubsetOf) import Data.Ord import Distribution.Compat.Graph ( IsNode(..) ) @@ -30,9 +33,10 @@ import Distribution.Solver.Modular.Index import Distribution.Solver.Modular.IndexConversion ( convPIs ) import Distribution.Solver.Modular.Log - ( SolverFailure(..), logToProgress ) + ( SolverFailure(..), displayLogMessages ) import Distribution.Solver.Modular.Package ( PN ) +import Distribution.Solver.Modular.RetryLog import Distribution.Solver.Modular.Solver ( SolverConfig(..), PruneAfterFirstSuccess(..), solve ) import Distribution.Solver.Types.DependencyResolver @@ -46,6 +50,8 @@ import Distribution.Solver.Types.Progress import Distribution.Solver.Types.Variable import Distribution.System ( Platform(..) ) +import Distribution.Simple.Setup + ( BooleanFlag(..) ) import Distribution.Simple.Utils ( ordNubBy ) import Distribution.Verbosity @@ -80,16 +86,17 @@ modularResolver sc (Platform arch os) cinfo iidx sidx pkgConfigDB pprefs pcs pns -- -- When there is no solution, we produce the error message by rerunning the -- solver but making it prefer the goals from the final conflict set from the --- first run. We also set the backjump limit to 0, so that the log stops at the --- first backjump and is relatively short. Preferring goals from the final --- conflict set increases the probability that the log to the first backjump --- contains package, flag, and stanza choices that are relevant to the final --- failure. The solver shouldn't need to choose any packages that aren't in the --- final conflict set. (For every variable in the final conflict set, the final --- conflict set should also contain the variable that introduced that variable. --- The solver can then follow that chain of variables in reverse order from the --- user target to the conflict.) However, it is possible that the conflict set --- contains unnecessary variables. +-- first run (or a subset of the final conflict set with +-- --minimize-conflict-set). We also set the backjump limit to 0, so that the +-- log stops at the first backjump and is relatively short. Preferring goals +-- from the final conflict set increases the probability that the log to the +-- first backjump contains package, flag, and stanza choices that are relevant +-- to the final failure. The solver shouldn't need to choose any packages that +-- aren't in the final conflict set. (For every variable in the final conflict +-- set, the final conflict set should also contain the variable that introduced +-- that variable. The solver can then follow that chain of variables in reverse +-- order from the user target to the conflict.) However, it is possible that the +-- conflict set contains unnecessary variables. -- -- Producing an error message when the solver reaches the backjump limit is more -- complicated. There is no final conflict set, so we create one for the minimal @@ -116,33 +123,53 @@ solve' :: SolverConfig -> Set PN -> Progress String String (Assignment, RevDepMap) solve' sc cinfo idx pkgConfigDB pprefs gcs pns = - foldProgress Step (uncurry createErrorMsg) Done (runSolver printFullLog sc) + toProgress $ retry (runSolver printFullLog sc) createErrorMsg where runSolver :: Bool -> SolverConfig - -> Progress String (SolverFailure, String) (Assignment, RevDepMap) + -> RetryLog String SolverFailure (Assignment, RevDepMap) runSolver keepLog sc' = - logToProgress keepLog (solverVerbosity sc') (maxBackjumps sc') $ + displayLogMessages keepLog $ solve sc' cinfo idx pkgConfigDB pprefs gcs pns - createErrorMsg :: SolverFailure -> String - -> Progress String String (Assignment, RevDepMap) - createErrorMsg (ExhaustiveSearch cs _) msg = - Fail $ rerunSolverForErrorMsg cs ++ msg - createErrorMsg BackjumpLimitReached msg = - Step ("Backjump limit reached. Rerunning dependency solver to generate " + createErrorMsg :: SolverFailure + -> RetryLog String String (Assignment, RevDepMap) + createErrorMsg failure@(ExhaustiveSearch cs cm) = + if asBool $ minimizeConflictSet sc + then continueWith ("Found no solution after exhaustively searching the " + ++ "dependency tree. Rerunning the dependency solver " + ++ "to minimize the conflict set ({" + ++ showConflictSet cs ++ "}).") $ + retry (tryToMinimizeConflictSet (runSolver printFullLog) sc cs cm) $ + \case + ExhaustiveSearch cs' cm' -> + fromProgress $ Fail $ + rerunSolverForErrorMsg cs' + ++ finalErrorMsg sc (ExhaustiveSearch cs' cm') + BackjumpLimitReached -> + fromProgress $ Fail $ + "Reached backjump limit while trying to minimize the " + ++ "conflict set to create a better error message. " + ++ "Original error message:\n" + ++ rerunSolverForErrorMsg cs + ++ finalErrorMsg sc failure + else fromProgress $ Fail $ + rerunSolverForErrorMsg cs ++ finalErrorMsg sc failure + createErrorMsg failure@BackjumpLimitReached = + continueWith + ("Backjump limit reached. Rerunning dependency solver to generate " ++ "a final conflict set for the search tree containing the " ++ "first backjump.") $ - foldProgress Step (f . fst) Done $ - runSolver printFullLog - sc { pruneAfterFirstSuccess = PruneAfterFirstSuccess True } - where - f :: SolverFailure -> Progress String String (Assignment, RevDepMap) - f (ExhaustiveSearch cs _) = Fail $ rerunSolverForErrorMsg cs ++ msg - f BackjumpLimitReached = - -- This case is possible when the number of goals involved in - -- conflicts is greater than the backjump limit. - Fail $ msg ++ "Failed to generate a summarized dependency solver " - ++ "log due to low backjump limit." + retry (runSolver printFullLog sc { pruneAfterFirstSuccess = PruneAfterFirstSuccess True }) $ + \case + ExhaustiveSearch cs _ -> + fromProgress $ Fail $ + rerunSolverForErrorMsg cs ++ finalErrorMsg sc failure + BackjumpLimitReached -> + -- This case is possible when the number of goals involved in + -- conflicts is greater than the backjump limit. + fromProgress $ Fail $ finalErrorMsg sc failure + ++ "Failed to generate a summarized dependency solver " + ++ "log due to low backjump limit." rerunSolverForErrorMsg :: ConflictSet -> String rerunSolverForErrorMsg cs = @@ -155,21 +182,158 @@ solve' sc cinfo idx pkgConfigDB pprefs gcs pns = -- original goal order. goalOrder' = preferGoalsFromConflictSet cs <> fromMaybe mempty (goalOrder sc) - in unlines ("Could not resolve dependencies:" : messages (runSolver True sc')) + in unlines ("Could not resolve dependencies:" : messages (toProgress (runSolver True sc'))) printFullLog = solverVerbosity sc >= verbose messages :: Progress step fail done -> [step] messages = foldProgress (:) (const []) (const []) +-- | Try to remove variables from the given conflict set to create a minimal +-- conflict set. +-- +-- Minimal means that no proper subset of the conflict set is also a conflict +-- set, though there may be other possible conflict sets with fewer variables. +-- This function minimizes the input by trying to remove one variable at a time. +-- It only makes one pass over the variables, so it runs the solver at most N +-- times when given a conflict set of size N. Only one pass is necessary, +-- because every superset of a conflict set is also a conflict set, meaning that +-- failing to remove variable X from a conflict set in one step means that X +-- cannot be removed from any subset of that conflict set in a subsequent step. +-- +-- Example steps: +-- +-- Start with {A, B, C}. +-- Try to remove A from {A, B, C} and fail. +-- Try to remove B from {A, B, C} and succeed. +-- Try to remove C from {A, C} and fail. +-- Return {A, C} +-- +-- This function can fail for two reasons: +-- +-- 1. The solver can reach the backjump limit on any run. In this case the +-- returned RetryLog ends with BackjumpLimitReached. +-- TODO: Consider applying the backjump limit to all solver runs combined, +-- instead of each individual run. For example, 10 runs with 10 backjumps +-- each should count as 100 backjumps. +-- 2. Since this function works by rerunning the solver, it is possible for the +-- solver to add new unnecessary variables to the conflict set. This function +-- discards the result from any run that adds new variables to the conflict +-- set, but the end result may not be completely minimized. +tryToMinimizeConflictSet :: forall a . (SolverConfig -> RetryLog String SolverFailure a) + -> SolverConfig + -> ConflictSet + -> ConflictMap + -> RetryLog String SolverFailure a +tryToMinimizeConflictSet runSolver sc cs cm = + foldl (\r v -> retryNoSolution r $ tryToRemoveOneVar v) + (fromProgress $ Fail $ ExhaustiveSearch cs cm) + (CS.toList cs) + where + -- This function runs the solver and makes it prefer goals in the following + -- order: + -- + -- 1. variables in 'smallestKnownCS', excluding 'v' + -- 2. 'v' + -- 3. all other variables + -- + -- If 'v' is not necessary, then the solver will find that there is no + -- solution before starting to solve for 'v', and the new final conflict set + -- will be very likely to not contain 'v'. If 'v' is necessary, the solver + -- will most likely need to try solving for 'v' before finding that there is + -- no solution, and the new final conflict set will still contain 'v'. + -- However, this method isn't perfect, because it is possible for the solver + -- to add new unnecessary variables to the conflict set on any run. This + -- function prevents the conflict set from growing by checking that the new + -- conflict set is a subset of the old one and falling back to using the old + -- conflict set when that check fails. + tryToRemoveOneVar :: Var QPN + -> ConflictSet + -> ConflictMap + -> RetryLog String SolverFailure a + tryToRemoveOneVar v smallestKnownCS smallestKnownCM + -- Check whether v is still present, because it may have already been + -- removed in a previous solver rerun. + | not (v `CS.member` smallestKnownCS) = + fromProgress $ Fail $ ExhaustiveSearch smallestKnownCS smallestKnownCM + | otherwise = + continueWith ("Trying to remove variable " ++ varStr ++ " from the " + ++ "conflict set.") $ + retry (runSolver sc') $ \case + err@(ExhaustiveSearch cs' _) + | CS.toSet cs' `isSubsetOf` CS.toSet smallestKnownCS -> + let msg = if not $ CS.member v cs' + then "Successfully removed " ++ varStr ++ " from " + ++ "the conflict set." + else "Failed to remove " ++ varStr ++ " from the " + ++ "conflict set." + in -- Use the new conflict set, even if v wasn't removed, + -- because other variables may have been removed. + failWith (msg ++ " Continuing with " ++ showCS cs' ++ ".") err + | otherwise -> + failWith ("Failed to find a smaller conflict set. The new " + ++ "conflict set is not a subset of the previous " + ++ "conflict set: " ++ showCS cs') $ + ExhaustiveSearch smallestKnownCS smallestKnownCM + BackjumpLimitReached -> + failWith ("Reached backjump limit while minimizing conflict set.") + BackjumpLimitReached + where + varStr = "\"" ++ showVar v ++ "\"" + showCS cs' = "{" ++ showConflictSet cs' ++ "}" + + sc' = sc { goalOrder = Just goalOrder' } + + goalOrder' = + preferGoalsFromConflictSet (v `CS.delete` smallestKnownCS) + <> preferGoal v + <> fromMaybe mempty (goalOrder sc) + + -- Like 'retry', except that it only applies the input function when the + -- backjump limit has not been reached. + retryNoSolution :: RetryLog step SolverFailure done + -> (ConflictSet -> ConflictMap -> RetryLog step SolverFailure done) + -> RetryLog step SolverFailure done + retryNoSolution lg f = retry lg $ \case + ExhaustiveSearch cs' cm' -> f cs' cm' + BackjumpLimitReached -> fromProgress (Fail BackjumpLimitReached) + -- | Goal ordering that chooses goals contained in the conflict set before -- other goals. preferGoalsFromConflictSet :: ConflictSet -> Variable QPN -> Variable QPN -> Ordering -preferGoalsFromConflictSet cs = - comparing $ \v -> not $ CS.member (toVar v) cs - where - toVar :: Variable QPN -> Var QPN - toVar (PackageVar qpn) = P qpn - toVar (FlagVar qpn fn) = F (FN qpn fn) - toVar (StanzaVar qpn sn) = S (SN qpn sn) +preferGoalsFromConflictSet cs = comparing $ \v -> not $ CS.member (toVar v) cs + +-- | Goal ordering that chooses the given goal first. +preferGoal :: Var QPN -> Variable QPN -> Variable QPN -> Ordering +preferGoal preferred = comparing $ \v -> toVar v /= preferred + +toVar :: Variable QPN -> Var QPN +toVar (PackageVar qpn) = P qpn +toVar (FlagVar qpn fn) = F (FN qpn fn) +toVar (StanzaVar qpn sn) = S (SN qpn sn) + +finalErrorMsg :: SolverConfig -> SolverFailure -> String +finalErrorMsg sc failure = + case failure of + ExhaustiveSearch cs cm -> + "After searching the rest of the dependency tree exhaustively, " + ++ "these were the goals I've had most trouble fulfilling: " + ++ showCS cm cs + ++ flagSuggestion + where + showCS = if solverVerbosity sc > normal + then CS.showCSWithFrequency + else CS.showCSSortedByFrequency + flagSuggestion = + -- Don't suggest --minimize-conflict-set if the conflict set is + -- already small, because it is unlikely to be reduced further. + if CS.size cs > 3 && not (asBool (minimizeConflictSet sc)) + then "\nTry running with --minimize-conflict-set to improve the " + ++ "error message." + else "" + BackjumpLimitReached -> + "Backjump limit reached (" ++ currlimit (maxBackjumps sc) ++ + "change with --max-backjumps or try to run with --reorder-goals).\n" + where currlimit (Just n) = "currently " ++ show n ++ ", " + currlimit Nothing = "" diff --git a/cabal-install/Distribution/Solver/Modular/ConflictSet.hs b/cabal-install/Distribution/Solver/Modular/ConflictSet.hs index 46e25f76c4d..28991e3da04 100644 --- a/cabal-install/Distribution/Solver/Modular/ConflictSet.hs +++ b/cabal-install/Distribution/Solver/Modular/ConflictSet.hs @@ -18,12 +18,15 @@ module Distribution.Solver.Modular.ConflictSet ( , showCSSortedByFrequency , showCSWithFrequency -- Set-like operations + , toSet , toList , union , unions , insert + , delete , empty , singleton + , size , member , filter , fromList @@ -98,6 +101,9 @@ showCS showCount cm = Set-like operations -------------------------------------------------------------------------------} +toSet :: ConflictSet -> Set (Var QPN) +toSet = conflictSetToSet + toList :: ConflictSet -> [Var QPN] toList = S.toList . conflictSetToSet @@ -137,6 +143,11 @@ insert var cs = CS { #endif } +delete :: Var QPN -> ConflictSet -> ConflictSet +delete var cs = CS { + conflictSetToSet = S.delete var (conflictSetToSet cs) + } + empty :: #ifdef DEBUG_CONFLICT_SETS (?loc :: CallStack) => @@ -161,6 +172,9 @@ singleton var = CS { #endif } +size :: ConflictSet -> Int +size = S.size . conflictSetToSet + member :: Var QPN -> ConflictSet -> Bool member var = S.member var . conflictSetToSet diff --git a/cabal-install/Distribution/Solver/Modular/Log.hs b/cabal-install/Distribution/Solver/Modular/Log.hs index fa08137c6f0..321a051070b 100644 --- a/cabal-install/Distribution/Solver/Modular/Log.hs +++ b/cabal-install/Distribution/Solver/Modular/Log.hs @@ -1,5 +1,5 @@ module Distribution.Solver.Modular.Log - ( logToProgress + ( displayLogMessages , SolverFailure(..) ) where @@ -10,47 +10,22 @@ import Distribution.Solver.Types.Progress import Distribution.Solver.Modular.Dependency import Distribution.Solver.Modular.Message -import qualified Distribution.Solver.Modular.ConflictSet as CS import Distribution.Solver.Modular.RetryLog -import Distribution.Verbosity -- | Information about a dependency solver failure. data SolverFailure = ExhaustiveSearch ConflictSet ConflictMap | BackjumpLimitReached --- | Postprocesses a log file. When the dependency solver fails to find a --- solution, the log ends with a SolverFailure and a message describing the --- failure. This function discards all log messages and avoids calling --- 'showMessages' if the log isn't needed (specified by 'keepLog'), for --- efficiency. -logToProgress :: Bool - -> Verbosity - -> Maybe Int - -> RetryLog Message SolverFailure a - -> Progress String (SolverFailure, String) a -logToProgress keepLog verbosity mbj lg = +-- | Postprocesses a log file. This function discards all log messages and +-- avoids calling 'showMessages' if the log isn't needed (specified by +-- 'keepLog'), for efficiency. +displayLogMessages :: Bool + -> RetryLog Message SolverFailure a + -> RetryLog String SolverFailure a +displayLogMessages keepLog lg = fromProgress $ if keepLog then showMessages progress else foldProgress (const id) Fail Done progress where - progress = - -- Convert the RetryLog to a Progress (with toProgress) as late as - -- possible, to take advantage of efficient updates at failures. - toProgress $ - mapFailure (\failure -> (failure, finalErrorMsg failure)) lg - - finalErrorMsg :: SolverFailure -> String - finalErrorMsg (ExhaustiveSearch cs cm) = - "After searching the rest of the dependency tree exhaustively, " - ++ "these were the goals I've had most trouble fulfilling: " - ++ showCS cm cs - where - showCS = if verbosity > normal - then CS.showCSWithFrequency - else CS.showCSSortedByFrequency - finalErrorMsg BackjumpLimitReached = - "Backjump limit reached (" ++ currlimit mbj ++ - "change with --max-backjumps or try to run with --reorder-goals).\n" - where currlimit (Just n) = "currently " ++ show n ++ ", " - currlimit Nothing = "" + progress = toProgress lg diff --git a/cabal-install/Distribution/Solver/Modular/Solver.hs b/cabal-install/Distribution/Solver/Modular/Solver.hs index 73ad9d70175..b0e40cb9a0a 100644 --- a/cabal-install/Distribution/Solver/Modular/Solver.hs +++ b/cabal-install/Distribution/Solver/Modular/Solver.hs @@ -57,6 +57,7 @@ import Debug.Trace.Tree.Assoc (Assoc(..)) data SolverConfig = SolverConfig { reorderGoals :: ReorderGoals, countConflicts :: CountConflicts, + minimizeConflictSet :: MinimizeConflictSet, independentGoals :: IndependentGoals, avoidReinstalls :: AvoidReinstalls, shadowPkgs :: ShadowPkgs, diff --git a/cabal-install/Distribution/Solver/Types/Settings.hs b/cabal-install/Distribution/Solver/Types/Settings.hs index 635caa7fbbc..94a99879489 100644 --- a/cabal-install/Distribution/Solver/Types/Settings.hs +++ b/cabal-install/Distribution/Solver/Types/Settings.hs @@ -3,6 +3,7 @@ module Distribution.Solver.Types.Settings ( ReorderGoals(..) , IndependentGoals(..) + , MinimizeConflictSet(..) , AvoidReinstalls(..) , ShadowPkgs(..) , StrongFlags(..) @@ -28,6 +29,9 @@ newtype ReorderGoals = ReorderGoals Bool newtype CountConflicts = CountConflicts Bool deriving (BooleanFlag, Eq, Generic, Show) +newtype MinimizeConflictSet = MinimizeConflictSet Bool + deriving (BooleanFlag, Eq, Generic, Show) + newtype IndependentGoals = IndependentGoals Bool deriving (BooleanFlag, Eq, Generic, Show) @@ -59,6 +63,7 @@ newtype SolveExecutables = SolveExecutables Bool instance Binary ReorderGoals instance Binary CountConflicts instance Binary IndependentGoals +instance Binary MinimizeConflictSet instance Binary AvoidReinstalls instance Binary ShadowPkgs instance Binary StrongFlags diff --git a/cabal-install/changelog b/cabal-install/changelog index 66860772579..7cf896c5082 100644 --- a/cabal-install/changelog +++ b/cabal-install/changelog @@ -6,6 +6,8 @@ * Improved error messages for cabal file parse errors. (#5710) * Removed support for `.zip` format source distributions (#5755) * Add "simple project" initialization option. (#5707) + * Add '--minimize-conflict-set' flag to try to improve the solver's + error message, but with an increase in run time. (#5647) * v2-test now succeeds when there are no test suites. (#5435) 2.4.1.0 Mikhail Glushenkov November 2018 diff --git a/cabal-install/solver-dsl/UnitTests/Distribution/Solver/Modular/DSL.hs b/cabal-install/solver-dsl/UnitTests/Distribution/Solver/Modular/DSL.hs index 3396a540cb4..75735c08064 100644 --- a/cabal-install/solver-dsl/UnitTests/Distribution/Solver/Modular/DSL.hs +++ b/cabal-install/solver-dsl/UnitTests/Distribution/Solver/Modular/DSL.hs @@ -646,6 +646,7 @@ exResolve :: ExampleDb -> [ExamplePkgName] -> Maybe Int -> CountConflicts + -> MinimizeConflictSet -> IndependentGoals -> ReorderGoals -> AllowBootLibInstalls @@ -658,8 +659,9 @@ exResolve :: ExampleDb -> C.Verbosity -> EnableAllTests -> Progress String String CI.SolverInstallPlan.SolverInstallPlan -exResolve db exts langs pkgConfigDb targets mbj countConflicts indepGoals - reorder allowBootLibInstalls onlyConstrained enableBj solveExes goalOrder constraints +exResolve db exts langs pkgConfigDb targets mbj countConflicts + minimizeConflictSet indepGoals reorder allowBootLibInstalls + onlyConstrained enableBj solveExes goalOrder constraints prefs verbosity enableAllTests = resolveDependencies C.buildPlatform compiler pkgConfigDb Modular params where @@ -684,6 +686,7 @@ exResolve db exts langs pkgConfigDb targets mbj countConflicts indepGoals $ addConstraints (fmap toLpc enableTests) $ addPreferences (fmap toPref prefs) $ setCountConflicts countConflicts + $ setMinimizeConflictSet minimizeConflictSet $ setIndependentGoals indepGoals $ setReorderGoals reorder $ setMaxBackjumps mbj diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs index d8b82f8ca64..66ff8971a4f 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs @@ -438,7 +438,7 @@ instance Arbitrary ProjectConfigShared where <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary - <*> arbitrary + <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary @@ -468,14 +468,15 @@ instance Arbitrary ProjectConfigShared where , projectConfigMaxBackjumps = x16 , projectConfigReorderGoals = x17 , projectConfigCountConflicts = x18 - , projectConfigStrongFlags = x19 - , projectConfigAllowBootLibInstalls = x20 - , projectConfigOnlyConstrained = x21 - , projectConfigPerComponent = x22 - , projectConfigIndependentGoals = x23 - , projectConfigConfigFile = x24 - , projectConfigProgPathExtra = x25 - , projectConfigStoreDir = x26 } = + , projectConfigMinimizeConflictSet = x19 + , projectConfigStrongFlags = x20 + , projectConfigAllowBootLibInstalls = x21 + , projectConfigOnlyConstrained = x22 + , projectConfigPerComponent = x23 + , projectConfigIndependentGoals = x24 + , projectConfigConfigFile = x25 + , projectConfigProgPathExtra = x26 + , projectConfigStoreDir = x27 } = [ ProjectConfigShared { projectConfigDistDir = x00' , projectConfigProjectFile = x01' , projectConfigHcFlavor = x02' @@ -495,25 +496,26 @@ instance Arbitrary ProjectConfigShared where , projectConfigMaxBackjumps = x16' , projectConfigReorderGoals = x17' , projectConfigCountConflicts = x18' - , projectConfigStrongFlags = x19' - , projectConfigAllowBootLibInstalls = x20' - , projectConfigOnlyConstrained = x21' - , projectConfigPerComponent = x22' - , projectConfigIndependentGoals = x23' - , projectConfigConfigFile = x24' - , projectConfigProgPathExtra = x25' - , projectConfigStoreDir = x26' } + , projectConfigMinimizeConflictSet = x19' + , projectConfigStrongFlags = x20' + , projectConfigAllowBootLibInstalls = x21' + , projectConfigOnlyConstrained = x22' + , projectConfigPerComponent = x23' + , projectConfigIndependentGoals = x24' + , projectConfigConfigFile = x25' + , projectConfigProgPathExtra = x26' + , projectConfigStoreDir = x27' } | ((x00', x01', x02', x03', x04'), (x05', x06', x07', x08', x09'), (x10', x11', x12', x13', x14', x15'), - (x16', x17', x18', x19', x20'), - x21', x22', x23', x24', x25', x26') + (x16', x17', x18', x19', x20', x21'), + x22', x23', x24', x25', x26', x27') <- shrink ((x00, x01, x02, fmap NonEmpty x03, fmap NonEmpty x04), (x05, x06, x07, x08, preShrink_Constraints x09), (x10, x11, x12, x13, x14, x15), - (x16, x17, x18, x19, x20), - x21, x22, x23, x24, x25, x26) + (x16, x17, x18, x19, x20, x21), + x22, x23, x24, x25, x26, x27) ] where preShrink_Constraints = map fst @@ -838,6 +840,9 @@ instance Arbitrary ReorderGoals where instance Arbitrary CountConflicts where arbitrary = CountConflicts <$> arbitrary +instance Arbitrary MinimizeConflictSet where + arbitrary = MinimizeConflictSet <$> arbitrary + instance Arbitrary IndependentGoals where arbitrary = IndependentGoals <$> arbitrary diff --git a/cabal-install/tests/UnitTests/Distribution/Solver/Modular/DSL/TestCaseUtils.hs b/cabal-install/tests/UnitTests/Distribution/Solver/Modular/DSL/TestCaseUtils.hs index ef8f9cb6657..2f8c7a69f59 100644 --- a/cabal-install/tests/UnitTests/Distribution/Solver/Modular/DSL/TestCaseUtils.hs +++ b/cabal-install/tests/UnitTests/Distribution/Solver/Modular/DSL/TestCaseUtils.hs @@ -4,6 +4,7 @@ module UnitTests.Distribution.Solver.Modular.DSL.TestCaseUtils ( SolverTest , SolverResult(..) , maxBackjumps + , minimizeConflictSet , independentGoals , allowBootLibInstalls , onlyConstrained @@ -53,6 +54,10 @@ import UnitTests.Options maxBackjumps :: Maybe Int -> SolverTest -> SolverTest maxBackjumps mbj test = test { testMaxBackjumps = mbj } +minimizeConflictSet :: SolverTest -> SolverTest +minimizeConflictSet test = + test { testMinimizeConflictSet = MinimizeConflictSet True } + -- | Combinator to turn on --independent-goals behavior, i.e. solve -- for the goals as if we were solving for each goal independently. independentGoals :: SolverTest -> SolverTest @@ -100,6 +105,7 @@ data SolverTest = SolverTest { , testTargets :: [String] , testResult :: SolverResult , testMaxBackjumps :: Maybe Int + , testMinimizeConflictSet :: MinimizeConflictSet , testIndepGoals :: IndependentGoals , testAllowBootLibInstalls :: AllowBootLibInstalls , testOnlyConstrained :: OnlyConstrained @@ -195,6 +201,7 @@ mkTestExtLangPC exts langs pkgConfigDb db label targets result = SolverTest { , testTargets = targets , testResult = result , testMaxBackjumps = Nothing + , testMinimizeConflictSet = MinimizeConflictSet False , testIndepGoals = IndependentGoals False , testAllowBootLibInstalls = AllowBootLibInstalls False , testOnlyConstrained = OnlyConstrainedNone @@ -216,7 +223,8 @@ runTest SolverTest{..} = askOption $ \(OptionShowSolverLog showSolverLog) -> testCase testLabel $ do let progress = exResolve testDb testSupportedExts testSupportedLangs testPkgConfigDb testTargets - testMaxBackjumps (CountConflicts True) testIndepGoals + testMaxBackjumps (CountConflicts True) + testMinimizeConflictSet testIndepGoals (ReorderGoals False) testAllowBootLibInstalls testOnlyConstrained testEnableBackjumping testSolveExecutables (sortGoals <$> testGoalOrder) testConstraints diff --git a/cabal-install/tests/UnitTests/Distribution/Solver/Modular/QuickCheck.hs b/cabal-install/tests/UnitTests/Distribution/Solver/Modular/QuickCheck.hs index ae4589a72ed..c6cf5d44262 100644 --- a/cabal-install/tests/UnitTests/Distribution/Solver/Modular/QuickCheck.hs +++ b/cabal-install/tests/UnitTests/Distribution/Solver/Modular/QuickCheck.hs @@ -143,8 +143,9 @@ solve enableBj reorder countConflicts indep goalOrder test = -- The backjump limit prevents individual tests from using -- too much time and memory. (Just defaultMaxBackjumps) - countConflicts indep reorder (AllowBootLibInstalls False) - OnlyConstrainedNone enableBj (SolveExecutables True) (unVarOrdering <$> goalOrder) + countConflicts (MinimizeConflictSet False) indep reorder + (AllowBootLibInstalls False) OnlyConstrainedNone enableBj + (SolveExecutables True) (unVarOrdering <$> goalOrder) (testConstraints test) (testPreferences test) normal (EnableAllTests False) diff --git a/cabal-install/tests/UnitTests/Distribution/Solver/Modular/Solver.hs b/cabal-install/tests/UnitTests/Distribution/Solver/Modular/Solver.hs index e4be7ebb97f..d477c37e98b 100644 --- a/cabal-install/tests/UnitTests/Distribution/Solver/Modular/Solver.hs +++ b/cabal-install/tests/UnitTests/Distribution/Solver/Modular/Solver.hs @@ -445,6 +445,10 @@ tests = [ "Backjump limit reached (currently 1, change with --max-backjumps " ++ "or try to run with --reorder-goals).\n" ++ "Failed to generate a summarized dependency solver log due to low backjump limit." + , testMinimizeConflictSet + "minimize conflict set with --minimize-conflict-set" + , testNoMinimizeConflictSet + "show original conflict set with --no-minimize-conflict-set" ] ] where @@ -1418,6 +1422,80 @@ testSummarizedLog testName mbj expectedMsg = goals :: [ExampleVar] goals = [P QualNone pkg | pkg <- ["A", "B", "C", "D"]] +dbMinimizeConflictSet :: ExampleDb +dbMinimizeConflictSet = [ + Right $ exAv "A" 3 [ExFix "B" 2, ExFix "C" 1, ExFix "D" 2] + , Right $ exAv "A" 2 [ExFix "B" 1, ExFix "C" 2, ExFix "D" 2] + , Right $ exAv "A" 1 [ExFix "B" 1, ExFix "C" 1, ExFix "D" 2] + , Right $ exAv "B" 1 [] + , Right $ exAv "C" 1 [] + , Right $ exAv "D" 1 [] + ] + +-- | Test that the solver can find a minimal conflict set with +-- --minimize-conflict-set. In the first run, the goal order causes the solver +-- to find that A-3 conflicts with B, A-2 conflicts with C, and A-1 conflicts +-- with D. The full log should show that the original final conflict set is +-- {A, B, C, D}. Then the solver should be able to reduce the conflict set to +-- {A, D}, since all versions of A conflict with D. The summarized log should +-- only mention A and D. +testMinimizeConflictSet :: String -> TestTree +testMinimizeConflictSet testName = + runTest $ minimizeConflictSet $ goalOrder goals $ setVerbose $ + mkTest dbMinimizeConflictSet testName ["A"] $ + SolverResult checkFullLog (Left (== expectedMsg)) + where + checkFullLog :: [String] -> Bool + checkFullLog = containsInOrder [ + "[__0] fail (backjumping, conflict set: A, B, C, D)" + , "Found no solution after exhaustively searching the dependency tree. " + ++ "Rerunning the dependency solver to minimize the conflict set ({A, B, C, D})." + , "Trying to remove variable \"A\" from the conflict set." + , "Failed to remove \"A\" from the conflict set. Continuing with {A, B, C, D}." + , "Trying to remove variable \"B\" from the conflict set." + , "Successfully removed \"B\" from the conflict set. Continuing with {A, C, D}." + , "Trying to remove variable \"C\" from the conflict set." + , "Successfully removed \"C\" from the conflict set. Continuing with {A, D}." + , "Trying to remove variable \"D\" from the conflict set." + , "Failed to remove \"D\" from the conflict set. Continuing with {A, D}." + ] + + expectedMsg = + "Could not resolve dependencies:\n" + ++ "[__0] trying: A-3.0.0 (user goal)\n" + ++ "[__1] next goal: D (dependency of A)\n" + ++ "[__1] rejecting: D-1.0.0 (conflict: A => D==2.0.0)\n" + ++ "[__1] fail (backjumping, conflict set: A, D)\n" + ++ "After searching the rest of the dependency tree exhaustively, these " + ++ "were the goals I've had most trouble fulfilling: A (7), D (6)" + + goals :: [ExampleVar] + goals = [P QualNone pkg | pkg <- ["A", "B", "C", "D"]] + +-- | This test uses the same packages and goal order as testMinimizeConflictSet, +-- but it doesn't set --minimize-conflict-set. The solver should print the +-- original final conflict set and the conflict between A and B. It should also +-- suggest rerunning with --minimize-conflict-set. +testNoMinimizeConflictSet :: String -> TestTree +testNoMinimizeConflictSet testName = + runTest $ goalOrder goals $ setVerbose $ + mkTest dbMinimizeConflictSet testName ["A"] $ + solverFailure (== expectedMsg) + where + expectedMsg = + "Could not resolve dependencies:\n" + ++ "[__0] trying: A-3.0.0 (user goal)\n" + ++ "[__1] next goal: B (dependency of A)\n" + ++ "[__1] rejecting: B-1.0.0 (conflict: A => B==2.0.0)\n" + ++ "[__1] fail (backjumping, conflict set: A, B)\n" + ++ "After searching the rest of the dependency tree exhaustively, " + ++ "these were the goals I've had most trouble fulfilling: " + ++ "A (7), B (2), C (2), D (2)\n" + ++ "Try running with --minimize-conflict-set to improve the error message." + + goals :: [ExampleVar] + goals = [P QualNone pkg | pkg <- ["A", "B", "C", "D"]] + {------------------------------------------------------------------------------- Simple databases for the illustrations for the backjumping blog post -------------------------------------------------------------------------------} @@ -1700,3 +1778,12 @@ dbIssue3775 = [ Right $ exAv "A" 2 [ExFix "warp" 1] `withExe` ExExe "warp" [ExAny "A"], Right $ exAv "B" 2 [ExAny "A", ExAny "warp"] ] + +-- | Returns true if the second list contains all elements of the first list, in +-- order. +containsInOrder :: Eq a => [a] -> [a] -> Bool +containsInOrder [] _ = True +containsInOrder _ [] = False +containsInOrder (x:xs) (y:ys) + | x == y = containsInOrder xs ys + | otherwise = containsInOrder (x:xs) ys