Skip to content

Commit a564348

Browse files
committed
[WIP] Add --json solver config arg
This allows the cabal-install solver logs to be mechanized, e.g., similar to the tree view provided by nix-output-monitor for Nix.
1 parent a260cde commit a564348

File tree

5 files changed

+184
-87
lines changed

5 files changed

+184
-87
lines changed

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

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import Distribution.Simple.Setup
5454
import Distribution.Simple.Utils
5555
( ordNubBy )
5656
import Distribution.Verbosity
57-
57+
import Distribution.Solver.Modular.Message (SolverTrace (..))
5858

5959
-- | Ties the two worlds together: classic cabal-install vs. the modular
6060
-- solver. Performs the necessary translations before and after.
@@ -120,25 +120,25 @@ solve' :: SolverConfig
120120
-> (PN -> PackagePreferences)
121121
-> Map PN [LabeledPackageConstraint]
122122
-> Set PN
123-
-> Progress String String (Assignment, RevDepMap)
123+
-> Progress SolverTrace String (Assignment, RevDepMap)
124124
solve' sc cinfo idx pkgConfigDB pprefs gcs pns =
125-
toProgress $ retry (runSolver printFullLog sc) createErrorMsg
125+
toProgress $ retry (runSolver printFullLog sc) handleFailure
126126
where
127127
runSolver :: Bool -> SolverConfig
128-
-> RetryLog String SolverFailure (Assignment, RevDepMap)
128+
-> RetryLog SolverTrace SolverFailure (Assignment, RevDepMap)
129129
runSolver keepLog sc' =
130130
displayLogMessages keepLog $
131131
solve sc' cinfo idx pkgConfigDB pprefs gcs pns
132132

133-
createErrorMsg :: SolverFailure
134-
-> RetryLog String String (Assignment, RevDepMap)
135-
createErrorMsg failure@(ExhaustiveSearch cs cm) =
133+
handleFailure :: SolverFailure
134+
-> RetryLog SolverTrace String (Assignment, RevDepMap)
135+
handleFailure failure@(ExhaustiveSearch cs _cm) =
136136
if asBool $ minimizeConflictSet sc
137-
then continueWith ("Found no solution after exhaustively searching the "
137+
then continueWith (mkErrorMsg ("Found no solution after exhaustively searching the "
138138
++ "dependency tree. Rerunning the dependency solver "
139139
++ "to minimize the conflict set ({"
140-
++ showConflictSet cs ++ "}).") $
141-
retry (tryToMinimizeConflictSet (runSolver printFullLog) sc cs cm) $
140+
++ showConflictSet cs ++ "}).")) $
141+
retry (tryToMinimizeConflictSet (runSolver printFullLog) sc cs _cm) $
142142
\case
143143
ExhaustiveSearch cs' cm' ->
144144
fromProgress $ Fail $
@@ -151,13 +151,13 @@ solve' sc cinfo idx pkgConfigDB pprefs gcs pns =
151151
++ "Original error message:\n"
152152
++ rerunSolverForErrorMsg cs
153153
++ finalErrorMsg sc failure
154-
else fromProgress $ Fail $
155-
rerunSolverForErrorMsg cs ++ finalErrorMsg sc failure
156-
createErrorMsg failure@BackjumpLimitReached =
154+
else
155+
fromProgress $ Fail $ rerunSolverForErrorMsg cs ++ finalErrorMsg sc failure
156+
handleFailure failure@BackjumpLimitReached =
157157
continueWith
158-
("Backjump limit reached. Rerunning dependency solver to generate "
158+
(mkErrorMsg ("Backjump limit reached. Rerunning dependency solver to generate "
159159
++ "a final conflict set for the search tree containing the "
160-
++ "first backjump.") $
160+
++ "first backjump.")) $
161161
retry (runSolver printFullLog sc { pruneAfterFirstSuccess = PruneAfterFirstSuccess True }) $
162162
\case
163163
ExhaustiveSearch cs _ ->
@@ -181,13 +181,16 @@ solve' sc cinfo idx pkgConfigDB pprefs gcs pns =
181181
-- original goal order.
182182
goalOrder' = preferGoalsFromConflictSet cs <> fromMaybe mempty (goalOrder sc)
183183

