Skip to content

Commit 6c95e3b

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 9f37325 commit 6c95e3b

File tree

5 files changed

+178
-87
lines changed

5 files changed

+178
-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: 111 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
{-# LANGUAGE BangPatterns #-}
2+
{-# LANGUAGE InstanceSigs #-}
23

34
module Distribution.Solver.Modular.Message (
45
Message(..),
5-
showMessages
6+
SolverTrace(..),
7+
groupMessages,
68
) where
79

810
import qualified Data.List as L
@@ -41,51 +43,130 @@ data Message =
4143
| Success
4244
| Failure ConflictSet FailReason
4345

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

90171
-- special handler for many subsequent package rejections
91172
goPReject :: Int
@@ -94,32 +175,24 @@ showMessages = go 0
94175
-> ConflictSet
95176
-> FailReason
96177
-> Progress Message a b
97-
-> Progress String a b
178+
-> Progress SolverTrace a b
98179
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
180+
| qpn == qpn' && fr == fr' =
181+
goPReject l qpn (i : is) c fr ms
100182
goPReject l qpn is c fr ms =
101-
(atLevel l $ "rejecting: " ++ L.intercalate ", " (map (showQPNPOpt qpn) (reverse is)) ++ showFR c fr) (go l ms)
183+
Step (SolverTrace $ AtLevel l $ (RejectMany qpn is c fr)) (go l ms)
102184

103185
-- Handle many subsequent skipped package instances.
104186
goPSkip :: Int
105187
-> QPN
106188
-> [POption]
107189
-> Set CS.Conflict
108190
-> Progress Message a b
109-
-> Progress String a b
191+
-> Progress SolverTrace a b
110192
goPSkip l qpn is conflicts (Step (TryP qpn' i) (Step Enter (Step (Skip conflicts') (Step Leave ms))))
111193
| qpn == qpn' && conflicts == conflicts' = goPSkip l qpn (i : is) conflicts ms
112194
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
195+
Step (SolverTrace $ AtLevel l $ (SkipMany qpn is conflicts)) (go l ms)
123196

124197
-- | Display the set of 'Conflicts' for a skipped package version.
125198
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)