Skip to content

Commit 84700a2

Browse files
committed
[WIP] Fix #4251: Simplify verbose "rejecting" message in solver
This commit makes the following changes: - Enhancements to the codebase: * Adds several TODO/FIXME/HELP comments, laying the groundwork for improved solver output as described in RFC 4251 ; * Refactors the `showMessages` function to split the logic of building the output (now as a `Message'` enumeration) and the string representation of it (now formatted by `displayMessage'`). - Modifications to the solver output: * If the `-v3` or `--minimize-conflict-set` flags are not set, it now prompts the user to consider using them in cases of the "Could not resolve dependencies: ..." error ; * The message "(has the same characteristics that caused the previous version to fail: ...)" has been rephrased to "all other available packages. They are excluded due to the same constraint that caused the last version attempted to fail: ..." ; * Package lists are now grouped by name. For example, instead of displaying `aeson-1.0.2.1, aeson-1.0.2.0, aeson-1.0.1.0, ...`, it now shows `aeson: 1.0.2.1, 1.0.2.0, 1.0.1.0, ...`.
1 parent 747af13 commit 84700a2

File tree

2 files changed

+130
-45
lines changed

2 files changed

+130
-45
lines changed

cabal-install-solver/src/Distribution/Solver/Modular.hs

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ import Distribution.Solver.Modular.Index
3232
import Distribution.Solver.Modular.IndexConversion
3333
( convPIs )
3434
import Distribution.Solver.Modular.Log
35-
( SolverFailure(..), displayLogMessages )
35+
( SolverFailure(..) )
36+
import Distribution.Solver.Modular.Message
37+
( showMessages )
3638
import Distribution.Solver.Modular.Package
3739
( PN )
3840
import Distribution.Solver.Modular.RetryLog
@@ -127,8 +129,8 @@ solve' sc cinfo idx pkgConfigDB pprefs gcs pns =
127129
runSolver :: Bool -> SolverConfig
128130
-> RetryLog String SolverFailure (Assignment, RevDepMap)
129131
runSolver keepLog sc' =
130-
displayLogMessages keepLog $
131-
solve sc' cinfo idx pkgConfigDB pprefs gcs pns
132+
let progress = toProgress $ solve sc' cinfo idx pkgConfigDB pprefs gcs pns
133+
in fromProgress $ if keepLog then showMessages progress else foldProgress (const id) Fail Done progress
132134

133135
createErrorMsg :: SolverFailure
134136
-> RetryLog String String (Assignment, RevDepMap)
@@ -170,23 +172,37 @@ solve' sc cinfo idx pkgConfigDB pprefs gcs pns =
170172
++ "Failed to generate a summarized dependency solver "
171173
++ "log due to low backjump limit."
172174

175+
-- TODO: Show all backtracking by default. This is currently displayed only with `-v3' because it's too verbose and hard to decipher!
173176
rerunSolverForErrorMsg :: ConflictSet -> String
174-
rerunSolverForErrorMsg cs =
175-
let sc' = sc {
176-
goalOrder = Just goalOrder'
177-
, maxBackjumps = Just 0
178-
}
177+
rerunSolverForErrorMsg cs = unlines $ "Could not resolve dependencies:" : messages ++ suggestV3 ++ suggestMinimizeCS
178+
where
179+
messages = toMessages $ toProgress $ runSolver True sc'
180+
where
181+
toMessages :: Progress step fail done -> [step]
182+
toMessages = foldProgress (:) (const []) (const [])
179183

180-
-- Preferring goals from the conflict set takes precedence over the
181-
-- original goal order.
182-
goalOrder' = preferGoalsFromConflictSet cs <> fromMaybe mempty (goalOrder sc)
184+
sc' = sc {goalOrder = Just goalOrder', maxBackjumps = Just 0}
185+
where
186+
-- Preferring goals from the conflict set takes precedence over the
187+
-- original goal order.
188+
goalOrder' = preferGoalsFromConflictSet cs <> fromMaybe mempty (goalOrder sc)
183189

184-
in unlines ("Could not resolve dependencies:" : messages (toProgress (runSolver True sc')))
190+
suggestV3 = if printFullLog
191+
then []
192+
else ["For detailed error messages, please rerun with the `-v3' flag."]
185193

