@@ -1,3 +1,4 @@
1+{-# LANGUAGE OverloadedStrings #-}
12module Install
23 ( Args(..)
34 , run
@@ -5,15 +6,23 @@ module Install
56 where
67
78
8−import Control.Monad.Trans (liftIO)
9+import Data.Map ((!))
10+import qualified Data.Map as Map
11+import qualified Data.Map.Merge.Strict as Map
912
10−import qualified Elm.Install as Install
11−import qualified Elm.PerUserCache as PerUserCache
13+import qualified Deps.Solver as Solver
14+import qualified Deps.Registry as Registry
15+import qualified Elm.Constraint as C
16+import qualified Elm.Details as Details
1217import qualified Elm.Package as Pkg
18+import qualified Elm.Outline as Outline
19+import qualified Elm.Version as V
20+import qualified Reporting
21+import Reporting.Doc ((<>), (<+>))
22+import qualified Reporting.Doc as D
1323import qualified Reporting.Exit as Exit
14−import qualified Reporting.Exit.Install as E
1524import qualified Reporting.Task as Task
16−import qualified Reporting.Progress.Terminal as Terminal
25+import qualified Stuff
1726
1827
1928
@@ -27,12 +36,405 @@ data Args
2736
2837run :: Args -> () -> IO ()
2938run args () =
30− do reporter <- Terminal.create
31− Task.run reporter $
32− case args of
33− NoArgs ->
34− do elmHome <- liftIO PerUserCache.getElmHome
35− Task.throw (Exit.Install (E.NoArgs elmHome))
36−
37− Install pkg ->
38− Install.install pkg
39+ Reporting.attempt Exit.installToReport $
40+ do maybeRoot <- Stuff.findRoot
41+ case maybeRoot of
42+ Nothing ->
43+ return (Left Exit.InstallNoProject)
44+
45+ Just root ->
46+ case args of
47+ NoArgs ->
48+ do elmHome <- Stuff.getElmHome
49+ return (Left (Exit.InstallNoArgs elmHome))
50+
51+ Install pkg ->
52+ Task.run $
53+ do env <- getEnv
54+ oldOutline <- getOutline root
55+ case oldOutline of
56+ Outline.App outline ->
57+ do changes <- makeAppPlan env pkg outline
58+ attemptChanges root env oldOutline V.toChars changes
59+
60+ Outline.Pkg outline ->
61+ do changes <- makePkgPlan env pkg outline
62+ attemptChanges root env oldOutline C.toChars changes
63+
64+
65+
66+getEnv :: Task Solver.Env
67+getEnv =
68+ do eitherEnv <- Task.io Solver.initEnv
69+ case eitherEnv of
70+ Right env -> return env
71+ Left problem -> Task.throw (error "TODO getEnv" problem)
72+
73+
74+getOutline :: FilePath -> Task Outline.Outline
75+getOutline root =
76+ do eitherOutline <- Task.io (Outline.read root)
77+ case eitherOutline of
78+ Right outline -> return outline
79+ Left exit -> Task.throw (error "TODO getOutline" exit)
80+
81+
82+
83+-- ATTEMPT CHANGES
84+
85+
86+data Changes vsn
87+ = AlreadyInstalled
88+ | PromoteTest Outline.Outline
89+ | PromoteIndirect Outline.Outline
90+ | Changes (Map.Map Pkg.Name (Change vsn)) Outline.Outline
91+
92+
93+type Task = Task.Task Exit.Install
94+
95+
96+attemptChanges :: FilePath -> Solver.Env -> Outline.Outline -> (a -> String) -> Changes a -> Task ()
97+attemptChanges root env oldOutline toChars changes =
98+ case changes of
99+ AlreadyInstalled ->
100+ Task.io $ putStrLn "It is already installed!"
101+
102+ PromoteIndirect newOutline ->
103+ attemptChangesHelp root env oldOutline newOutline $
104+ D.vcat
105+ [ D.fillSep
106+ ["I","found","it","in","your","elm.json","file,"
107+ ,"but","in","the",D.dullyellow "\"indirect\"","dependencies."
108+ ]
109+ , D.fillSep
110+ ["Should","I","move","it","into",D.green "\"direct\""
111+ ,"dependencies","for","more","general","use?","[Y/n]: "
112+ ]
113+ ]
114+
115+ PromoteTest newOutline ->
116+ attemptChangesHelp root env oldOutline newOutline $
117+ D.vcat
118+ [ D.fillSep
119+ ["I","found","it","in","your","elm.json","file,"
120+ ,"but","in","the",D.dullyellow "\"test-dependencies\"","field."
121+ ]
122+ , D.fillSep
123+ ["Should","I","move","it","into",D.green "\"dependencies\""
124+ ,"for","more","general","use?","[Y/n]: "
125+ ]
126+ ]
127+
128+ Changes changeDict newOutline ->
129+ let
130+ widths = Map.foldrWithKey (widen toChars) (Widths 0 0 0) changeDict
131+ changeDocs = Map.foldrWithKey (addChange toChars widths) (Docs [] [] []) changeDict
132+ in
133+ attemptChangesHelp root env oldOutline newOutline $ D.vcat $
134+ [ "Here is my plan:"
135+ , viewChangeDocs changeDocs
136+ , ""
137+ , "Would you like me to update your elm.json accordingly? [Y/n]: "
138+ ]
139+
140+
141+attemptChangesHelp :: FilePath -> Solver.Env -> Outline.Outline -> Outline.Outline -> D.Doc -> Task ()
142+attemptChangesHelp root env oldOutline newOutline question =
143+ Task.eio Exit.InstallHasBadDetails $
144+ do approved <- Reporting.ask question
145+ if approved
146+ then
147+ do Outline.write root newOutline
148+ result <- Details.verifyInstall root env newOutline
149+ case result of
150+ Left exit ->
151+ do Outline.write root oldOutline
152+ return (Left exit)
153+
154+ Right () ->
155+ do putStrLn "Success!"
156+ return (Right ())
157+ else
158+ do putStrLn "Okay, I did not change anything!"
159+ return (Right ())
160+
161+
162+
163+-- MAKE APP PLAN
164+
165+
166+makeAppPlan :: Solver.Env -> Pkg.Name -> Outline.AppOutline -> Task (Changes V.Version)
167+makeAppPlan (Solver.Env cache _ connection registry) pkg outline@(Outline.AppOutline _ _ direct indirect testDirect testIndirect) =
168+ if Map.member pkg direct then
169+ return AlreadyInstalled
170+
171+ else
172+ -- is it already indirect?
173+ case Map.lookup pkg indirect of
174+ Just vsn ->
175+ return $ PromoteIndirect $ Outline.App $
176+ outline
177+ { Outline._app_deps_direct = Map.insert pkg vsn direct
178+ , Outline._app_deps_indirect = Map.delete pkg indirect
179+ }
180+
181+ Nothing ->
182+ -- is it already a test dependency?
183+ case Map.lookup pkg testDirect of
184+ Just vsn ->
185+ return $ PromoteTest $ Outline.App $
186+ outline
187+ { Outline._app_deps_direct = Map.insert pkg vsn direct
188+ , Outline._app_test_direct = Map.delete pkg testDirect
189+ }
190+
191+ Nothing ->
192+ -- is it already an indirect test dependency?
193+ case Map.lookup pkg testIndirect of
194+ Just vsn ->
195+ return $ PromoteTest $ Outline.App $
196+ outline
197+ { Outline._app_deps_direct = Map.insert pkg vsn direct
198+ , Outline._app_test_indirect = Map.delete pkg testIndirect
199+ }
200+
201+ Nothing ->
202+ -- finally try to add it from scratch
203+ case Registry.getVersions' pkg registry of
204+ Left suggestions ->
205+ case connection of
206+ Solver.Online _ -> Task.throw (Exit.InstallUnknownPackageOnline suggestions)
207+ Solver.Offline -> Task.throw (Exit.InstallUnknownPackageOffline suggestions)
208+
209+ Right _ ->
210+ do result <- Task.io $ Solver.addToApp cache connection registry pkg outline
211+ case result of
212+ Solver.Ok (Solver.AppSolution old new app) ->
213+ return (Changes (detectChanges old new) (Outline.App app))
214+
215+ Solver.NoSolution ->
216+ Task.throw Exit.InstallNoOnlineSolution
217+
218+ Solver.NoOfflineSolution ->
219+ Task.throw Exit.InstallNoOfflineSolution
220+
221+ Solver.Err exit ->
222+ Task.throw (Exit.InstallHadSolverTrouble exit)
223+
224+
225+
226+-- MAKE PACKAGE PLAN
227+
228+
229+makePkgPlan :: Solver.Env -> Pkg.Name -> Outline.PkgOutline -> Task (Changes C.Constraint)
230+makePkgPlan (Solver.Env cache _ connection registry) pkg outline@(Outline.PkgOutline _ _ _ _ _ deps test _) =
231+ if Map.member pkg deps then
232+ return AlreadyInstalled
233+ else
234+ -- is already in test dependencies?
235+ case Map.lookup pkg test of
236+ Just con ->
237+ return $ PromoteTest $ Outline.Pkg $
238+ outline
239+ { Outline._pkg_deps = Map.insert pkg con deps
240+ , Outline._pkg_test_deps = Map.delete pkg test
241+ }
242+
243+ Nothing ->
244+ -- try to add a new dependency
245+ case Registry.getVersions' pkg registry of
246+ Left suggestions ->
247+ case connection of
248+ Solver.Online _ -> Task.throw (Exit.InstallUnknownPackageOnline suggestions)
249+ Solver.Offline -> Task.throw (Exit.InstallUnknownPackageOffline suggestions)
250+
251+ Right (Registry.KnownVersions _ _) ->
252+ do let old = Map.union deps test
253+ let cons = Map.insert pkg C.anything old
254+ result <- Task.io $ Solver.verify cache connection registry cons
255+ case result of
256+ Solver.Ok solution ->
257+ let
258+ (Solver.Details vsn _) = solution ! pkg
259+
260+ con = C.untilNextMajor vsn
261+ new = Map.insert pkg con old
262+ changes = detectChanges old new
263+ news = Map.mapMaybe keepNew changes
264+ in
265+ return $ Changes changes $ Outline.Pkg $
266+ outline
267+ { Outline._pkg_deps = addNews (Just pkg) news deps
268+ , Outline._pkg_test_deps = addNews Nothing news test
269+ }
270+
271+ Solver.NoSolution ->
272+ Task.throw Exit.InstallNoOnlineSolution
273+
274+ Solver.NoOfflineSolution ->
275+ Task.throw Exit.InstallNoOfflineSolution
276+
277+ Solver.Err exit ->
278+ Task.throw (Exit.InstallHadSolverTrouble exit)
279+
280+
281+addNews :: Maybe Pkg.Name -> Map.Map Pkg.Name C.Constraint -> Map.Map Pkg.Name C.Constraint -> Map.Map Pkg.Name C.Constraint
282+addNews pkg new old =
283+ Map.merge
284+ Map.preserveMissing
285+ (Map.mapMaybeMissing (\k c -> if Just k == pkg then Just c else Nothing))
286+ (Map.zipWithMatched (\_ _ n -> n))
287+ old
288+ new
289+
290+
291+
292+-- CHANGES
293+
294+
295+data Change a
296+ = Insert a
297+ | Change a a
298+ | Remove a
299+
300+
301+detectChanges :: (Eq a) => Map.Map Pkg.Name a -> Map.Map Pkg.Name a -> Map.Map Pkg.Name (Change a)
302+detectChanges old new =
303+ Map.merge
304+ (Map.mapMissing (\_ v -> Remove v))
305+ (Map.mapMissing (\_ v -> Insert v))
306+ (Map.zipWithMaybeMatched keepChange)
307+ old
308+ new
309+
310+
311+keepChange :: (Eq v) => k -> v -> v -> Maybe (Change v)
312+keepChange _ old new =
313+ if old == new then
314+ Nothing
315+ else
316+ Just (Change old new)
317+
318+
319+keepNew :: Change a -> Maybe a
320+keepNew change =
321+ case change of
322+ Insert a ->
323+ Just a
324+
325+ Change _ a ->
326+ Just a
327+
328+ Remove _ ->
329+ Nothing
330+
331+
332+
333+-- VIEW CHANGE DOCS
334+
335+
336+data ChangeDocs =
337+ Docs
338+ { _doc_inserts :: [D.Doc]
339+ , _doc_changes :: [D.Doc]
340+ , _doc_removes :: [D.Doc]
341+ }
342+
343+
344+viewChangeDocs :: ChangeDocs -> D.Doc
345+viewChangeDocs (Docs inserts changes removes) =
346+ D.indent 2 $ D.vcat $ concat $
347+ [ viewNonZero "Add:" inserts
348+ , viewNonZero "Change:" changes
349+ , viewNonZero "Remove:" removes
350+ ]
351+
352+
353+viewNonZero :: String -> [D.Doc] -> [D.Doc]
354+viewNonZero title entries =
355+ if null entries then
356+ []
357+ else
358+ [ ""
359+ , D.fromChars title
360+ , D.indent 2 (D.vcat entries)
361+ ]
362+
363+
364+
365+-- VIEW CHANGE
366+
367+
368+addChange :: (a -> String) -> Widths -> Pkg.Name -> Change a -> ChangeDocs -> ChangeDocs
369+addChange toChars widths name change (Docs inserts changes removes) =
370+ case change of
371+ Insert new ->
372+ Docs (viewInsert toChars widths name new : inserts) changes removes
373+
374+ Change old new ->
375+ Docs inserts (viewChange toChars widths name old new : changes) removes
376+
377+ Remove old ->
378+ Docs inserts changes (viewRemove toChars widths name old : removes)
379+
380+
381+viewInsert :: (a -> String) -> Widths -> Pkg.Name -> a -> D.Doc
382+viewInsert toChars (Widths nameWidth leftWidth _) name new =
383+ viewName nameWidth name <+> pad leftWidth (toChars new)
384+
385+
386+viewChange :: (a -> String) -> Widths -> Pkg.Name -> a -> a -> D.Doc
387+viewChange toChars (Widths nameWidth leftWidth rightWidth) name old new =
388+ D.hsep
389+ [ viewName nameWidth name
390+ , pad leftWidth (toChars old)
391+ , "=>"
392+ , pad rightWidth (toChars new)
393+ ]
394+
395+
396+viewRemove :: (a -> String) -> Widths -> Pkg.Name -> a -> D.Doc
397+viewRemove toChars (Widths nameWidth leftWidth _) name old =
398+ viewName nameWidth name <+> pad leftWidth (toChars old)
399+
400+
401+viewName :: Int -> Pkg.Name -> D.Doc
402+viewName width name =
403+ D.fill (width + 3) (D.fromUtf8 (Pkg.toString name))
404+
405+
406+pad :: Int -> String -> D.Doc
407+pad width string =
408+ D.fromChars (replicate (width - length string) ' ') <> D.fromChars string
409+
410+
411+
412+-- WIDTHS
413+
414+
415+data Widths =
416+ Widths
417+ { _name :: !Int
418+ , _left :: !Int
419+ , _right :: !Int
420+ }
421+
422+
423+widen :: (a -> String) -> Pkg.Name -> Change a -> Widths -> Widths
424+widen toChars pkg change (Widths name left right) =
425+ let
426+ toLength a =
427+ length (toChars a)
428+
429+ newName =
430+ max name (length (Pkg.toChars pkg))
431+ in
432+ case change of
433+ Insert new ->
434+ Widths newName (max left (toLength new)) right
435+
436+ Change old new ->
437+ Widths newName (max left (toLength old)) (max right (toLength new))
438+
439+ Remove old ->
440+ Widths newName (max left (toLength old)) right