184-
in unlines ("Could not resolve dependencies:" : messages (toProgress (runSolver True sc')))
184+
in unlines ("Could not resolve dependencies:" : map show (messages (toProgress (runSolver True sc'))))
185185

186186
printFullLog = solverVerbosity sc >= verbose
187187

188188
messages :: Progress step fail done -> [step]
189189
messages = foldProgress (:) (const []) (const [])
190190

191+
mkErrorMsg :: String -> SolverTrace
192+
mkErrorMsg msg = ErrorMsg msg
193+
191194
-- | Try to remove variables from the given conflict set to create a minimal
192195
-- conflict set.
193196
--
@@ -219,13 +222,13 @@ solve' sc cinfo idx pkgConfigDB pprefs gcs pns =
219222
-- solver to add new unnecessary variables to the conflict set. This function
220223
-- discards the result from any run that adds new variables to the conflict
221224
-- set, but the end result may not be completely minimized.
222-
tryToMinimizeConflictSet :: forall a . (SolverConfig -> RetryLog String SolverFailure a)
225+
tryToMinimizeConflictSet :: forall a . (SolverConfig -> RetryLog SolverTrace SolverFailure a)
223226
-> SolverConfig
224227
-> ConflictSet
225228
-> ConflictMap
226-
-> RetryLog String SolverFailure a
229+
-> RetryLog SolverTrace SolverFailure a
227230
tryToMinimizeConflictSet runSolver sc cs cm =
228-
foldl (\r v -> retryNoSolution r $ tryToRemoveOneVar v)
231+
foldl (\r v -> retryMap mkErrorMsg $ retryNoSolution (retryMap show r) $ tryToRemoveOneVar v)
229232
(fromProgress $ Fail $ ExhaustiveSearch cs cm)
230233
(CS.toList cs)
231234
where
@@ -258,7 +261,7 @@ tryToMinimizeConflictSet runSolver sc cs cm =
258261
| otherwise =
259262
continueWith ("Trying to remove variable " ++ varStr ++ " from the "
260263
++ "conflict set.") $
261-
retry (runSolver sc') $ \case
264+
retry (retryMap show $ runSolver sc') $ \case
262265
err@(ExhaustiveSearch cs' _)
263266
| CS.toSet cs' `isSubsetOf` CS.toSet smallestKnownCS ->
264267
let msg = if not $ CS.member v cs'
@@ -297,6 +300,9 @@ tryToMinimizeConflictSet runSolver sc cs cm =
297300
ExhaustiveSearch cs' cm' -> f cs' cm'
298301
BackjumpLimitReached -> fromProgress (Fail BackjumpLimitReached)
299302

303+
retryMap :: (t -> step) -> RetryLog t fail done -> RetryLog step fail done
304+
retryMap f l = fromProgress $ (\p -> foldProgress (\x xs -> Step (f x) xs) Fail Done p) $ toProgress l
305+
300306
-- | Goal ordering that chooses goals contained in the conflict set before
301307
-- other goals.
302308
preferGoalsFromConflictSet :: ConflictSet

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ data SolverFailure =
2222
-- 'keepLog'), for efficiency.
2323
displayLogMessages :: Bool
2424
-> RetryLog Message SolverFailure a
25-
-> RetryLog String SolverFailure a
25+
-> RetryLog SolverTrace SolverFailure a
2626
displayLogMessages keepLog lg = fromProgress $
2727
if keepLog
28-
then showMessages progress
28+
then groupMessages progress
2929
else foldProgress (const id) Fail Done progress
3030
where
3131
progress = toProgress lg

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

Lines changed: 110 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
module Distribution.Solver.Modular.Message (
44
Message(..),
5-
showMessages
5+
SolverTrace(..),
6+
groupMessages,
67
) where
78

89
import qualified Data.List as L
@@ -41,51 +42,130 @@ data Message =
4142
| Success
4243
| Failure ConflictSet FailReason
4344

45+
data Log
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+
| SuccessMsg
57+
| FailureMsg ConflictSet FailReason
58+
59+
data AtLevel a = AtLevel Int a
60+
61+
type Trace = AtLevel Log
62+
63+
data SolverTrace = SolverTrace Trace | ErrorMsg String
64+
65+
instance Show SolverTrace where
66+
show (SolverTrace i) = displayMessageAtLevel i
67+
show (ErrorMsg s) = show s
68+
69+
instance Show Log where
70+
show = displayMessage
71+
72+
displayMessageAtLevel :: Trace -> String
73+
displayMessageAtLevel (AtLevel l msg) =
74+
let s = show l
75+
in "[" ++ replicate (3 - length s) '_' ++ s ++ "] " ++ displayMessage msg
76+
77+
displayMessage :: Log -> String
78+
displayMessage (PackageGoal qpn gr) = "next goal: " ++ showQPN qpn ++ showGR gr
79+
displayMessage (RejectF qfn b c fr) = "rejecting: " ++ showQFNBool qfn b ++ showFR c fr
80+
displayMessage (RejectS qsn b c fr) = "rejecting: " ++ showQSNBool qsn b ++ showFR c fr
81+
displayMessage (Skipping' cs) = showConflicts cs
82+
displayMessage (TryingF qfn b) = "trying: " ++ showQFNBool qfn b
83+
displayMessage (TryingP qpn i mgr) = "trying: " ++ showQPNPOpt qpn i ++ maybe "" showGR mgr
84+
displayMessage (TryingS qsn b) = "trying: " ++ showQSNBool qsn b
85+
displayMessage (UnknownPackage' qpn gr) = "unknown package" ++ showQPN qpn ++ showGR gr
86+
displayMessage SuccessMsg = "done"
87+
displayMessage (FailureMsg c fr) = "fail: " ++ showFR c fr
88+
displayMessage (SkipMany _ _ cs) = "skipping: " ++ showConflicts cs
89+
-- TODO: Instead of displaying `aeson-1.0.2.1, aeson-1.0.2.0, aeson-1.0.1.0, ...`,
90+
-- the following line aim to display `aeson: 1.0.2.1, 1.0.2.0, 1.0.1.0, ...`.
91+
--
92+
-- displayMessage (RejectMany qpn is c fr) = "rejecting: " ++ fmtPkgsGroupedByName (map (showQPNPOpt qpn) (reverse is)) ++ showFR c fr
93+
displayMessage (RejectMany qpn is c fr) = "rejecting: " ++ L.intercalate ", " (map (showQPNPOpt qpn) (reverse is)) ++ showFR c fr
94+
95+
-- TODO: This function should take as input the Index? So even without calling the solver, We can say things as
96+
-- "There is no version in the Hackage index that match the given constraints".
97+
--
98+
-- Alternatively, by passing this to the solver, we could get a more semantic output like:
99+
-- `all versions of aeson available are in conflict with ...`. Isn't already what `tryToMinimizeConflictSet` is doing?
100+
-- fmtPkgsGroupedByName :: [String] -> String
101+
-- fmtPkgsGroupedByName pkgs = L.intercalate " " $ fmtPkgGroup (groupByName pkgs)
102+
-- where
103+
-- groupByName :: [String] -> Map.Map String [String]
104+
-- groupByName = foldr f Map.empty
105+
-- where
106+
-- f versionString m = let (pkg, ver) = splitOnLastHyphen versionString
107+
-- in Map.insertWith (++) pkg [ver] m
108+
-- -- FIXME: This is not a very robust way to split the package name and version.
109+
-- -- I should rather retrieve the package name and version from the QPN ...
110+
-- splitOnLastHyphen :: String -> (String, String)
111+
-- splitOnLastHyphen s =
112+
-- case reverse (L.elemIndices '-' s) of
113+
-- (x:_) -> (take x s, drop (x + 1) s)
114+
-- _ -> error "splitOnLastHyphen: no hyphen found"
115+
116+
-- fmtPkgGroup :: Map.Map String [String] -> [String]
117+
-- fmtPkgGroup = map formatEntry . Map.toList
118+
-- where
119+
-- formatEntry (pkg, versions) = pkg ++ ": " ++ L.intercalate ", " versions
120+
44121
-- | Transforms the structured message type to actual messages (strings).
45122
--
46123
-- The log contains level numbers, which are useful for any trace that involves
47124
-- backtracking, because only the level numbers will allow to keep track of
48125
-- backjumps.
49-
showMessages :: Progress Message a b -> Progress String a b
50-
showMessages = go 0
126+
groupMessages :: Progress Message a b -> Progress SolverTrace a b
127+
groupMessages = go 0
51128
where
52129
-- 'go' increments the level for a recursive call when it encounters
53130
-- 'TryP', 'TryF', or 'TryS' and decrements the level when it encounters 'Leave'.
54-
go :: Int -> Progress Message a b -> Progress String a b
131+
go :: Int -> Progress Message a b -> Progress SolverTrace a b
55132
go !_ (Done x) = Done x
56133
go !_ (Fail x) = Fail x
134+
57135
-- complex patterns
58136
go !l (Step (TryP qpn i) (Step Enter (Step (Failure c fr) (Step Leave ms)))) =
59137
goPReject l qpn [i] c fr ms
138+
60139
go !l (Step (TryP qpn i) (Step Enter (Step (Skip conflicts) (Step Leave ms)))) =
61140
goPSkip l qpn [i] conflicts ms
141+
62142
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)
143+
Step (SolverTrace $ AtLevel l $ (RejectF qfn b c fr)) (go l ms)
144+
64145
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)
146+
Step (SolverTrace $ AtLevel l $ (RejectS qsn b c fr)) (go l ms)
147+
148+
-- "Trying ..." message when a new goal is started
66149
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)
150+
Step (SolverTrace $ AtLevel l $ (TryingP qpn' i (Just gr))) (go l ms)
151+
68152
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
153+
Step (SolverTrace $ AtLevel l $ (UnknownPackage' qpn gr)) (go l ms)
154+
70155
-- standard display
71156
go !l (Step Enter ms) = go (l+1) ms
72157
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
158+
159+
go !l (Step (TryP qpn i) ms) = Step (SolverTrace $ AtLevel l $ (TryingP qpn i Nothing)) (go l ms)
160+
go !l (Step (TryF qfn b) ms) = Step (SolverTrace $ AtLevel l $ (TryingF qfn b)) (go l ms)
161+
go !l (Step (TryS qsn b) ms) = Step (SolverTrace $ AtLevel l $ (TryingS qsn b)) (go l ms)
162+
go !l (Step (Next (Goal (P qpn) gr)) ms) = Step (SolverTrace $ AtLevel l $ (PackageGoal qpn gr)) (go l ms)
163+
go !l (Step (Next _) ms) = go l ms -- ignore flag goals in the log
164+
165+
-- 'Skip' should always be handled by 'goPSkip' in the case above.
166+
go !l (Step (Skip conflicts) ms) = Step (SolverTrace $ AtLevel l $ (Skipping' conflicts)) (go l ms)
167+
go !l (Step (Success) ms) = Step (SolverTrace $ AtLevel l $ SuccessMsg) (go l ms)
168+
go !l (Step (Failure c fr) ms) = Step (SolverTrace $ AtLevel l $ (FailureMsg c fr)) (go l ms)
89169

90170
-- special handler for many subsequent package rejections
91171
goPReject :: Int
@@ -94,32 +174,24 @@ showMessages = go 0
94174
-> ConflictSet
95175
-> FailReason
96176
-> Progress Message a b
97-
-> Progress String a b
177+
-> Progress SolverTrace a b
98178
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
179+
| qpn == qpn' && fr == fr' =
180+
goPReject l qpn (i : is) c fr ms
100181
goPReject l qpn is c fr ms =
101-
(atLevel l $ "rejecting: " ++ L.intercalate ", " (map (showQPNPOpt qpn) (reverse is)) ++ showFR c fr) (go l ms)
182+
Step (SolverTrace $ AtLevel l $ (RejectMany qpn is c fr)) (go l ms)
102183

103184
-- Handle many subsequent skipped package instances.
104185
goPSkip :: Int
105186
-> QPN
106187
-> [POption]
107188
-> Set CS.Conflict
108189
-> Progress Message a b
109-
-> Progress String a b
190+
-> Progress SolverTrace a b
110191
goPSkip l qpn is conflicts (Step (TryP qpn' i) (Step Enter (Step (Skip conflicts') (Step Leave ms))))
111192
| qpn == qpn' && conflicts == conflicts' = goPSkip l qpn (i : is) conflicts ms
112193
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)
117-
118-
-- write a message with the current level number
119-
atLevel :: Int -> String -> Progress String a b -> Progress String a b
120-
atLevel l x xs =
121-
let s = show l
122-
in Step ("[" ++ replicate (3 - length s) '_' ++ s ++ "] " ++ x) xs
194+
Step (SolverTrace $ AtLevel l $ (SkipMany qpn is conflicts)) (go l ms)
123195

124196
-- | Display the set of 'Conflicts' for a skipped package version.
125197
showConflicts :: Set CS.Conflict -> String

cabal-install-solver/src/Distribution/Solver/Types/DependencyResolver.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import Distribution.Simple.PackageIndex ( InstalledPackageIndex )
1717
import Distribution.Package ( PackageName )
1818
import Distribution.Compiler ( CompilerInfo )
1919
import Distribution.System ( Platform )
20+
import Distribution.Solver.Modular.Message ( SolverTrace )
2021

2122
-- | A dependency resolver is a function that works out an installation plan
2223
-- given the set of installed and available packages and a set of deps to
@@ -34,4 +35,4 @@ type DependencyResolver loc = Platform
3435
-> (PackageName -> PackagePreferences)
3536
-> [LabeledPackageConstraint]
3637
-> Set PackageName
37-
-> Progress String String [ResolverPackage loc]
38+
-> Progress SolverTrace String [ResolverPackage loc]

0 commit comments

Comments
 (0)