186-
printFullLog = solverVerbosity sc >= verbose
194+
suggestMinimizeCS = if asBool $ minimizeConflictSet sc
195+
then if not printFullLog
196+
then ["Warning: you may want to use the `-v3' flag to display the `--minimize-conflict-set' output."]
197+
else []
198+
else ["To improve the solver output, consider running with the `--minimize-conflict-set' option."]
199+
200+
-- TODO: If index-state is outdated (Cabal might know, right?), then suggest running `cabal update`.
201+
-- suggestCabalUpdate = ...
187202

188-
messages :: Progress step fail done -> [step]
189-
messages = foldProgress (:) (const []) (const [])
203+
-- HELP: where can I fould this info?
204+
205+
printFullLog = solverVerbosity sc >= verbose
190206

191207
-- | Try to remove variables from the given conflict set to create a minimal
192208
-- conflict set.

cabal-install-solver/src/Distribution/Solver/Modular/Message.hs

Lines changed: 99 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import Distribution.Solver.Types.PackagePath
2929
import Distribution.Solver.Types.Progress
3030
import Distribution.Types.LibraryName
3131
import Distribution.Types.UnqualComponentName
32+
import qualified Data.Map as Map
3233

3334
data Message =
3435
Enter -- ^ increase indentation level
@@ -41,6 +42,67 @@ data Message =
4142
| Success
4243
| Failure ConflictSet FailReason
4344

45+
data Message'
46+
= PackageGoal QPN QGoalReason
47+
| RejectF QFN Bool ConflictSet FailReason
48+
| RejectS QSN Bool ConflictSet FailReason
49+
| Skipping' (Set CS.Conflict)
50+
| TryingF QFN Bool
51+
| TryingP QPN POption (Maybe (GoalReason QPN))
52+
| TryingS QSN Bool
53+
| RejectMany QPN [POption] ConflictSet FailReason
54+
| SkipMany QPN [POption] (Set CS.Conflict)
55+
| UnknownPackage' QPN (GoalReason QPN)
56+
| Success'
57+
| Failure' ConflictSet FailReason
58+
59+
-- TODO: This function should take as input the Index? So even without calling the solver, I con say things as
60+
-- "There is no version in the Hackage index that match the given constraints".
61+
--
62+
-- Alternatively, by passing this to the solver, we could get a more semantic output like:
63+
-- `all versions of aeson available are in conflict with ...`. Isn't already what `tryToMinimizeConflictSet` is doing?
64+
fmtPkgsGroupedByName :: [String] -> String
65+
fmtPkgsGroupedByName pkgs = L.intercalate " " $ fmtPkgGroup (groupByName pkgs)
66+
where
67+
groupByName :: [String] -> Map.Map String [String]
68+
groupByName = foldr f Map.empty
69+
where
70+
f versionString m = let (pkg, ver) = splitOnLastHyphen versionString
71+
in Map.insertWith (++) pkg [ver] m
72+
-- FIXME: This is not a very robust way to split the package name and version.
73+
-- I should rather retrieve the package name and version from the QPN ...
74+
splitOnLastHyphen :: String -> (String, String)
75+
splitOnLastHyphen s =
76+
case reverse (L.elemIndices '-' s) of
77+
(x:_) -> (take x s, drop (x + 1) s)
78+
_ -> error "splitOnLastHyphen: no hyphen found"
79+
80+
fmtPkgGroup :: Map.Map String [String] -> [String]
81+
fmtPkgGroup = map formatEntry . Map.toList
82+
where
83+
formatEntry (pkg, versions) = pkg ++ ": " ++ L.intercalate ", " versions
84+
85+
displayMessage' :: Message' -> String
86+
displayMessage' (PackageGoal qpn gr) = "next goal: " ++ showQPN qpn ++ showGR gr
87+
displayMessage' (RejectF qfn b c fr) = "rejecting: " ++ showQFNBool qfn b ++ showFR c fr
88+
displayMessage' (RejectS qsn b c fr) = "rejecting: " ++ showQSNBool qsn b ++ showFR c fr
89+
displayMessage' (Skipping' cs) = showConflicts cs
90+
displayMessage' (TryingF qfn b) = "trying: " ++ showQFNBool qfn b
91+
displayMessage' (TryingP qpn i mgr) = "trying: " ++ showQPNPOpt qpn i ++ maybe "" showGR mgr
92+
displayMessage' (TryingS qsn b) = "trying: " ++ showQSNBool qsn b
93+
displayMessage' (UnknownPackage' qpn gr) = "unknown package: " ++ showQPN qpn ++ showGR gr
94+
displayMessage' Success' = "done"
95+
displayMessage' (Failure' c fr) = "fail" ++ showFR c fr
96+
97+
-- E.g. instead of displaying `aeson-1.0.2.1, aeson-1.0.2.0, aeson-1.0.1.0, ...`,
98+
-- aim to display `aeson: 1.0.2.1, 1.0.2.0, 1.0.1.0, ...`.
99+
displayMessage' (SkipMany _ _ cs) = "skipping: " ++ showConflicts cs -- Here, I'm not sure to see the point of displaying the list of packages since the constraint is already displayed (TODO: display it only in -v3 ...)
100+
displayMessage' (RejectMany qpn is c fr) = "rejecting: " ++ fmtPkgsGroupedByName (map (showQPNPOpt qpn) (reverse is)) ++ showFR c fr
101+
-- HELP: How to pass the verbosity level to this function?
102+
103+
-- Optionnal: I have yet no clue of how to display the solver output as a "tree",
104+
-- as suggested in the proposal https://github.com/haskell/cabal/issues/8939
105+
44106
-- | Transforms the structured message type to actual messages (strings).
45107
--
46108
-- The log contains level numbers, which are useful for any trace that involves
@@ -54,38 +116,42 @@ showMessages = go 0
54116
go :: Int -> Progress Message a b -> Progress String a b
55117
go !_ (Done x) = Done x
56118
go !_ (Fail x) = Fail x
119+
-- TODO: I should use the the level of recursion to change the displayMessage' indentation level ...
120+
57121
-- complex patterns
58122
go !l (Step (TryP qpn i) (Step Enter (Step (Failure c fr) (Step Leave ms)))) =
59123
goPReject l qpn [i] c fr ms
124+
60125
go !l (Step (TryP qpn i) (Step Enter (Step (Skip conflicts) (Step Leave ms)))) =
61126
goPSkip l qpn [i] conflicts ms
127+
62128
go !l (Step (TryF qfn b) (Step Enter (Step (Failure c fr) (Step Leave ms)))) =
63-
(atLevel l $ "rejecting: " ++ showQFNBool qfn b ++ showFR c fr) (go l ms)
129+
(atLevel l $ displayMessage' (RejectF qfn b c fr)) (go l ms)
130+
64131
go !l (Step (TryS qsn b) (Step Enter (Step (Failure c fr) (Step Leave ms)))) =
65-
(atLevel l $ "rejecting: " ++ showQSNBool qsn b ++ showFR c fr) (go l ms)
132+
(atLevel l $ displayMessage' (RejectS qsn b c fr)) (go l ms)
133+
134+
-- "Trying ..." message when a new goal is started
66135
go !l (Step (Next (Goal (P _ ) gr)) (Step (TryP qpn' i) ms@(Step Enter (Step (Next _) _)))) =
67-
(atLevel l $ "trying: " ++ showQPNPOpt qpn' i ++ showGR gr) (go l ms)
136+
(atLevel l $ displayMessage' (TryingP qpn' i (Just gr))) (go l ms)
137+
68138
go !l (Step (Next (Goal (P qpn) gr)) (Step (Failure _c UnknownPackage) ms)) =
69-
atLevel l ("unknown package: " ++ showQPN qpn ++ showGR gr) $ go l ms
139+
(atLevel l $ displayMessage' (UnknownPackage' qpn gr)) (go l ms)
140+
70141
-- standard display
71142
go !l (Step Enter ms) = go (l+1) ms
72143
go !l (Step Leave ms) = go (l-1) ms
73-
go !l (Step (TryP qpn i) ms) = (atLevel l $ "trying: " ++ showQPNPOpt qpn i) (go l ms)
74-
go !l (Step (TryF qfn b) ms) = (atLevel l $ "trying: " ++ showQFNBool qfn b) (go l ms)
75-
go !l (Step (TryS qsn b) ms) = (atLevel l $ "trying: " ++ showQSNBool qsn b) (go l ms)
76-
go !l (Step (Next (Goal (P qpn) gr)) ms) = (atLevel l $ showPackageGoal qpn gr) (go l ms)
77-
go !l (Step (Next _) ms) = go l ms -- ignore flag goals in the log
78-
go !l (Step (Skip conflicts) ms) =
79-
-- 'Skip' should always be handled by 'goPSkip' in the case above.
80-
(atLevel l $ "skipping: " ++ showConflicts conflicts) (go l ms)
81-
go !l (Step (Success) ms) = (atLevel l $ "done") (go l ms)
82-
go !l (Step (Failure c fr) ms) = (atLevel l $ showFailure c fr) (go l ms)
83-
84-
showPackageGoal :: QPN -> QGoalReason -> String
85-
showPackageGoal qpn gr = "next goal: " ++ showQPN qpn ++ showGR gr
86-
87-
showFailure :: ConflictSet -> FailReason -> String
88-
showFailure c fr = "fail" ++ showFR c fr
144+
145+
go !l (Step (TryP qpn i) ms) = (atLevel l $ displayMessage' (TryingP qpn i Nothing)) (go l ms)
146+
go !l (Step (TryF qfn b) ms) = (atLevel l $ displayMessage' (TryingF qfn b)) (go l ms)
147+
go !l (Step (TryS qsn b) ms) = (atLevel l $ displayMessage' (TryingS qsn b)) (go l ms)
148+
go !l (Step (Next (Goal (P qpn) gr)) ms) = (atLevel l $ displayMessage' (PackageGoal qpn gr)) (go l ms)
149+
go !l (Step (Next _) ms) = go l ms -- ignore flag goals in the log
150+
151+
-- 'Skip' should always be handled by 'goPSkip' in the case above.
152+
go !l (Step (Skip conflicts) ms) = (atLevel l $ displayMessage' (Skipping' conflicts)) (go l ms)
153+
go !l (Step (Success) ms) = (atLevel l $ displayMessage' Success') (go l ms)
154+
go !l (Step (Failure c fr) ms) = (atLevel l $ displayMessage' (Failure' c fr)) (go l ms)
89155

90156
-- special handler for many subsequent package rejections
91157
goPReject :: Int
@@ -96,9 +162,10 @@ showMessages = go 0
96162
-> Progress Message a b
97163
-> Progress String a b
98164
goPReject l qpn is c fr (Step (TryP qpn' i) (Step Enter (Step (Failure _ fr') (Step Leave ms))))
99-
| qpn == qpn' && fr == fr' = goPReject l qpn (i : is) c fr ms
165+
| qpn == qpn' && fr == fr' =
166+
goPReject l qpn (i : is) c fr ms
100167
goPReject l qpn is c fr ms =
101-
(atLevel l $ "rejecting: " ++ L.intercalate ", " (map (showQPNPOpt qpn) (reverse is)) ++ showFR c fr) (go l ms)
168+
(atLevel l $ displayMessage' (RejectMany qpn is c fr)) (go l ms)
102169

103170
-- Handle many subsequent skipped package instances.
104171
goPSkip :: Int
@@ -110,10 +177,7 @@ showMessages = go 0
110177
goPSkip l qpn is conflicts (Step (TryP qpn' i) (Step Enter (Step (Skip conflicts') (Step Leave ms))))
111178
| qpn == qpn' && conflicts == conflicts' = goPSkip l qpn (i : is) conflicts ms
112179
goPSkip l qpn is conflicts ms =
113-
let msg = "skipping: "
114-
++ L.intercalate ", " (map (showQPNPOpt qpn) (reverse is))
115-
++ showConflicts conflicts
116-
in atLevel l msg (go l ms)
180+
(atLevel l $ displayMessage' (SkipMany qpn is conflicts)) (go l ms)
117181

118182
-- write a message with the current level number
119183
atLevel :: Int -> String -> Progress String a b -> Progress String a b
@@ -124,8 +188,8 @@ showMessages = go 0
124188
-- | Display the set of 'Conflicts' for a skipped package version.
125189
showConflicts :: Set CS.Conflict -> String
126190
showConflicts conflicts =
127-
" (has the same characteristics that caused the previous version to fail: "
128-
++ conflictMsg ++ ")"
191+
"all other available packages. They are excluded by the same constraint that caused the last version tried to fail:\n"
192+
++ conflictMsg -- FIXME: Is this message important to highlight to user? It's currently easy to miss...
129193
where
130194
conflictMsg :: String
131195
conflictMsg =
@@ -220,7 +284,7 @@ showFR :: ConflictSet -> FailReason -> String
220284
showFR _ (UnsupportedExtension ext) = " (conflict: requires " ++ showUnsupportedExtension ext ++ ")"
221285
showFR _ (UnsupportedLanguage lang) = " (conflict: requires " ++ showUnsupportedLanguage lang ++ ")"
222286
showFR _ (MissingPkgconfigPackage pn vr) = " (conflict: pkg-config package " ++ prettyShow pn ++ prettyShow vr ++ ", not found in the pkg-config database)"
223-
showFR _ (NewPackageDoesNotMatchExistingConstraint d) = " (conflict: " ++ showConflictingDep d ++ ")"
287+
showFR _ (NewPackageDoesNotMatchExistingConstraint d) = " (conflict: new package does not match existing constraint " ++ showConflictingDep d ++ ")"
224288
showFR _ (ConflictingConstraints d1 d2) = " (conflict: " ++ L.intercalate ", " (L.map showConflictingDep [d1, d2]) ++ ")"
225289
showFR _ (NewPackageIsMissingRequiredComponent comp dr) = " (does not contain " ++ showExposedComponent comp ++ ", which is required by " ++ showDependencyReason dr ++ ")"
226290
showFR _ (NewPackageHasPrivateRequiredComponent comp dr) = " (" ++ showExposedComponent comp ++ " is private, but it is required by " ++ showDependencyReason dr ++ ")"
@@ -258,8 +322,13 @@ showExposedComponent (ExposedExe name) = "executable '" ++ unUnqua
258322
constraintSource :: ConstraintSource -> String
259323
constraintSource src = "constraint from " ++ showConstraintSource src
260324

325+
-- FIXME: use a ANSI formating library like ansi-terminal to achieve this?
326+
-- bold :: String -> String
327+
-- bold str = "\ESC[1m" ++ str ++ "\ESC[0m"
328+
--
329+
-- The point would be to highlight the conflict in solver output:
261330
showConflictingDep :: ConflictingDep -> String
262-
showConflictingDep (ConflictingDep dr (PkgComponent qpn comp) ci) =
331+
showConflictingDep (ConflictingDep dr (PkgComponent qpn comp) ci) = -- bold $
263332
let DependencyReason qpn' _ _ = dr
264333
componentStr = case comp of
265334
ExposedExe exe -> " (exe " ++ unUnqualComponentName exe ++ ")"

0 commit comments

Comments
 (0)