on master help sign in
Start moving to monorepo. Move evancz/builder in.
1c3cda7Evan Czaplicki
builder/builder.cabalnew file+123
@@ -0,0 +1,123 @@
1+Name: builder
2+Version: 0.19.0
3+
4+Synopsis:
5+ Library to download and build Elm projects
6+Description:
7+ Solves dependencies, crawls Elm files, parallelizes downloads,
8+ parallelizes compilation, reports coherent errors, etc.
9+
10+Homepage:
11+ http://github.com/evancz/builder
12+
13+License: BSD3
14+License-file: LICENSE
15+
16+Author: Evan Czaplicki
17+Maintainer: info@elm-lang.org
18+Copyright: Copyright: (c) 2013-present Evan Czaplicki
19+
20+Category: Language
21+
22+Build-type: Simple
23+Cabal-version: >=1.9
24+
25+source-repository head
26+ type: git
27+ location: git://github.com/evancz/builder.git
28+
29+Flag dev {
30+ Description: Turn off optimization and make warnings errors
31+ Default: False
32+}
33+
34+
35+Library
36+ if flag(dev)
37+ ghc-options: -O0 -Wall -Werror
38+ else
39+ ghc-options: -threaded -O2 -fsimpl-tick-factor=140
40+
41+ Hs-Source-Dirs:
42+ src
43+
44+ exposed-modules:
45+ Elm.Bump,
46+ Elm.Diff,
47+ Elm.Install,
48+ Elm.PerUserCache,
49+ Elm.Project,
50+ Elm.Project.Constraint,
51+ Elm.Project.Flags,
52+ Elm.Project.Json,
53+ Elm.Project.Licenses,
54+ Elm.Publish,
55+ File.IO,
56+ Reporting.Progress.Repl,
57+ Reporting.Progress.Terminal,
58+ Reporting.Task
59+
60+ other-modules:
61+ Deps.Diff,
62+ Deps.Explorer,
63+ Deps.Get,
64+ Deps.Solver,
65+ Deps.Verify,
66+ Deps.Website,
67+ Elm.Project.Root,
68+ Elm.Project.Summary,
69+ File.Args,
70+ File.Artifacts,
71+ File.Compile,
72+ File.Crawl,
73+ File.Find,
74+ File.Hash,
75+ File.Header,
76+ File.Plan,
77+ Generate.Functions,
78+ Generate.Output,
79+ Reporting.Error,
80+ Reporting.Error.Assets,
81+ Reporting.Error.Bump,
82+ Reporting.Error.Compile,
83+ Reporting.Error.Crawl,
84+ Reporting.Error.Deps,
85+ Reporting.Error.Diff,
86+ Reporting.Error.Help,
87+ Reporting.Error.Http,
88+ Reporting.Error.Publish,
89+ Reporting.Progress,
90+ Reporting.Progress.Bar,
91+ Reporting.Task.Http,
92+ Stuff.Paths,
93+ Stuff.Verify
94+
95+ Build-depends:
96+ ansi-wl-pprint >= 0.6.7.3,
97+ base >=4.2 && <5,
98+ binary >= 0.8 && < 0.9,
99+ blaze-html,
100+ blaze-markup,
101+ containers >= 0.3 && < 0.6,
102+ bytestring >= 0.9 && < 0.11,
103+ directory >= 1.2.3.0 && < 2.0,
104+ elm-compiler == 0.19.0,
105+ filepath >= 1 && < 2.0,
106+ fsnotify >= 0.2.1,
107+ HTTP >= 4000.2.5 && < 4000.4,
108+ http-client >= 0.5 && < 0.6,
109+ http-client-tls >= 0.3 && < 0.4,
110+ http-types >= 0.9 && < 1.0,
111+ logict,
112+ mtl >= 2.2.1 && < 3,
113+ pretty,
114+ process,
115+ raw-strings-qq,
116+ scientific,
117+ semigroups,
118+ SHA,
119+ text,
120+ time,
121+ unordered-containers,
122+ vector,
123+ zip-archive
builder/src/Deps/Diff.hsnew file+356
@@ -0,0 +1,356 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Deps.Diff
4+ ( diff
5+ , PackageChanges(..)
6+ , ModuleChanges(..)
7+ , Changes(..)
8+ , Magnitude(..)
9+ , moduleChangeMagnitude
10+ , magnitudeToString
11+ , toMagnitude
12+ , bump
13+ )
14+ where
15+
16+
17+import Control.Monad (zipWithM)
18+import Data.Function (on)
19+import qualified Data.List as List
20+import qualified Data.Map as Map
21+import qualified Data.Set as Set
22+import qualified Data.Text as Text
23+
24+import qualified Elm.Compiler.Module as Module
25+import qualified Elm.Compiler.Type as Type
26+import qualified Elm.Docs as Docs
27+import qualified Elm.Name as N
28+import qualified Elm.Package as Pkg
29+
30+
31+
32+-- CHANGES
33+
34+
35+data PackageChanges =
36+ PackageChanges
37+ { _modules_added :: [Module.Raw]
38+ , _modules_changed :: Map.Map Module.Raw ModuleChanges
39+ , _modules_removed :: [Module.Raw]
40+ }
41+
42+
43+data ModuleChanges =
44+ ModuleChanges
45+ { _unions :: Changes N.Name Docs.Union
46+ , _aliases :: Changes N.Name Docs.Alias
47+ , _values :: Changes N.Name Docs.Value
48+ , _binops :: Changes N.Name Docs.Binop
49+ }
50+
51+
52+data Changes k v =
53+ Changes
54+ { _added :: Map.Map k v
55+ , _changed :: Map.Map k (v,v)
56+ , _removed :: Map.Map k v
57+ }
58+
59+
60+getChanges :: (Ord k) => (v -> v -> Bool) -> Map.Map k v -> Map.Map k v -> Changes k v
61+getChanges isEquivalent old new =
62+ let
63+ overlap = Map.intersectionWith (,) old new
64+ changed = Map.filter (not . uncurry isEquivalent) overlap
65+ in
66+ Changes (Map.difference new old) changed (Map.difference old new)
67+
68+
69+
70+-- DIFF
71+
72+
73+diff :: Docs.Documentation -> Docs.Documentation -> PackageChanges
74+diff oldDocs newDocs =
75+ let
76+ filterOutPatches chngs =
77+ Map.filter (\chng -> moduleChangeMagnitude chng /= PATCH) chngs
78+
79+ (Changes added changed removed) =
80+ getChanges (\_ _ -> False) oldDocs newDocs
81+ in
82+ PackageChanges
83+ (Map.keys added)
84+ (filterOutPatches (Map.map diffModule changed))
85+ (Map.keys removed)
86+
87+
88+
89+diffModule :: (Docs.Module, Docs.Module) -> ModuleChanges
90+diffModule (Docs.Module _ _ u1 a1 v1 b1, Docs.Module _ _ u2 a2 v2 b2) =
91+ ModuleChanges
92+ (getChanges isEquivalentUnion u1 u2)
93+ (getChanges isEquivalentAlias a1 a2)
94+ (getChanges isEquivalentValue v1 v2)
95+ (getChanges isEquivalentBinop b1 b2)
96+
97+
98+
99+-- EQUIVALENCE
100+
101+
102+isEquivalentUnion :: Docs.Union -> Docs.Union -> Bool
103+isEquivalentUnion (Docs.Union oldComment oldVars oldCtors) (Docs.Union newComment newVars newCtors) =
104+ length oldCtors == length newCtors
105+ && and (zipWith (==) (map fst oldCtors) (map fst newCtors))
106+ && and (Map.elems (Map.intersectionWith equiv (Map.fromList oldCtors) (Map.fromList newCtors)))
107+ where
108+ equiv :: [Type.Type] -> [Type.Type] -> Bool
109+ equiv oldTypes newTypes =
110+ let
111+ allEquivalent =
112+ zipWith
113+ isEquivalentAlias
114+ (map (Docs.Alias oldComment oldVars) oldTypes)
115+ (map (Docs.Alias newComment newVars) newTypes)
116+ in
117+ length oldTypes == length newTypes
118+ && and allEquivalent
119+
120+
121+isEquivalentAlias :: Docs.Alias -> Docs.Alias -> Bool
122+isEquivalentAlias (Docs.Alias _ oldVars oldType) (Docs.Alias _ newVars newType) =
123+ case diffType oldType newType of
124+ Nothing ->
125+ False
126+
127+ Just renamings ->
128+ length oldVars == length newVars
129+ && isEquivalentRenaming (zip oldVars newVars ++ renamings)
130+
131+
132+isEquivalentValue :: Docs.Value -> Docs.Value -> Bool
133+isEquivalentValue (Docs.Value c1 t1) (Docs.Value c2 t2) =
134+ isEquivalentAlias (Docs.Alias c1 [] t1) (Docs.Alias c2 [] t2)
135+
136+
137+isEquivalentBinop :: Docs.Binop -> Docs.Binop -> Bool
138+isEquivalentBinop (Docs.Binop c1 t1 a1 p1) (Docs.Binop c2 t2 a2 p2) =
139+ isEquivalentAlias (Docs.Alias c1 [] t1) (Docs.Alias c2 [] t2)
140+ && a1 == a2
141+ && p1 == p2
142+
143+
144+
145+-- DIFF TYPES
146+
147+
148+diffType :: Type.Type -> Type.Type -> Maybe [(N.Name,N.Name)]
149+diffType oldType newType =
150+ case (oldType, newType) of
151+ (Type.Var oldName, Type.Var newName) ->
152+ Just [(oldName, newName)]
153+
154+ (Type.Lambda a b, Type.Lambda a' b') ->
155+ (++)
156+ <$> diffType a a'
157+ <*> diffType b b'
158+
159+ (Type.Type oldName oldArgs, Type.Type newName newArgs) ->
160+ if not (isSameName oldName newName) || length oldArgs /= length newArgs then
161+ Nothing
162+ else
163+ concat <$> zipWithM diffType oldArgs newArgs
164+
165+ (Type.Record fields maybeExt, Type.Record fields' maybeExt') ->
166+ case (maybeExt, maybeExt') of
167+ (Nothing, Just _) ->
168+ Nothing
169+
170+ (Just _, Nothing) ->
171+ Nothing
172+
173+ (Nothing, Nothing) ->
174+ diffFields fields fields'
175+
176+ (Just oldExt, Just newExt) ->
177+ (:) (oldExt, newExt) <$> diffFields fields fields'
178+
179+ (_, _) ->
180+ Nothing
181+
182+
183+-- handle very old docs that do not use qualified names
184+isSameName :: N.Name -> N.Name -> Bool
185+isSameName oldFullName newFullName =
186+ let
187+ dedot name =
188+ reverse (Text.splitOn "." (N.toText name))
189+ in
190+ case ( dedot oldFullName, dedot newFullName ) of
191+ (oldName:[], newName:_) ->
192+ oldName == newName
193+
194+ (oldName:_, newName:[]) ->
195+ oldName == newName
196+
197+ _ ->
198+ oldFullName == newFullName
199+
200+
201+diffFields :: [(N.Name, Type.Type)] -> [(N.Name, Type.Type)] -> Maybe [(N.Name,N.Name)]
202+diffFields oldRawFields newRawFields =
203+ let
204+ sort = List.sortBy (compare `on` fst)
205+ oldFields = sort oldRawFields
206+ newFields = sort newRawFields
207+ in
208+ if length oldRawFields /= length newRawFields then
209+ Nothing
210+
211+ else if or (zipWith ((/=) `on` fst) oldFields newFields) then
212+ Nothing
213+
214+ else
215+ concat <$> zipWithM (diffType `on` snd) oldFields newFields
216+
217+
218+
219+-- TYPE VARIABLES
220+
221+
222+isEquivalentRenaming :: [(N.Name,N.Name)] -> Bool
223+isEquivalentRenaming varPairs =
224+ let
225+ renamings =
226+ Map.toList (foldr insert Map.empty varPairs)
227+
228+ insert (old,new) dict =
229+ Map.insertWith (++) old [new] dict
230+
231+ verify (old, news) =
232+ case news of
233+ [] ->
234+ Nothing
235+
236+ new : rest ->
237+ if all (new ==) rest then
238+ Just (old, new)
239+ else
240+ Nothing
241+
242+ allUnique list =
243+ length list == Set.size (Set.fromList list)
244+ in
245+ case mapM verify renamings of
246+ Nothing ->
247+ False
248+
249+ Just verifiedRenamings ->
250+ all compatableVars verifiedRenamings
251+ &&
252+ allUnique (map snd verifiedRenamings)
253+
254+
255+compatableVars :: (N.Name, N.Name) -> Bool
256+compatableVars (old, new) =
257+ case (categorizeVar old, categorizeVar new) of
258+ (CompAppend, CompAppend) -> True
259+ (Comparable, Comparable) -> True
260+ (Appendable, Appendable) -> True
261+ (Number , Number ) -> True
262+
263+ (Comparable, CompAppend) -> True
264+ (Appendable, CompAppend) -> True
265+ (Number , CompAppend) -> True
266+ (Number , Comparable) -> True
267+
268+ (_, Var) -> True
269+
270+ (_, _) -> False
271+
272+
273+data TypeVarCategory
274+ = CompAppend
275+ | Comparable
276+ | Appendable
277+ | Number
278+ | Var
279+
280+
281+categorizeVar :: N.Name -> TypeVarCategory
282+categorizeVar name
283+ | N.startsWith "compappend" name = CompAppend
284+ | N.startsWith "comparable" name = Comparable
285+ | N.startsWith "appendable" name = Appendable
286+ | N.startsWith "number" name = Number
287+ | otherwise = Var
288+
289+
290+
291+-- MAGNITUDE
292+
293+
294+data Magnitude
295+ = PATCH
296+ | MINOR
297+ | MAJOR
298+ deriving (Eq, Ord)
299+
300+
301+magnitudeToString :: Magnitude -> String
302+magnitudeToString magnitude =
303+ case magnitude of
304+ PATCH ->
305+ "PATCH"
306+
307+ MINOR ->
308+ "MINOR"
309+
310+ MAJOR ->
311+ "MAJOR"
312+
313+
314+bump :: PackageChanges -> Pkg.Version -> Pkg.Version
315+bump changes version =
316+ case toMagnitude changes of
317+ PATCH ->
318+ Pkg.bumpPatch version
319+
320+ MINOR ->
321+ Pkg.bumpMinor version
322+
323+ MAJOR ->
324+ Pkg.bumpMajor version
325+
326+
327+toMagnitude :: PackageChanges -> Magnitude
328+toMagnitude (PackageChanges added changed removed) =
329+ let
330+ addMag = if null added then PATCH else MINOR
331+ removeMag = if null removed then PATCH else MAJOR
332+ changeMags = map moduleChangeMagnitude (Map.elems changed)
333+ in
334+ maximum (addMag : removeMag : changeMags)
335+
336+
337+moduleChangeMagnitude :: ModuleChanges -> Magnitude
338+moduleChangeMagnitude (ModuleChanges unions aliases values binops) =
339+ maximum
340+ [ changeMagnitude unions
341+ , changeMagnitude aliases
342+ , changeMagnitude values
343+ , changeMagnitude binops
344+ ]
345+
346+
347+changeMagnitude :: Changes k v -> Magnitude
348+changeMagnitude (Changes added changed removed) =
349+ if Map.size removed > 0 || Map.size changed > 0 then
350+ MAJOR
351+
352+ else if Map.size added > 0 then
353+ MINOR
354+
355+ else
356+ PATCH
builder/src/Deps/Explorer.hsnew file+100
@@ -0,0 +1,100 @@
1+{-# OPTIONS_GHC -Wall #-}
2+module Deps.Explorer
3+ ( Explorer
4+ , Metadata
5+ , Info(..)
6+ , run
7+ , getVersions
8+ , getConstraints
9+ )
10+ where
11+
12+{-| It is expensive to load ALL package metadata. You would need to:
13+
14+ 1. Know all the packages and all their versions.
15+ 2. Download or read the elm.json file for each version.
16+
17+The goal of this module is to only pay for (1) and pay for (2) as needed.
18+-}
19+
20+
21+import Control.Monad.Except (lift, throwError)
22+import Control.Monad.State (StateT, evalStateT, gets, modify)
23+import Data.Map (Map)
24+import qualified Data.Map as Map
25+
26+import Elm.Package (Name, Version)
27+
28+import qualified Deps.Get as Get
29+import qualified Elm.Project.Json as Project
30+import Elm.Project.Constraint (Constraint)
31+import qualified Reporting.Error as Error
32+import qualified Reporting.Error.Deps as E
33+import qualified Reporting.Task as Task
34+
35+
36+
37+-- EXPLORER
38+
39+
40+type Explorer =
41+ StateT Metadata Task.Task
42+
43+
44+data Metadata =
45+ Metadata
46+ { _vsns :: Get.AllPackages
47+ , _info :: Map (Name, Version) Info
48+ }
49+
50+
51+data Info =
52+ Info
53+ { _elm :: Constraint
54+ , _pkgs :: Map Name Constraint
55+ }
56+
57+
58+run :: Explorer a -> Task.Task a
59+run explorer =
60+ do versions <- Get.all Get.AllowOffline
61+ evalStateT explorer (Metadata versions Map.empty)
62+
63+
64+
65+-- VERSIONS
66+
67+
68+getVersions :: Name -> Explorer [Version]
69+getVersions name =
70+ do pkgs <- gets _vsns
71+ case Get.versions name pkgs of
72+ Right versions ->
73+ return versions
74+
75+ Left _suggestions ->
76+ throwError (Error.Deps (E.CorruptVersionCache name))
77+
78+
79+
80+-- CONSTRAINTS
81+
82+
83+getConstraints :: Name -> Version -> Explorer Info
84+getConstraints name version =
85+ do allInfo <- gets _info
86+ case Map.lookup (name, version) allInfo of
87+ Just info ->
88+ return info
89+
90+ Nothing ->
91+ do pkgInfo <- lift $ Get.info name version
92+
93+ let elm = Project._pkg_elm_version pkgInfo
94+ let pkgs = Project._pkg_deps pkgInfo
95+ let info = Info elm pkgs
96+
97+ modify $ \(Metadata vsns infos) ->
98+ Metadata vsns $ Map.insert (name, version) info infos
99+
100+ return info
builder/src/Deps/Get.hsnew file+182
@@ -0,0 +1,182 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Deps.Get
4+ ( all
5+ , AllPackages
6+ , Mode(..)
7+ , versions
8+ , info
9+ , docs
10+ )
11+ where
12+
13+import Prelude hiding (all)
14+import Control.Monad.Except (catchError, liftIO)
15+import qualified Data.ByteString as BS
16+import qualified Data.List as List
17+import qualified Data.Map as Map
18+import Data.Map (Map)
19+import qualified System.Directory as Dir
20+import System.FilePath ((</>))
21+import qualified Text.PrettyPrint.ANSI.Leijen as P
22+
23+import qualified Elm.Docs as Docs
24+import qualified Elm.Package as Pkg
25+import Elm.Package (Name, Version)
26+
27+import qualified Deps.Website as Website
28+import qualified Elm.Project.Json as Project
29+import qualified Elm.Utils as Utils
30+import qualified File.IO as IO
31+import qualified Reporting.Error as Error
32+import qualified Reporting.Error.Assets as E
33+import qualified Reporting.Progress as Progress
34+import qualified Reporting.Task as Task
35+import qualified Json.Decode as Decode
36+
37+
38+
39+-- ALL VERSIONS
40+
41+
42+data Mode = RequireLatest | AllowOffline
43+
44+
45+newtype AllPackages = AllPackages (Map Name [Version])
46+
47+
48+all :: Mode -> Task.Task AllPackages
49+all mode =
50+ do dir <- Task.getPackageCacheDir
51+ let versionsFile = dir </> "versions.dat"
52+ exists <- liftIO $ Dir.doesFileExist versionsFile
53+ if exists
54+ then fetchNew versionsFile mode
55+ else fetchAll versionsFile
56+
57+
58+fetchAll :: FilePath -> Task.Task AllPackages
59+fetchAll versionsFile =
60+ do packages <- Website.getAllPackages
61+ let size = Map.foldr ((+) . length) 0 packages
62+ IO.writeBinary versionsFile (size, packages)
63+ return (AllPackages packages)
64+
65+
66+fetchNew :: FilePath -> Mode -> Task.Task AllPackages
67+fetchNew versionsFile mode =
68+ do (size, packages) <- IO.readBinary versionsFile
69+
70+ news <-
71+ case mode of
72+ RequireLatest ->
73+ Website.getNewPackages size
74+
75+ AllowOffline ->
76+ Website.getNewPackages size `catchError` \_ ->
77+ do Task.report Progress.UnableToLoadLatestPackages
78+ return []
79+
80+ if null news
81+ then return (AllPackages packages)
82+ else
83+ do let newAllPkgs = List.foldl' addNew packages news
84+ IO.writeBinary versionsFile (size + length news, newAllPkgs)
85+ return (AllPackages newAllPkgs)
86+
87+
88+addNew :: Map Name [Version] -> (Name, Version) -> Map Name [Version]
89+addNew packages (name, version) =
90+ Map.insertWith (++) name [version] packages
91+
92+
93+
94+-- VERSIONS
95+
96+
97+versions :: Name -> AllPackages -> Either [Name] [Version]
98+versions name (AllPackages pkgs) =
99+ case Map.lookup name pkgs of
100+ Just vsns ->
101+ Right vsns
102+
103+ Nothing ->
104+ Left (Utils.nearbyNames Pkg.toString name (Map.keys pkgs))
105+
106+
107+
108+-- PACKAGE INFO
109+
110+
111+info :: Name -> Version -> Task.Task Project.PkgInfo
112+info name version =
113+ do dir <- Task.getPackageCacheDirFor name version
114+ let elmJson = dir </> "elm.json"
115+
116+ exists <- liftIO $ Dir.doesFileExist elmJson
117+
118+ json <-
119+ if exists
120+ then liftIO (BS.readFile elmJson)
121+ else
122+ do bits <- Website.getElmJson name version
123+ liftIO (BS.writeFile elmJson bits)
124+ return bits
125+
126+ case Decode.parse "elm.json" "project" E.badContentToDocs Project.pkgDecoder json of
127+ Right pkgInfo ->
128+ return pkgInfo
129+
130+ Left _ ->
131+ do IO.remove elmJson
132+ Task.throw $ Error.Assets $ E.CorruptElmJson name version
133+
134+
135+
136+-- DOCS
137+
138+
139+docs :: Name -> Version -> Task.Task Docs.Documentation
140+docs name version =
141+ do dir <- Task.getPackageCacheDirFor name version
142+ let docsJson = dir </> "docs.json"
143+
144+ exists <- liftIO $ Dir.doesFileExist docsJson
145+
146+ json <-
147+ if exists
148+ then liftIO (BS.readFile docsJson)
149+ else
150+ do bits <- Website.getDocs name version
151+ liftIO (BS.writeFile docsJson bits)
152+ return bits
153+
154+ case Decode.parse "docs.json" "docs" errorToDocs (Docs.toDict <$> Decode.list Docs.decoder) json of
155+ Right pkgInfo ->
156+ return pkgInfo
157+
158+ Left _ ->
159+ do IO.remove docsJson
160+ Task.throw $ Error.Assets $ E.CorruptDocumentation name version
161+
162+
163+errorToDocs :: Docs.Error -> [P.Doc]
164+errorToDocs err =
165+ let
166+ details =
167+ case err of
168+ Docs.BadAssociativity txt ->
169+ ["Binary","operators","cannot","have",P.red (P.text (show txt)),"as","their","associativity."]
170+
171+ Docs.BadName ->
172+ ["It","is","not","a","valid","module","name."]
173+
174+ Docs.BadType ->
175+ ["It","is","not","a","valid","Elm","type."]
176+ in
177+ details
178+ ++
179+ ["This","should","not","happen","in","general,","so","please","report","this","at"
180+ ,"<https://github.com/elm-lang/package.elm-lang.org/issues>"
181+ ,"if","you","think","it","is","on","the","Elm","side!"
182+ ]
builder/src/Deps/Plan.hsnew file+63
@@ -0,0 +1,63 @@
1+module Install.Plan where
2+
3+import qualified Data.Map as Map
4+
5+import qualified Elm.Package.Solution as S
6+import qualified Elm.Package as Package
7+
8+
9+data Plan = Plan
10+ { installs :: Map.Map Package.Name Package.Version
11+ , upgrades :: Map.Map Package.Name (Package.Version, Package.Version)
12+ , removals :: Map.Map Package.Name Package.Version
13+ }
14+
15+
16+create :: S.Solution -> S.Solution -> Plan
17+create old new =
18+ Plan
19+ { installs = Map.difference new old
20+ , upgrades = discardNoOps (Map.intersectionWith (,) old new)
21+ , removals = Map.difference old new
22+ }
23+ where
24+ discardNoOps updates =
25+ Map.mapMaybe isChanged updates
26+
27+ isChanged upgrade@(oldVersion,newVersion) =
28+ if oldVersion == newVersion
29+ then Nothing
30+ else Just upgrade
31+
32+
33+isEmpty :: Plan -> Bool
34+isEmpty (Plan installs upgrades removals) =
35+ Map.null installs
36+ && Map.null upgrades
37+ && Map.null removals
38+
39+
40+-- DISPLAY
41+
42+display :: Plan -> String
43+display (Plan installs upgrades removals) =
44+ "\n"
45+ ++ displayCategory "Install" displayInstall installs
46+ ++ displayCategory "Upgrade" displayUpgrade upgrades
47+ ++ displayCategory "Remove" displayRemove removals
48+ where
49+ displayCategory name render category =
50+ if Map.null category then "" else
51+ " " ++ name ++ ":"
52+ ++ concatMap (\entry -> "\n " ++ render entry) (Map.toList category)
53+ ++ "\n"
54+
55+ displayInstall (name, version) =
56+ Package.toString name ++ " " ++ Package.versionToString version
57+
58+ displayUpgrade (name, (old, new)) =
59+ Package.toString name ++ " ("
60+ ++ Package.versionToString old ++ " => " ++ Package.versionToString new ++ ")"
61+
62+ displayRemove (name, _version) =
63+ Package.toString name
builder/src/Deps/Solver.hsnew file+103
@@ -0,0 +1,103 @@
1+module Deps.Solver
2+ ( Solver
3+ , run
4+ , solve
5+ )
6+ where
7+
8+
9+import Control.Monad (foldM, guard, mzero, msum)
10+import Control.Monad.Logic (LogicT, runLogicT, lift)
11+import qualified Data.List as List
12+import qualified Data.Map as Map
13+import Data.Map (Map)
14+
15+import Elm.Package (Name, Version)
16+
17+import Deps.Explorer (Explorer)
18+import qualified Deps.Explorer as Explorer
19+import Elm.Project.Constraint (Constraint)
20+import qualified Elm.Project.Constraint as Con
21+
22+
23+
24+-- SOLVE
25+
26+
27+solve :: Map Name Constraint -> Solver (Map Name Version)
28+solve cons =
29+ mkSolver (State Map.empty cons)
30+
31+
32+type Solver a =
33+ LogicT Explorer a
34+
35+
36+run :: Solver a -> Explorer (Maybe a)
37+run solver =
38+ runLogicT solver (const . return . Just) (return Nothing)
39+
40+
41+
42+-- SOLVER
43+
44+
45+data State =
46+ State
47+ { _solution :: Map Name Version
48+ , _unsolved :: Map Name Constraint
49+ }
50+
51+
52+mkSolver :: State -> Solver (Map Name Version)
53+mkSolver (State solution unsolved) =
54+ case Map.minViewWithKey unsolved of
55+ Nothing ->
56+ return solution
57+
58+ Just ((name, constraint), otherUnsolved) ->
59+ do allVersions <- lift $ Explorer.getVersions name
60+ let versions =
61+ reverse $ List.sort $
62+ filter (Con.satisfies constraint) allVersions
63+
64+ let state1 = State solution otherUnsolved
65+ state2 <- msum (map (addVersion state1 name) versions)
66+ mkSolver state2
67+
68+
69+addVersion :: State -> Name -> Version -> Solver State
70+addVersion (State solution unsolved) name version =
71+ do (Explorer.Info elm cons) <-
72+ lift $ Explorer.getConstraints name version
73+
74+ guard (Con.goodElm elm)
75+ newUnsolved <- foldM (addConstraint solution) unsolved (Map.toList cons)
76+ return (State (Map.insert name version solution) newUnsolved)
77+
78+
79+addConstraint :: Map Name Version -> Map Name Constraint -> (Name, Constraint) -> Solver (Map Name Constraint)
80+addConstraint solution unsolved (name, newConstraint) =
81+ case Map.lookup name solution of
82+ Just version ->
83+ if Con.satisfies newConstraint version then
84+ return unsolved
85+ else
86+ mzero
87+
88+ Nothing ->
89+ case Map.lookup name unsolved of
90+ Nothing ->
91+ return $ Map.insert name newConstraint unsolved
92+
93+ Just oldConstraint ->
94+ case Con.intersect oldConstraint newConstraint of
95+ Nothing ->
96+ mzero
97+
98+ Just mergedConstraint ->
99+ if oldConstraint == mergedConstraint then
100+ return unsolved
101+ else
102+ return (Map.insert name mergedConstraint unsolved)
103+
builder/src/Deps/Verify.hsnew file+362
@@ -0,0 +1,362 @@
1+module Deps.Verify
2+ ( verify
3+ )
4+ where
5+
6+
7+import Control.Concurrent (forkIO)
8+import Control.Concurrent.MVar (MVar, newMVar, newEmptyMVar, putMVar, readMVar, takeMVar)
9+import Control.Monad (filterM, void)
10+import Control.Monad.Trans (liftIO)
11+import qualified Data.Map as Map
12+import qualified Data.Set as Set
13+import Data.Map (Map)
14+import Data.Set (Set)
15+import System.Directory (doesDirectoryExist)
16+import System.FilePath ((</>))
17+
18+import qualified Elm.Compiler.Module as Module
19+import qualified Elm.Compiler.Objects as Obj
20+import qualified Elm.Docs as Docs
21+import Elm.Package (Name, Version)
22+import Elm.Project.Json (Project(..), AppInfo(..), PkgInfo(..))
23+import qualified Json.Encode as Encode
24+
25+import qualified Deps.Explorer as Explorer
26+import qualified Deps.Get as Get
27+import qualified Deps.Solver as Solver
28+import qualified Deps.Website as Website
29+import qualified Elm.Compiler as Compiler
30+import qualified Elm.Project.Json as Project
31+import qualified Elm.Project.Constraint as Con
32+import qualified Elm.Project.Summary as Summary
33+import qualified File.Args as Args
34+import qualified File.Artifacts as Artifacts
35+import qualified File.Compile as Compile
36+import qualified File.Crawl as Crawl
37+import qualified File.IO as IO
38+import qualified File.Plan as Plan
39+import qualified Reporting.Error as Error
40+import qualified Reporting.Error.Deps as E
41+import qualified Reporting.Progress as Progress
42+import qualified Reporting.Task as Task
43+import qualified Stuff.Paths as Paths
44+
45+
46+
47+-- VERIFY
48+
49+
50+verify :: FilePath -> Project -> Task.Task (Map Name Version, Summary.Summary)
51+verify root project =
52+ do solution <- Project.get verifyApp verifyPkg project
53+ (RawInfo infos ifaces) <- verifyArtifacts solution
54+ let summary = Summary.init root project infos ifaces
55+ return (solution, summary)
56+
57+
58+throw :: E.Error -> Task.Task a
59+throw err =
60+ Task.throw (Error.Deps err)
61+
62+
63+
64+-- VERIFY SOLUTION
65+
66+
67+verifyApp :: AppInfo -> Task.Task (Map Name Version)
68+verifyApp info =
69+ if _app_elm_version info /= Compiler.version then
70+ throw (E.AppBadElm (_app_elm_version info))
71+
72+ else
73+ do let oldSolution = Project.appSolution info
74+ let solver = Solver.solve (Map.map Con.exactly oldSolution)
75+ maybeSolution <- Explorer.run (Solver.run solver)
76+ case maybeSolution of
77+ Nothing ->
78+ throw E.BadDeps
79+
80+ Just solution ->
81+ return solution
82+
83+
84+verifyPkg :: PkgInfo -> Task.Task (Map Name Version)
85+verifyPkg info =
86+ if not (Con.goodElm (_pkg_elm_version info)) then
87+ throw (E.PkgBadElm (_pkg_elm_version info))
88+
89+ else
90+ do let deps = Map.union (_pkg_deps info) (_pkg_test_deps info)
91+ let solver = Solver.solve deps
92+ maybeSolution <- Explorer.run (Solver.run solver)
93+ case maybeSolution of
94+ Nothing ->
95+ throw E.BadDeps
96+
97+ Just solution ->
98+ return solution
99+
100+
101+
102+-- VERIFY ARTIFACTS
103+
104+
105+verifyArtifacts :: Map Name Version -> Task.Task RawInfo
106+verifyArtifacts solution =
107+ do Website.download =<< filterM noSrc (Map.toList solution)
108+ verifyBuildArtifacts solution
109+
110+
111+noSrc :: (Name, Version) -> Task.Task Bool
112+noSrc (name, version) =
113+ do root <- Task.getPackageCacheDirFor name version
114+ liftIO $ not <$> doesDirectoryExist (root </> "src")
115+
116+
117+
118+-- VERIFY BUILD ARTIFACTS
119+
120+
121+verifyBuildArtifacts :: Map Name Version -> Task.Task RawInfo
122+verifyBuildArtifacts solution =
123+ do Task.report (Progress.BuildDepsStart (Map.size solution))
124+ startMVar <- liftIO newEmptyMVar
125+ ifacesMVar <- liftIO $ newMVar Map.empty
126+ pkgInfoMVars <- Map.traverseWithKey (verifyBuild startMVar ifacesMVar) solution
127+ liftIO $ putMVar startMVar pkgInfoMVars
128+ answers <- liftIO $ traverse readMVar pkgInfoMVars
129+ Task.report Progress.BuildDepsEnd
130+ RawInfo
131+ <$> Map.traverseMaybeWithKey toInfo answers
132+ <*> liftIO (readMVar ifacesMVar)
133+
134+
135+data RawInfo =
136+ RawInfo (Map Name PkgInfo) Module.Interfaces
137+
138+
139+verifyBuild
140+ :: MVar (Map Name (MVar Answer))
141+ -> MVar Module.Interfaces
142+ -> Name
143+ -> Version
144+ -> Task.Task (MVar Answer)
145+verifyBuild pkgInfoMVar ifacesMVar name version =
146+ do mvar <- liftIO newEmptyMVar
147+ info <- Get.info name version
148+ report <- Task.getReporter
149+ runner <- Task.getSilentRunner
150+
151+ liftIO $ void $ forkIO $
152+ do allMVars <- readMVar pkgInfoMVar
153+ let deps = Project._pkg_deps info
154+ let depsMVars = Map.intersection allMVars deps
155+ depAnswers <- traverse readMVar depsMVars
156+
157+ answer <- ifNotBlocked depAnswers $ \infos ->
158+ do ifacesBefore <- readMVar ifacesMVar
159+ result <- runner (getIface name version info infos ifacesBefore)
160+ case result of
161+ Right ifaces ->
162+ do ifacesNow <- takeMVar ifacesMVar
163+ putMVar ifacesMVar (Map.union ifacesNow ifaces)
164+ return (Ok info)
165+
166+ Left _ ->
167+ return (Err name version)
168+
169+ report Progress.BuildDepsProgress
170+ putMVar mvar answer
171+
172+ return mvar
173+
174+
175+
176+-- ANSWERS
177+
178+
179+data Answer
180+ = Ok PkgInfo
181+ | Blocked
182+ | Err Name Version
183+
184+
185+toInfo :: Name -> Answer -> Task.Task (Maybe PkgInfo)
186+toInfo _ answer =
187+ case answer of
188+ Ok info ->
189+ return (Just info)
190+
191+ Blocked ->
192+ return Nothing
193+
194+ Err name version ->
195+ throw (E.BuildFailure name version)
196+
197+
198+ifNotBlocked :: Map Name Answer -> (Map Name PkgInfo -> IO Answer) -> IO Answer
199+ifNotBlocked answers callback =
200+ case traverse isOk answers of
201+ Nothing ->
202+ return Blocked
203+
204+ Just infos ->
205+ callback infos
206+
207+
208+isOk :: Answer -> Maybe PkgInfo
209+isOk answer =
210+ case answer of
211+ Ok info -> Just info
212+ Blocked -> Nothing
213+ Err _ _ -> Nothing
214+
215+
216+
217+-- GET INTERFACE
218+
219+
220+getIface
221+ :: Name
222+ -> Version
223+ -> PkgInfo
224+ -> Map Name PkgInfo
225+ -> Module.Interfaces
226+ -> Task.Task Module.Interfaces
227+getIface name version info infos depIfaces =
228+ do root <- Task.getPackageCacheDirFor name version
229+ let solution = Map.map _pkg_version infos
230+
231+ cached <- isCached root solution
232+
233+ if cached
234+ then IO.readBinary (root </> "ifaces.dat")
235+ else
236+ do Paths.removeStuff root
237+
238+ let summary = Summary.cheapInit root info infos depIfaces
239+ args <- Args.fromSummary summary
240+ graph <- Crawl.crawl summary args
241+ (dirty, cachedIfaces) <- Plan.plan summary graph
242+ answers <- Compile.compile (Pkg info) cachedIfaces dirty
243+ results <- Artifacts.ignore answers
244+ _ <- Artifacts.writeDocs (root </> "docs.json") results
245+
246+ Paths.removeStuff root
247+
248+ updateCache root name info solution graph results
249+
250+
251+
252+-- IS CACHED?
253+
254+
255+isCached :: FilePath -> Map Name Version -> Task.Task Bool
256+isCached root solution =
257+ IO.andM
258+ [ IO.exists (root </> "cached.dat")
259+ , IO.exists (root </> "ifaces.dat")
260+ , IO.exists (root </> "objs.dat")
261+ , IO.exists (root </> "documentation.json")
262+ , isCachedHelp solution <$> IO.readBinary (root </> "cached.dat")
263+ ]
264+
265+
266+isCachedHelp :: Map Name Version -> Map Name (Set Version) -> Bool
267+isCachedHelp solution cachedDeps =
268+ let
269+ matches =
270+ Map.intersectionWith Set.member solution cachedDeps
271+ in
272+ Map.size solution == Map.size matches
273+ && Map.foldr (&&) True matches
274+
275+
276+
277+-- UPDATE CACHE
278+
279+
280+updateCache
281+ :: FilePath
282+ -> Name
283+ -> PkgInfo
284+ -> Map Name Version
285+ -> Crawl.Result
286+ -> Map Module.Raw Compiler.Artifacts
287+ -> Task.Task Module.Interfaces
288+updateCache root name info solution graph results =
289+ do let path = root </> "cached.dat"
290+ let deps = Map.map Set.singleton solution
291+ let elmi = crush name info results
292+
293+ exists <- IO.exists path
294+
295+ if exists
296+ then
297+ do oldDeps <- IO.readBinary path
298+ IO.writeBinary path (Map.unionWith Set.union deps oldDeps)
299+ else
300+ do IO.writeBinary (root </> "ifaces.dat") elmi
301+
302+ IO.writeBinary path deps
303+
304+ IO.writeBinary (root </> "objs.dat") $
305+ Map.foldr addGraph (objGraphFromKernels graph) results
306+
307+ liftIO $ Encode.write (root </> "documentation.json") $
308+ Encode.list Docs.encode $
309+ Map.foldr addDocs [] results
310+
311+ return elmi
312+
313+
314+
315+-- CRUSH INTERFACES
316+
317+
318+crush :: Name -> PkgInfo -> Map Module.Raw Compiler.Artifacts -> Module.Interfaces
319+crush pkg info results =
320+ let
321+ exposed =
322+ Set.fromList (Project.getExposed info)
323+ in
324+ Map.mapKeys (Module.Canonical pkg) $
325+ Map.mapMaybeWithKey (crushHelp exposed) results
326+
327+
328+crushHelp :: Set Module.Raw -> Module.Raw -> Compiler.Artifacts -> Maybe Module.Interface
329+crushHelp exposed name (Compiler.Artifacts elmi _ _) =
330+ if Set.member name exposed then
331+ Just elmi
332+
333+ else
334+ Nothing
335+
336+
337+
338+-- DOCUMENTATION
339+
340+
341+addDocs :: Compiler.Artifacts -> [Docs.Module] -> [Docs.Module]
342+addDocs (Compiler.Artifacts _ _ maybeDocs) docsList =
343+ case maybeDocs of
344+ Nothing ->
345+ docsList
346+
347+ Just docs ->
348+ docs : docsList
349+
350+
351+
352+-- OBJECT GRAPH
353+
354+
355+addGraph :: Compiler.Artifacts -> Obj.Graph -> Obj.Graph
356+addGraph (Compiler.Artifacts _ elmo _) graph =
357+ Obj.union elmo graph
358+
359+
360+objGraphFromKernels :: Crawl.Result -> Obj.Graph
361+objGraphFromKernels (Crawl.Graph _ _ kernels _ _) =
362+ Obj.fromKernels kernels
builder/src/Deps/Website.hsnew file+344
@@ -0,0 +1,344 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Deps.Website
4+ ( getElmJson
5+ , getDocs
6+ , getNewPackages
7+ , getAllPackages
8+ , download
9+ , githubCommit
10+ , githubDownload
11+ , Sha
12+ , register
13+ )
14+ where
15+
16+import Prelude hiding (zip)
17+import qualified Codec.Archive.Zip as Zip
18+import Control.Monad (void)
19+import qualified Data.Binary as Binary
20+import qualified Data.Binary.Get as Binary
21+import qualified Data.ByteString.Char8 as BS
22+import qualified Data.ByteString.Lazy as LBS
23+import qualified Data.Digest.Pure.SHA as SHA
24+import qualified Data.HashMap.Lazy as HashMap
25+import qualified Data.Map as Map
26+import qualified Data.Text as Text
27+import qualified Network.HTTP.Client as Client
28+import qualified Network.HTTP.Client.MultipartFormData as Multi
29+import qualified Network.HTTP.Types as Http
30+import qualified System.Directory as Dir
31+import System.FilePath ((</>), splitFileName)
32+import qualified Text.PrettyPrint.ANSI.Leijen as P
33+
34+import Elm.Package (Name, Version)
35+import qualified Elm.Package as Pkg
36+import qualified Json.Decode as D
37+
38+import qualified Reporting.Error.Http as E
39+import qualified Reporting.Progress as Progress
40+import qualified Reporting.Task as Task
41+import qualified Reporting.Task.Http as Http
42+import qualified Stuff.Paths as Path
43+
44+
45+
46+-- GET PACKAGE INFO
47+
48+
49+getElmJson :: Name -> Version -> Task.Task BS.ByteString
50+getElmJson name version =
51+ Http.run $ fetchByteString $
52+ "packages/" ++ Pkg.toUrl name ++ "/" ++ Pkg.versionToString version ++ "/elm.json"
53+
54+
55+getDocs :: Name -> Version -> Task.Task BS.ByteString
56+getDocs name version =
57+ Http.run $ fetchByteString $
58+ "packages/" ++ Pkg.toUrl name ++ "/" ++ Pkg.versionToString version ++ "/docs.json"
59+
60+
61+
62+-- NEW PACKAGES
63+
64+
65+getNewPackages :: Int -> Task.Task [(Name, Version)]
66+getNewPackages index =
67+ Http.run $ fetchJson "packages" E.badJsonToDocs (D.list newPkgDecoder) ("all-packages/since/" ++ show index)
68+
69+
70+newPkgDecoder :: D.Decoder E.BadJson ( Name, Version )
71+newPkgDecoder =
72+ do txt <- D.text
73+ case Text.splitOn "@" txt of
74+ [key, value] ->
75+ case Pkg.fromText key of
76+ Right pkg ->
77+ case Pkg.versionFromText value of
78+ Just vsn ->
79+ D.succeed (pkg, vsn)
80+
81+ Nothing ->
82+ D.fail (E.BadNewPkg txt)
83+
84+ Left _ ->
85+ D.fail (E.BadNewPkg txt)
86+
87+ _ ->
88+ D.fail (E.BadNewPkg txt)
89+
90+
91+
92+-- ALL PACKAGES
93+
94+
95+getAllPackages :: Task.Task (Map.Map Name [Version])
96+getAllPackages =
97+ Http.run $ fetchJson "packages" E.badJsonToDocs allPkgsDecoder "all-packages"
98+
99+
100+allPkgsDecoder :: D.Decoder E.BadJson (Map.Map Name [Version])
101+allPkgsDecoder =
102+ let
103+ checkKeys pairs pkgs =
104+ case pairs of
105+ [] ->
106+ D.succeed pkgs
107+
108+ (key, versions) : rest ->
109+ case Pkg.fromText key of
110+ Left msg ->
111+ D.fail (E.BadAllPkg key msg)
112+
113+ Right pkg ->
114+ checkKeys rest (Map.insert pkg versions pkgs)
115+ in
116+ do dict <- D.dict (D.list (D.mapError E.BadAllVsn Pkg.versionDecoder))
117+ checkKeys (HashMap.toList dict) Map.empty
118+
119+
120+
121+-- HELPERS
122+
123+
124+fetchByteString :: String -> Http.Fetch BS.ByteString
125+fetchByteString path =
126+ Http.package path [] $ \request manager ->
127+ do response <- Client.httpLbs request manager
128+ return $ Right $ LBS.toStrict $ Client.responseBody response
129+
130+
131+fetchJson :: String -> (e -> [P.Doc]) -> D.Decoder e a -> String -> Http.Fetch a
132+fetchJson rootName errorToDocs decoder path =
133+ Http.package path [] $ \request manager ->
134+ do response <- Client.httpLbs request manager
135+ let bytes = LBS.toStrict (Client.responseBody response)
136+ case D.parse path rootName errorToDocs decoder bytes of
137+ Right value ->
138+ return $ Right value
139+
140+ Left jsonProblem ->
141+ return $ Left $ E.BadJson jsonProblem
142+
143+
144+
145+-- DOWNLOAD
146+
147+
148+download :: [(Name, Version)] -> Task.Task ()
149+download packages =
150+ case packages of
151+ [] ->
152+ Task.report Progress.DownloadSkip
153+
154+ _ ->
155+ do cache <- Task.getPackageCacheDir
156+
157+ let start = Progress.DownloadStart packages
158+ let toEnd = Progress.DownloadEnd
159+
160+ void $ Http.run $ Http.report start toEnd $
161+ Http.parallel $ map (downloadHelp cache) packages
162+
163+
164+downloadHelp :: FilePath -> (Name, Version) -> Http.Fetch ()
165+downloadHelp cache (name, version) =
166+ let
167+ endpointUrl =
168+ "packages/" ++ Pkg.toUrl name ++ "/" ++ Pkg.versionToString version ++ "/endpoint.json"
169+ in
170+ Http.andThen (fetchJson "version" id endpointDecoder endpointUrl) $ \(endpoint, hash) ->
171+ let
172+ start = Progress.DownloadPkgStart name version
173+ toEnd = Progress.DownloadPkgEnd name version
174+ in
175+ Http.report start toEnd $
176+ Http.anything endpoint $ \request manager ->
177+ Client.withResponse request manager (downloadArchive cache name version hash)
178+
179+
180+endpointDecoder :: D.Decoder e (String, String)
181+endpointDecoder =
182+ D.map2 (,)
183+ (D.field "url" D.string)
184+ (D.field "hash" D.string)
185+
186+
187+
188+-- DOWNLOAD ZIP ARCHIVE
189+
190+
191+downloadArchive :: FilePath -> Name -> Version -> String -> Client.Response Client.BodyReader -> IO (Either E.Error ())
192+downloadArchive cache name version expectedHash response =
193+ do result <- readArchive (Client.responseBody response) initialArchiveState
194+ case result of
195+ Left msg ->
196+ return (Left msg)
197+
198+ Right (sha, archive) ->
199+ if expectedHash == SHA.showDigest sha then
200+ Right <$> writeArchive archive (cache </> Pkg.toFilePath name) (Pkg.versionToString version)
201+ else
202+ return $ Left $ E.BadZipSha expectedHash (SHA.showDigest sha)
203+
204+
205+
206+-- READ ARCHIVE
207+
208+
209+data ArchiveState =
210+ AS
211+ { _len :: !Int
212+ , _sha :: !(Binary.Decoder SHA.SHA1State)
213+ , _zip :: !(Binary.Decoder Zip.Archive)
214+ }
215+
216+
217+initialArchiveState :: ArchiveState
218+initialArchiveState =
219+ AS 0 SHA.sha1Incremental (Binary.runGetIncremental Binary.get)
220+
221+
222+type Sha = SHA.Digest SHA.SHA1State
223+
224+
225+readArchive :: Client.BodyReader -> ArchiveState -> IO (Either E.Error (Sha, Zip.Archive))
226+readArchive body (AS len sha zip) =
227+ case zip of
228+ Binary.Fail _ _ _ ->
229+ return $ Left E.BadZipData
230+
231+ Binary.Partial k ->
232+ do chunk <- Client.brRead body
233+ readArchive body $ AS
234+ { _len = len + BS.length chunk
235+ , _sha = Binary.pushChunk sha chunk
236+ , _zip = k (if BS.null chunk then Nothing else Just chunk)
237+ }
238+
239+ Binary.Done _ _ archive ->
240+ return $ Right ( SHA.completeSha1Incremental sha len, archive )
241+
242+
243+
244+-- WRITE ARCHIVE
245+
246+
247+writeArchive :: Zip.Archive -> FilePath -> FilePath -> IO ()
248+writeArchive archive destination newRoot =
249+ do Dir.createDirectoryIfMissing True destination
250+ let opts = [Zip.OptDestination destination]
251+ mapM_ (Zip.writeEntry opts . replaceRoot newRoot) (Zip.zEntries archive)
252+
253+
254+replaceRoot :: String -> Zip.Entry -> Zip.Entry
255+replaceRoot root entry =
256+ let
257+ rootless =
258+ dropWhile (/='/') (Zip.eRelativePath entry)
259+ in
260+ entry { Zip.eRelativePath = root ++ rootless }
261+
262+
263+
264+-- FIND TAGGED COMMIT ON GITHUB
265+
266+
267+githubCommit :: Name -> Version -> Task.Task String
268+githubCommit name version =
269+ let
270+ endpoint =
271+ "https://api.github.com/repos/" ++ Pkg.toUrl name ++ "/git/refs/tags/temp-" ++ Pkg.versionToString version
272+
273+ headers =
274+ [ ( Http.hUserAgent, "elm-cli" )
275+ , ( Http.hAccept, "application/json" )
276+ ]
277+
278+ decoder =
279+ D.at ["object","sha"] D.string
280+ in
281+ Http.run $ Http.anything endpoint $ \request manager ->
282+ do response <- Client.httpLbs (request { Client.requestHeaders = headers }) manager
283+ let bytes = LBS.toStrict (Client.responseBody response)
284+ case D.parse "github.json" "github" id decoder bytes of
285+ Right value ->
286+ return $ Right value
287+
288+ Left jsonProblem ->
289+ return $ Left $ E.BadJson jsonProblem
290+
291+
292+
293+-- DOWNLOAD FROM GITHUB
294+
295+
296+githubDownload :: Name -> Version -> FilePath -> Task.Task Sha
297+githubDownload name version dir =
298+ let
299+ endpoint =
300+ "https://github.com/" ++ Pkg.toUrl name ++ "/zipball/temp-" ++ Pkg.versionToString version ++ "/"
301+ in
302+ Http.run $ Http.anything endpoint $ \request manager ->
303+ Client.withResponse request manager (githubDownloadHelp dir)
304+
305+
306+githubDownloadHelp :: FilePath -> Client.Response Client.BodyReader -> IO (Either E.Error Sha)
307+githubDownloadHelp targetDir response =
308+ do result <- readArchive (Client.responseBody response) initialArchiveState
309+ case result of
310+ Left msg ->
311+ return (Left msg)
312+
313+ Right (sha, archive) ->
314+ do let (dir, root) = splitFileName targetDir
315+ writeArchive archive dir root
316+ return $ Right sha
317+
318+
319+
320+-- REGISTER PACKAGES
321+
322+
323+register :: Name -> Version -> String -> Sha -> Task.Task ()
324+register name version commitHash digest =
325+ let
326+ params =
327+ [ ("name", Pkg.toString name)
328+ , ("version", Pkg.versionToString version)
329+ , ("commit-hash", commitHash)
330+ ]
331+
332+ files =
333+ [ Multi.partFileSource "elm.json" "elm.json"
334+ , Multi.partFileSource "docs.json" Path.docs
335+ , Multi.partFileSource "README.md" "README.md"
336+ , Multi.partFileRequestBody "github-hash" "github-hash" $
337+ Client.RequestBodyBS (BS.pack (SHA.showDigest digest))
338+ ]
339+ in
340+ Http.run $ Http.package "register" params $ \rawRequest manager ->
341+ do requestWithBody <- Multi.formDataBody files rawRequest
342+ let request = requestWithBody { Client.responseTimeout = Client.responseTimeoutNone }
343+ void $ Client.httpLbs request manager
344+ return $ Right ()
builder/src/Elm/Bump.hsnew file+140
@@ -0,0 +1,140 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Elm.Bump
4+ ( bump
5+ , toPossibleBumps
6+ )
7+ where
8+
9+
10+import Control.Monad.Trans (liftIO)
11+import qualified Data.List as List
12+import Text.PrettyPrint.ANSI.Leijen ((<>), (<+>))
13+import qualified Text.PrettyPrint.ANSI.Leijen as P
14+
15+import qualified Deps.Diff as Diff
16+import qualified Deps.Get as Get
17+import qualified Elm.Package as Pkg
18+import qualified Elm.Project as Project
19+import qualified Elm.Project.Json as Project
20+import qualified Elm.Project.Summary as Summary
21+import qualified Reporting.Error as Error
22+import qualified Reporting.Error.Bump as E
23+import qualified Reporting.Error.Help as Help
24+import qualified Reporting.Progress.Terminal as Terminal
25+import qualified Reporting.Task as Task
26+
27+
28+
29+-- BUMP
30+
31+
32+bump :: Summary.Summary -> Task.Task ()
33+bump summary@(Summary.Summary root project _ _ _) =
34+ case project of
35+ Project.App _ ->
36+ Task.throw (Error.Bump E.Application)
37+
38+ Project.Pkg info@(Project.PkgInfo name _ _ version _ _ _ _) ->
39+ do pkgs <- Get.all Get.RequireLatest
40+ case Get.versions name pkgs of
41+ Left _suggestions ->
42+ checkNewPackage root info
43+
44+ Right publishedVersions ->
45+ let
46+ bumpableVersions =
47+ map (\(old, _, _) -> old) (toPossibleBumps publishedVersions)
48+ in
49+ if elem version bumpableVersions then
50+ suggestVersion summary info
51+ else
52+ Task.throw $ Error.Bump $ E.Unbumpable version $
53+ map head (List.group (List.sort bumpableVersions))
54+
55+
56+
57+-- VALID BUMPS
58+
59+
60+toPossibleBumps :: [Pkg.Version] -> [(Pkg.Version, Pkg.Version, Diff.Magnitude)]
61+toPossibleBumps publishedVersions =
62+ case publishedVersions of
63+ [] ->
64+ []
65+
66+ _ ->
67+ let
68+ patchPoints =
69+ Pkg.filterLatest Pkg.majorAndMinor publishedVersions
70+
71+ minorPoints =
72+ Pkg.filterLatest Pkg._major publishedVersions
73+
74+ majorPoint =
75+ maximum publishedVersions
76+ in
77+ (majorPoint, Pkg.bumpMajor majorPoint, Diff.MAJOR)
78+ : map (\v -> (v, Pkg.bumpMinor v, Diff.MINOR)) minorPoints
79+ ++ map (\v -> (v, Pkg.bumpPatch v, Diff.PATCH)) patchPoints
80+
81+
82+
83+-- CHECK NEW PACKAGE
84+
85+
86+checkNewPackage :: FilePath -> Project.PkgInfo -> Task.Task ()
87+checkNewPackage root info@(Project.PkgInfo _ _ _ version _ _ _ _) =
88+ do liftIO $ putStrLn Terminal.newPackageOverview
89+ if version == Pkg.initialVersion
90+ then
91+ liftIO $ putStrLn "The version number in elm.json is correct so you are all set!"
92+ else
93+ changeVersion root info Pkg.initialVersion $
94+ "It looks like the version in elm.json has been changed though!\n\
95+ \Would you like me to change it back to "
96+ <> P.text (Pkg.versionToString Pkg.initialVersion) <> "? [Y/n] "
97+
98+
99+
100+-- SUGGEST VERSION
101+
102+
103+suggestVersion :: Summary.Summary -> Project.PkgInfo -> Task.Task ()
104+suggestVersion summary@(Summary.Summary root _ _ _ _) info@(Project.PkgInfo name _ _ version _ _ _ _) =
105+ do oldDocs <- Get.docs name version
106+ newDocs <- Task.silently (Project.generateDocs summary)
107+ let changes = Diff.diff oldDocs newDocs
108+ let newVersion = Diff.bump changes version
109+ changeVersion root info newVersion $
110+ let
111+ old = P.text $ Pkg.versionToString version
112+ new = P.text $ Pkg.versionToString newVersion
113+ mag = P.text $ Diff.magnitudeToString (Diff.toMagnitude changes)
114+ in
115+ "Based on your new API, this should be a" <+> P.green mag <+> "change (" <> old <> " => " <> new <> ")\n"
116+ <> "Bail out of this command and run 'elm diff' for a full explanation.\n"
117+ <> "\n"
118+ <> "Should I perform the update (" <> old <> " => " <> new <> ") in elm.json? [Y/n] "
119+
120+
121+
122+-- CHANGE VERSION
123+
124+
125+changeVersion :: FilePath -> Project.PkgInfo -> Pkg.Version -> P.Doc -> Task.Task ()
126+changeVersion root info targetVersion explanation =
127+ do liftIO $ Help.toStdout explanation
128+ approved <- Task.getApproval
129+ if not approved
130+ then
131+ liftIO $ putStrLn "Okay, no changes were made."
132+
133+ else
134+ do liftIO $ Project.write root $ Project.Pkg $
135+ info { Project._pkg_version = targetVersion }
136+
137+ liftIO $ Help.toStdout $
138+ "Version changed to "
139+ <> P.green (P.text (Pkg.versionToString targetVersion))
140+ <> "!\n"
builder/src/Elm/Diff.hsnew file+279
@@ -0,0 +1,279 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Elm.Diff
4+ ( Args(..)
5+ , diff
6+ )
7+ where
8+
9+
10+import Control.Monad.Except (liftIO)
11+import qualified Data.List as List
12+import qualified Data.Map as Map
13+import qualified Data.Maybe as Maybe
14+import Text.PrettyPrint.ANSI.Leijen ((<>), (<+>))
15+import qualified Text.PrettyPrint.ANSI.Leijen as P
16+
17+import Deps.Diff (PackageChanges(..), ModuleChanges(..), Changes(..), Magnitude(..))
18+import qualified Deps.Diff as Diff
19+import qualified Deps.Get as Get
20+import qualified Elm.Compiler.Module as Module
21+import qualified Elm.Compiler.Type as Type
22+import qualified Elm.Docs as Docs
23+import qualified Elm.Name as N
24+import qualified Elm.Package as Pkg
25+import qualified Elm.Project as Project
26+import qualified Elm.Project.Json as Project
27+import qualified Elm.Project.Summary as Summary
28+import qualified Reporting.Error as Error
29+import qualified Reporting.Error.Diff as E
30+import qualified Reporting.Error.Help as Help
31+import qualified Reporting.Task as Task
32+
33+
34+
35+-- DIFF
36+
37+
38+data Args
39+ = CodeVsLatest
40+ | CodeVsExactly Pkg.Version
41+ | LocalInquiry Pkg.Version Pkg.Version
42+ | GlobalInquiry Pkg.Name Pkg.Version Pkg.Version
43+
44+
45+diff :: Args -> Task.Task ()
46+diff args =
47+ case args of
48+ GlobalInquiry name v1 v2 ->
49+ do pkgs <- Get.all Get.RequireLatest
50+ case Get.versions name pkgs of
51+ Left suggestions ->
52+ throw $ E.UnknownPackage name suggestions
53+
54+ Right vsns ->
55+ do oldDocs <- getDocs name vsns (min v1 v2)
56+ newDocs <- getDocs name vsns (max v1 v2)
57+ writeDoc (toDoc (Diff.diff oldDocs newDocs))
58+
59+ LocalInquiry v1 v2 ->
60+ do (_, name, vsns) <- getPackageInfo
61+ oldDocs <- getDocs name vsns (min v1 v2)
62+ newDocs <- getDocs name vsns (max v1 v2)
63+ writeDoc (toDoc (Diff.diff oldDocs newDocs))
64+
65+ CodeVsLatest ->
66+ do (summary, name, vsns) <- getPackageInfo
67+ oldDocs <- getDocs name vsns (maximum vsns)
68+ newDocs <- Task.silently (Project.generateDocs summary)
69+ writeDoc (toDoc (Diff.diff oldDocs newDocs))
70+
71+ CodeVsExactly version ->
72+ do (summary, name, vsns) <- getPackageInfo
73+ oldDocs <- getDocs name vsns version
74+ newDocs <- Task.silently (Project.generateDocs summary)
75+ writeDoc (toDoc (Diff.diff oldDocs newDocs))
76+
77+
78+throw :: E.Error -> Task.Task a
79+throw err =
80+ Task.throw $ Error.Diff err
81+
82+
83+
84+-- DIFF HELPERS
85+
86+
87+getDocs :: Pkg.Name -> [Pkg.Version] -> Pkg.Version -> Task.Task Docs.Documentation
88+getDocs name allVersions version =
89+ if elem version allVersions then
90+ Get.docs name version
91+ else
92+ throw $ E.UnknownVersion name version allVersions
93+
94+
95+getPackageInfo :: Task.Task (Summary.Summary, Pkg.Name, [Pkg.Version])
96+getPackageInfo =
97+ do summary <- Project.getRoot
98+ case Summary._project summary of
99+ Project.App _ ->
100+ throw $ E.Application
101+
102+ Project.Pkg (Project.PkgInfo name _ _ _ _ _ _ _) ->
103+ do pkgs <- Get.all Get.RequireLatest
104+ let vsns = either (const []) id (Get.versions name pkgs)
105+ return ( summary, name, vsns )
106+
107+
108+
109+-- WRITE DOC
110+
111+
112+writeDoc :: P.Doc -> Task.Task ()
113+writeDoc doc =
114+ liftIO $ Help.toStdout (doc <> "\n")
115+
116+
117+
118+-- TO DOC
119+
120+
121+toDoc :: PackageChanges -> P.Doc
122+toDoc changes@(PackageChanges added changed removed) =
123+ if null added && Map.null changed && null removed then
124+ "No API changes detected, so this is a" <+> P.green "PATCH" <+> "change."
125+ else
126+ let
127+ magDoc =
128+ P.text (Diff.magnitudeToString (Diff.toMagnitude changes))
129+
130+ header =
131+ "This is a" <+> P.green magDoc <+> "change."
132+
133+ addedChunk =
134+ if null added then [] else
135+ [ Chunk "ADDED MODULES" MINOR $
136+ P.vcat $ map (P.text . Module.nameToString) added
137+ ]
138+
139+ removedChunk =
140+ if null removed then [] else
141+ [ Chunk "REMOVED MODULES" MAJOR $
142+ P.vcat $ map (P.text . Module.nameToString) removed
143+ ]
144+
145+ chunks =
146+ addedChunk ++ removedChunk ++ map changesToChunk (Map.toList changed)
147+ in
148+ P.vcat (header : "" : map chunkToDoc chunks)
149+
150+
151+data Chunk =
152+ Chunk
153+ { _title :: String
154+ , _magnitude :: Magnitude
155+ , _details :: P.Doc
156+ }
157+
158+
159+chunkToDoc :: Chunk -> P.Doc
160+chunkToDoc (Chunk title magnitude details) =
161+ let
162+ magDoc =
163+ Diff.magnitudeToString magnitude
164+
165+ header =
166+ "----" <+> P.text title <+> "-" <+> P.text magDoc <+> "----"
167+ in
168+ P.vcat
169+ [ P.dullcyan header
170+ , ""
171+ , P.indent 4 details
172+ , ""
173+ , ""
174+ ]
175+
176+
177+changesToChunk :: (N.Name, ModuleChanges) -> Chunk
178+changesToChunk (name, changes@(ModuleChanges unions aliases values binops)) =
179+ let
180+ magnitude =
181+ Diff.moduleChangeMagnitude changes
182+
183+ (unionAdd, unionChange, unionRemove) =
184+ changesToDocTriple unionToDoc unions
185+
186+ (aliasAdd, aliasChange, aliasRemove) =
187+ changesToDocTriple aliasToDoc aliases
188+
189+ (valueAdd, valueChange, valueRemove) =
190+ changesToDocTriple valueToDoc values
191+
192+ (binopAdd, binopChange, binopRemove) =
193+ changesToDocTriple binopToDoc binops
194+ in
195+ Chunk (N.toString name) magnitude $
196+ P.vcat $ List.intersperse "" $ Maybe.catMaybes $
197+ [ changesToDoc "Added" unionAdd aliasAdd valueAdd binopAdd
198+ , changesToDoc "Removed" unionRemove aliasRemove valueRemove binopRemove
199+ , changesToDoc "Changed" unionChange aliasChange valueChange binopChange
200+ ]
201+
202+
203+changesToDocTriple :: (k -> v -> P.Doc) -> Changes k v -> ([P.Doc], [P.Doc], [P.Doc])
204+changesToDocTriple entryToDoc (Changes added changed removed) =
205+ let
206+ indented (name, value) =
207+ P.indent 4 (entryToDoc name value)
208+
209+ diffed (name, (oldValue, newValue)) =
210+ P.vcat
211+ [ " - " <> entryToDoc name oldValue
212+ , " + " <> entryToDoc name newValue
213+ , ""
214+ ]
215+ in
216+ ( map indented (Map.toList added)
217+ , map diffed (Map.toList changed)
218+ , map indented (Map.toList removed)
219+ )
220+
221+
222+changesToDoc :: String -> [P.Doc] -> [P.Doc] -> [P.Doc] -> [P.Doc] -> Maybe P.Doc
223+changesToDoc categoryName unions aliases values binops =
224+ if null unions && null aliases && null values && null binops then
225+ Nothing
226+
227+ else
228+ Just $ P.vcat $
229+ P.text categoryName <> ":" : unions ++ aliases ++ binops ++ values
230+
231+
232+unionToDoc :: N.Name -> Docs.Union -> P.Doc
233+unionToDoc name (Docs.Union _ tvars ctors) =
234+ let
235+ setup =
236+ "type" <+> nameToDoc name <+> P.hsep (map nameToDoc tvars)
237+
238+ ctorDoc (ctor, tipes) =
239+ typeDoc (Type.Type ctor tipes)
240+ in
241+ P.hang 4 (P.sep (setup : zipWith (<+>) ("=" : repeat "|") (map ctorDoc ctors)))
242+
243+
244+aliasToDoc :: N.Name -> Docs.Alias -> P.Doc
245+aliasToDoc name (Docs.Alias _ tvars tipe) =
246+ let
247+ declaration =
248+ "type" <+> "alias" <+> P.hsep (map nameToDoc (name:tvars)) <+> "="
249+ in
250+ P.hang 4 (P.sep [ declaration, typeDoc tipe ])
251+
252+
253+valueToDoc :: N.Name -> Docs.Value -> P.Doc
254+valueToDoc name (Docs.Value _ tipe) =
255+ nameToDoc name <+> P.colon <+> typeDoc tipe
256+
257+
258+binopToDoc :: N.Name -> Docs.Binop -> P.Doc
259+binopToDoc name (Docs.Binop _ tipe associativity (Docs.Precedence n)) =
260+ "(" <> nameToDoc name <> ")" <+> P.colon <+> typeDoc tipe <> P.black details
261+ where
262+ details =
263+ " (" <> nameToDoc assoc <> "/" <> P.int n <> ")"
264+
265+ assoc =
266+ case associativity of
267+ Docs.Left -> "left"
268+ Docs.Non -> "non"
269+ Docs.Right -> "right"
270+
271+
272+typeDoc :: Type.Type -> P.Doc
273+typeDoc tipe =
274+ P.text (Type.toString Type.OneLine tipe)
275+
276+
277+nameToDoc :: N.Name -> P.Doc
278+nameToDoc name =
279+ P.text (N.toString name)
builder/src/Elm/Install.hsnew file+356
@@ -0,0 +1,356 @@
1+module Elm.Install
2+ ( install
3+ )
4+ where
5+
6+
7+import Control.Monad (filterM, forM, msum, void)
8+import Control.Monad.Except (catchError, lift, liftIO)
9+import Data.Map (Map, (!))
10+import qualified Data.Map as Map
11+import qualified Data.Map.Merge.Lazy as Map
12+import qualified System.IO as IO
13+import Text.PrettyPrint.ANSI.Leijen ((<>), (<+>))
14+import qualified Text.PrettyPrint.ANSI.Leijen as P
15+
16+import qualified Elm.Package as Pkg
17+import Elm.Package (Name, Version)
18+
19+import Deps.Explorer (Explorer)
20+import qualified Deps.Explorer as Explorer
21+import qualified Deps.Verify as Verify
22+import qualified Deps.Solver as Solver
23+import Elm.Project.Constraint (Constraint)
24+import qualified Elm.Project.Constraint as Con
25+import qualified Elm.Project.Json as Project
26+import qualified Elm.Project.Root as Root
27+import qualified Reporting.Error as Error
28+import qualified Reporting.Task as Task
29+
30+
31+
32+-- INSTALL
33+
34+
35+install :: Name -> Task.Task ()
36+install pkg =
37+ do (root, oldProject) <- Root.unsafeGet
38+ makePlan pkg oldProject $ \newProject ->
39+ let
40+ upgrade =
41+ do liftIO $ Project.write root newProject
42+ void $ Verify.verify root newProject
43+
44+ revert err =
45+ do liftIO $ Project.write root oldProject
46+ Task.throw err
47+ in
48+ upgrade `catchError` revert
49+
50+
51+
52+-- MAKE PLAN
53+
54+
55+makePlan :: Name -> Project.Project -> (Project.Project -> Task.Task ()) -> Task.Task ()
56+makePlan pkg project attemptInstall =
57+ case project of
58+ Project.App info@(Project.AppInfo elm srcDirs deps test trans) ->
59+ do changes <- addToApp pkg info
60+
61+ let news = Map.mapMaybe keepNew changes
62+ let newDeps = addNews (Just pkg) news deps
63+ let newTest = addNews Nothing news test
64+ let newTrans = Map.union (Map.difference news (Map.union newDeps newTest)) trans
65+ let newInfo = Project.AppInfo elm srcDirs newDeps newTest newTrans
66+ let newProject = Project.App newInfo
67+
68+ withApproval Pkg.versionToString changes (attemptInstall newProject)
69+
70+ Project.Pkg info@(Project.PkgInfo _ _ _ _ _ deps test _) ->
71+ do changes <- addToPkg pkg info
72+
73+ let news = Map.mapMaybe keepNew changes
74+ let newProject = Project.Pkg $
75+ info
76+ { Project._pkg_deps = addNews (Just pkg) news deps
77+ , Project._pkg_test_deps = addNews Nothing news test
78+ }
79+
80+ withApproval (show . Con.toString) changes (attemptInstall newProject)
81+
82+
83+addNews :: Maybe Name -> Map Name a -> Map Name a -> Map Name a
84+addNews pkg new old =
85+ Map.merge
86+ Map.preserveMissing
87+ (Map.mapMaybeMissing (\k v -> if Just k == pkg then Just v else Nothing))
88+ (Map.zipWithMatched (\_ _ n -> n))
89+ old
90+ new
91+
92+
93+
94+-- CHANGES
95+
96+
97+data Change a
98+ = Insert a
99+ | Change a a
100+ | Remove a
101+
102+
103+detectChanges :: (Eq a) => Map Name a -> Map Name a -> Map Name (Change a)
104+detectChanges old new =
105+ Map.merge
106+ (Map.mapMissing (\_ v -> Remove v))
107+ (Map.mapMissing (\_ v -> Insert v))
108+ (Map.zipWithMaybeMatched keepChange)
109+ old
110+ new
111+
112+
113+keepChange :: (Eq v) => k -> v -> v -> Maybe (Change v)
114+keepChange _ old new =
115+ if old == new then
116+ Nothing
117+ else
118+ Just (Change old new)
119+
120+
121+keepNew :: Change a -> Maybe a
122+keepNew change =
123+ case change of
124+ Insert a ->
125+ Just a
126+
127+ Change _ a ->
128+ Just a
129+
130+ Remove _ ->
131+ Nothing
132+
133+
134+
135+-- ADD TO APP
136+
137+
138+addToApp :: Name -> Project.AppInfo -> Task.Task (Map Name (Change Version))
139+addToApp pkg info@(Project.AppInfo _ _ deps tests trans) =
140+ Explorer.run $
141+ do let old = Map.unions [ deps, tests, trans ]
142+ result <- Solver.run (addToAppHelp pkg info)
143+ case result of
144+ Just new ->
145+ return $ detectChanges old new
146+
147+ Nothing ->
148+ do badNames <- filterM isBadElm (pkg : Map.keys old)
149+ lift $ Task.throw (Error.NoSolution badNames)
150+
151+
152+addToAppHelp :: Name -> Project.AppInfo -> Solver.Solver (Map Name Version)
153+addToAppHelp pkg (Project.AppInfo _ _ deps tests trans) =
154+ let
155+ directs =
156+ Map.union deps tests
157+
158+ everything =
159+ Map.union directs trans
160+
161+ try constraints =
162+ Solver.solve $ Map.insert pkg Con.anything constraints
163+ in
164+ msum $ map try $
165+ [ Map.map Con.exactly everything
166+ , Map.map Con.exactly directs
167+ , Map.map Con.untilNextMinor directs
168+ , Map.map Con.untilNextMajor directs
169+ , Map.map (\_ -> Con.anything) directs
170+ ]
171+
172+
173+
174+-- ADD TO PKG
175+
176+
177+addToPkg :: Name -> Project.PkgInfo -> Task.Task (Map Name (Change Constraint))
178+addToPkg pkg info@(Project.PkgInfo _ _ _ _ _ deps tests _) =
179+ Explorer.run $
180+ do let old = Map.union deps tests
181+ result <- Solver.run (addToPkgHelp pkg info)
182+ case result of
183+ Just new ->
184+ return $ detectChanges old new
185+
186+ Nothing ->
187+ do let pkgs = Map.keys deps ++ Map.keys tests
188+ badNames <- filterM isBadElm (pkg : pkgs)
189+ lift $ Task.throw (Error.NoSolution badNames)
190+
191+
192+addToPkgHelp :: Name -> Project.PkgInfo -> Solver.Solver (Map Name Constraint)
193+addToPkgHelp pkg (Project.PkgInfo _ _ _ _ _ deps tests _) =
194+ do let directs = Map.union deps tests
195+ let newCons = Map.insert pkg Con.anything directs
196+ solution <- Solver.solve newCons
197+ let con = Con.untilNextMajor (solution ! pkg)
198+ return $ Map.insert pkg con directs
199+
200+
201+
202+-- FAILURE HINTS
203+
204+
205+isBadElm :: Name -> Explorer Bool
206+isBadElm name =
207+ do versions <- Explorer.getVersions name
208+
209+ elmVersions <- forM versions $ \vsn ->
210+ Explorer._elm <$> Explorer.getConstraints name vsn
211+
212+ return (not (any Con.goodElm elmVersions))
213+
214+
215+
216+-- VIEW
217+
218+
219+withApproval :: (a -> String) -> Map Name (Change a) -> Task.Task () -> Task.Task ()
220+withApproval toString changes attemptInstall =
221+ let
222+ widths =
223+ Map.foldrWithKey (widen toString) (Widths 0 0 0) changes
224+
225+ changeDocs =
226+ Map.foldrWithKey (addChange toString widths) (Docs [] [] []) changes
227+
228+ printDoc doc =
229+ P.displayIO IO.stdout $ P.renderPretty 1 80 doc
230+ in
231+ case changeDocs of
232+ Docs [] [] [] ->
233+ do liftIO $ putStrLn "Already installed. No changes necessary!"
234+
235+ Docs inserts [] [] ->
236+ do liftIO $ putStrLn "Adding the following dependencies:\n"
237+ liftIO $ printDoc $ P.indent 2 (P.vcat (reverse inserts))
238+ liftIO $ putStrLn "\n"
239+ attemptInstall
240+
241+ Docs _ _ _ ->
242+ do liftIO $ putStrLn "Here is my plan:"
243+ liftIO $ printDoc $ viewChangeDocs changeDocs
244+ liftIO $ putStrLn "\nDo you approve? [Y/n]: "
245+ Task.withApproval attemptInstall
246+
247+
248+
249+-- VIEW CHANGE DOCS
250+
251+
252+data ChangeDocs =
253+ Docs
254+ { _doc_inserts :: [P.Doc]
255+ , _doc_changes :: [P.Doc]
256+ , _doc_removes :: [P.Doc]
257+ }
258+
259+
260+viewChangeDocs :: ChangeDocs -> P.Doc
261+viewChangeDocs (Docs inserts changes removes) =
262+ P.indent 2 $ P.vcat $ concat $
263+ [ viewNonZero "Add:" inserts
264+ , viewNonZero "Change:" changes
265+ , viewNonZero "Remove:" removes
266+ ]
267+
268+
269+viewNonZero :: String -> [P.Doc] -> [P.Doc]
270+viewNonZero title entries =
271+ if null entries then
272+ []
273+ else
274+ [ P.text ""
275+ , P.text title
276+ , P.indent 2 (P.vcat (reverse entries))
277+ ]
278+
279+
280+
281+-- VIEW CHANGE
282+
283+
284+addChange :: (a -> String) -> Widths -> Name -> Change a -> ChangeDocs -> ChangeDocs
285+addChange toString widths name change (Docs inserts changes removes) =
286+ case change of
287+ Insert new ->
288+ Docs (viewInsert toString widths name new : inserts) changes removes
289+
290+ Change old new ->
291+ Docs inserts (viewChange toString widths name old new : changes) removes
292+
293+ Remove old ->
294+ Docs inserts changes (viewRemove toString widths name old : removes)
295+
296+
297+viewInsert :: (a -> String) -> Widths -> Name -> a -> P.Doc
298+viewInsert toString (Widths nameWidth leftWidth _) name new =
299+ viewName nameWidth name <+> pad leftWidth (toString new)
300+
301+
302+viewChange :: (a -> String) -> Widths -> Name -> a -> a -> P.Doc
303+viewChange toString (Widths nameWidth leftWidth rightWidth) name old new =
304+ P.hsep
305+ [ viewName nameWidth name
306+ , pad leftWidth (toString old)
307+ , P.text "=>"
308+ , pad rightWidth (toString new)
309+ ]
310+
311+
312+viewRemove :: (a -> String) -> Widths -> Name -> a -> P.Doc
313+viewRemove toString (Widths nameWidth leftWidth _) name old =
314+ viewName nameWidth name <+> pad leftWidth (toString old)
315+
316+
317+viewName :: Int -> Name -> P.Doc
318+viewName width name =
319+ P.fill (width + 3) (P.text (Pkg.toString name))
320+
321+
322+pad :: Int -> String -> P.Doc
323+pad width string =
324+ P.text (replicate (width - length string) ' ') <> P.text string
325+
326+
327+
328+-- WIDTHS
329+
330+
331+data Widths =
332+ Widths
333+ { _name :: !Int
334+ , _left :: !Int
335+ , _right :: !Int
336+ }
337+
338+
339+widen :: (a -> String) -> Name -> Change a -> Widths -> Widths
340+widen toString pkg change (Widths name left right) =
341+ let
342+ toLength a =
343+ length (toString a)
344+
345+ newName =
346+ max name (length (Pkg.toString pkg))
347+ in
348+ case change of
349+ Insert new ->
350+ Widths newName (max left (toLength new)) right
351+
352+ Change old new ->
353+ Widths newName (max left (toLength old)) (max right (toLength new))
354+
355+ Remove old ->
356+ Widths newName (max left (toLength old)) right
builder/src/Elm/PerUserCache.hsnew file+41
@@ -0,0 +1,41 @@
1+{-# OPTIONS_GHC -Wall #-}
2+module Elm.PerUserCache
3+ ( getPackageRoot
4+ , getReplRoot
5+ )
6+ where
7+
8+import qualified System.Directory as Dir
9+import qualified System.Environment as Env
10+import System.FilePath ((</>))
11+
12+import qualified Elm.Compiler as Compiler
13+import qualified Elm.Package as Pkg
14+
15+
16+
17+-- ROOTS
18+
19+
20+getPackageRoot :: IO FilePath
21+getPackageRoot =
22+ getRoot "package"
23+
24+
25+getReplRoot :: IO FilePath
26+getReplRoot =
27+ getRoot "repl"
28+
29+
30+getRoot :: FilePath -> IO FilePath
31+getRoot projectName =
32+ do maybeHome <- Env.lookupEnv "ELM_HOME"
33+ home <- maybe (Dir.getAppUserDataDirectory "elm") return maybeHome
34+ let root = home </> version </> projectName
35+ Dir.createDirectoryIfMissing True root
36+ return root
37+
38+
39+version :: FilePath
40+version =
41+ Pkg.versionToString Compiler.version
builder/src/Elm/Project.hsnew file+85
@@ -0,0 +1,85 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Elm.Project
4+ ( getRoot
5+ , getRootWithReplFallback
6+ , compile
7+ , compileForRepl
8+ , generateDocs
9+ )
10+ where
11+
12+
13+import qualified Data.ByteString as BS
14+import System.FilePath ((</>))
15+
16+import qualified Elm.Docs as Docs
17+import qualified Elm.Name as N
18+import qualified Elm.Project.Flags as Flags
19+import qualified Elm.Project.Root as Root
20+import qualified Elm.Project.Summary as Summary
21+import Elm.Project.Summary (Summary)
22+import qualified File.Args as Args
23+import qualified File.Artifacts as Artifacts
24+import qualified File.Compile as Compile
25+import qualified File.Crawl as Crawl
26+import qualified File.Plan as Plan
27+import qualified Generate.Output as Output
28+import qualified Reporting.Task as Task
29+import qualified Stuff.Paths as Path
30+
31+
32+
33+-- GET ROOT
34+
35+
36+getRoot :: Task.Task Summary
37+getRoot =
38+ Root.get
39+
40+
41+getRootWithReplFallback :: IO FilePath
42+getRootWithReplFallback =
43+ Root.getWithReplFallback
44+
45+
46+
47+-- COMPILE
48+
49+
50+compile :: Flags.Options -> Summary -> [FilePath] -> Task.Task ()
51+compile options summary@(Summary.Summary root project _ _ _) paths =
52+ do args <- Args.fromPaths summary paths
53+ graph <- Crawl.crawl summary args
54+ (dirty, ifaces) <- Plan.plan summary graph
55+ answers <- Compile.compile project ifaces dirty
56+ results <- Artifacts.write root answers
57+ Output.generate options summary graph
58+
59+
60+
61+-- COMPILE FOR REPL
62+
63+
64+compileForRepl :: BS.ByteString -> Maybe N.Name -> Task.Task (Maybe FilePath)
65+compileForRepl source maybeName =
66+ do summary@(Summary.Summary root project _ _ _) <- getRoot
67+ graph <- Crawl.crawlFromSource summary source
68+ (dirty, ifaces) <- Plan.plan summary graph
69+ answers <- Compile.compile project ifaces dirty
70+ results <- Artifacts.write root answers
71+ traverse (Output.generateReplFile summary graph) maybeName
72+
73+
74+
75+-- GENERATE DOCS
76+
77+
78+generateDocs :: Summary.Summary -> Task.Task Docs.Documentation
79+generateDocs summary@(Summary.Summary root project _ _ _) =
80+ do args <- Args.fromSummary summary
81+ graph <- Crawl.crawl summary args
82+ (dirty, ifaces) <- Plan.plan summary graph
83+ answers <- Compile.compile project ifaces dirty
84+ results <- Artifacts.ignore answers
85+ Artifacts.writeDocs (root </> Path.docs) results
builder/src/Elm/Project/Constraint.hsnew file+234
@@ -0,0 +1,234 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Elm.Project.Constraint
4+ ( Constraint
5+ , exactly
6+ , anything
7+ , fromText
8+ , toString
9+ , toText
10+ , satisfies
11+ , check
12+ , intersect
13+ , goodElm
14+ , defaultElm
15+ , untilNextMajor
16+ , untilNextMinor
17+ , expand
18+ )
19+ where
20+
21+import qualified Data.Text as Text
22+import Data.Text (Text)
23+
24+import qualified Elm.Compiler as Compiler
25+import qualified Elm.Package as Pkg
26+import Elm.Package (Version(..))
27+
28+
29+
30+-- CONSTRAINTS
31+
32+
33+data Constraint
34+ = Range Version Op Op Version
35+ deriving (Eq)
36+
37+
38+data Op
39+ = Less
40+ | LessOrEqual
41+ deriving (Eq)
42+
43+
44+
45+-- COMMON CONSTRAINTS
46+
47+
48+exactly :: Version -> Constraint
49+exactly version =
50+ Range version LessOrEqual LessOrEqual version
51+
52+
53+anything :: Constraint
54+anything =
55+ Range (Version 1 0 0) LessOrEqual LessOrEqual (Version 30000 0 0)
56+
57+
58+
59+-- TEXT CONVERSION
60+
61+
62+toString :: Constraint -> String
63+toString constraint =
64+ Text.unpack (toText constraint)
65+
66+
67+toText :: Constraint -> Text
68+toText constraint =
69+ case constraint of
70+ Range lower lowerOp upperOp upper ->
71+ Text.intercalate " "
72+ [ Pkg.versionToText lower
73+ , opToText lowerOp
74+ , "v"
75+ , opToText upperOp
76+ , Pkg.versionToText upper
77+ ]
78+
79+
80+opToText :: Op -> Text
81+opToText op =
82+ case op of
83+ Less ->
84+ "<"
85+
86+ LessOrEqual ->
87+ "<="
88+
89+
90+fromText :: Text -> Either String Constraint
91+fromText text =
92+ case Text.splitOn " " text of
93+ [lower, lowerOp, "v", upperOp, upper] ->
94+ do lo <- versionFromText "lower" lower
95+ lop <- opFromText lowerOp
96+ hop <- opFromText upperOp
97+ hi <- versionFromText "upper" upper
98+ if lo < hi
99+ then Right (Range lo lop hop hi)
100+ else
101+ Left $
102+ "The lower bound (" ++ Text.unpack lower
103+ ++ ") must be less than the upper bound ("
104+ ++ Text.unpack upper ++ ")"
105+
106+ _ ->
107+ Left "The standard format is \"1.0.0 <= v < 2.0.0\""
108+
109+
110+versionFromText :: String -> Text -> Either String Version
111+versionFromText lowerOrUpper text =
112+ case Pkg.versionFromText text of
113+ Just vsn ->
114+ Right vsn
115+
116+ Nothing ->
117+ Left $ "The " ++ lowerOrUpper ++ " bound (" ++ Text.unpack text ++ ") is not a valid version number."
118+
119+
120+opFromText :: Text -> Either String Op
121+opFromText text =
122+ case text of
123+ "<=" ->
124+ Right LessOrEqual
125+
126+ "<" ->
127+ Right Less
128+
129+ _ ->
130+ Left $ "The operator (" ++ Text.unpack text ++ ") is not valid. Must be (<=) or (<)."
131+
132+
133+
134+-- IS SATISFIED
135+
136+
137+satisfies :: Constraint -> Version -> Bool
138+satisfies constraint version =
139+ case constraint of
140+ Range lower lowerOp upperOp upper ->
141+ isLess lowerOp lower version
142+ &&
143+ isLess upperOp version upper
144+
145+
146+isLess :: (Ord a) => Op -> (a -> a -> Bool)
147+isLess op =
148+ case op of
149+ Less ->
150+ (<)
151+
152+ LessOrEqual ->
153+ (<=)
154+
155+
156+check :: Constraint -> Version -> Ordering
157+check constraint version =
158+ case constraint of
159+ Range lower lowerOp upperOp upper ->
160+ if not (isLess lowerOp lower version) then
161+ LT
162+
163+ else if not (isLess upperOp version upper) then
164+ GT
165+
166+ else
167+ EQ
168+
169+
170+
171+-- INTERSECT
172+
173+
174+intersect :: Constraint -> Constraint -> Maybe Constraint
175+intersect (Range lo lop hop hi) (Range lo_ lop_ hop_ hi_) =
176+ let
177+ (newLo, newLop) =
178+ case compare lo lo_ of
179+ LT -> (lo_, lop_)
180+ EQ -> (lo, if elem Less [lop,lop_] then Less else LessOrEqual)
181+ GT -> (lo, lop)
182+
183+ (newHi, newHop) =
184+ case compare hi hi_ of
185+ LT -> (hi, hop)
186+ EQ -> (hi, if elem Less [hop, hop_] then Less else LessOrEqual)
187+ GT -> (hi_, hop_)
188+ in
189+ if newLo <= newHi then
190+ Just (Range newLo newLop newHop newHi)
191+ else
192+ Nothing
193+
194+
195+
196+-- ELM CONSTRAINT
197+
198+
199+goodElm :: Constraint -> Bool
200+goodElm constraint =
201+ satisfies constraint Compiler.version
202+
203+
204+defaultElm :: Constraint
205+defaultElm =
206+ if Pkg._major Compiler.version > 0
207+ then untilNextMajor Compiler.version
208+ else untilNextMinor Compiler.version
209+
210+
211+
212+-- CREATE CONSTRAINTS
213+
214+
215+untilNextMajor :: Version -> Constraint
216+untilNextMajor version =
217+ Range version LessOrEqual Less (Pkg.bumpMajor version)
218+
219+
220+untilNextMinor :: Version -> Constraint
221+untilNextMinor version =
222+ Range version LessOrEqual Less (Pkg.bumpMinor version)
223+
224+
225+expand :: Constraint -> Version -> Constraint
226+expand constraint@(Range lower lowerOp upperOp upper) version
227+ | version < lower =
228+ Range version LessOrEqual upperOp upper
229+
230+ | version > upper =
231+ Range lower lowerOp Less (Pkg.bumpMajor version)
232+
233+ | otherwise =
234+ constraint
builder/src/Elm/Project/Flags.hsnew file+38
@@ -0,0 +1,38 @@
1+{-# OPTIONS_GHC -Wall #-}
2+module Elm.Project.Flags
3+ ( Options(..)
4+ , Output(..)
5+ , safeCustomPath
6+ )
7+ where
8+
9+
10+import qualified System.Directory as Dir
11+import System.FilePath ((</>))
12+
13+import qualified Elm.Compiler.Objects as Obj
14+
15+
16+
17+data Options =
18+ Options
19+ { _mode :: Obj.Mode
20+ , _target :: Obj.Target
21+ , _output :: Maybe Output
22+ }
23+
24+
25+data Output
26+ = None
27+ | Custom (Maybe FilePath) FilePath
28+
29+
30+safeCustomPath :: Maybe FilePath -> FilePath -> IO FilePath
31+safeCustomPath maybeDirectory fileName =
32+ case maybeDirectory of
33+ Nothing ->
34+ do return fileName
35+
36+ Just dir ->
37+ do Dir.createDirectoryIfMissing True dir
38+ return (dir </> fileName)
builder/src/Elm/Project/Json.hsnew file+439
@@ -0,0 +1,439 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Elm.Project.Json
4+ ( Project(..)
5+ , AppInfo(..)
6+ , PkgInfo(..)
7+ , Exposed(..)
8+ , defaultSummary
9+ -- json
10+ , write
11+ , encode
12+ , read
13+ , pkgDecoder
14+ -- queries
15+ , appSolution
16+ , isPlatformPackage
17+ , isPackageRoot
18+ , get
19+ , getName
20+ , getExposed
21+ )
22+ where
23+
24+
25+import Prelude hiding (read)
26+import Control.Monad.Trans (liftIO)
27+import qualified Data.ByteString as BS
28+import Data.Foldable (traverse_)
29+import Data.Map (Map)
30+import Data.Text (Text)
31+import qualified Data.Map as Map
32+import qualified Data.Text as Text
33+import qualified System.Directory as Dir
34+import System.FilePath ((</>))
35+
36+import qualified Elm.Compiler.Module as Module
37+import qualified Elm.Package as Pkg
38+import Elm.Package (Name, Version)
39+
40+import qualified Elm.Project.Constraint as Con
41+import qualified Elm.Project.Licenses as Licenses
42+import qualified Json.Decode as D
43+import qualified Json.Encode as E
44+import qualified Reporting.Error as Error
45+import qualified Reporting.Error.Assets as E
46+import qualified Reporting.Task as Task
47+
48+
49+
50+-- PROJECT
51+
52+
53+data Project
54+ = App AppInfo
55+ | Pkg PkgInfo
56+
57+
58+
59+-- APPLICATION
60+
61+
62+data AppInfo =
63+ AppInfo
64+ { _app_elm_version :: Version
65+ , _app_source_dirs :: [FilePath]
66+ , _app_deps :: Map Name Version
67+ , _app_test_deps :: Map Name Version
68+ , _app_trans_deps :: Map Name Version
69+ }
70+
71+
72+
73+-- PACKAGE
74+
75+
76+data PkgInfo =
77+ PkgInfo
78+ { _pkg_name :: Name
79+ , _pkg_summary :: Text
80+ , _pkg_license :: Licenses.License
81+ , _pkg_version :: Version
82+ , _pkg_exposed :: Exposed
83+ , _pkg_deps :: Map Name Con.Constraint
84+ , _pkg_test_deps :: Map Name Con.Constraint
85+ , _pkg_elm_version :: Con.Constraint
86+ }
87+
88+
89+data Exposed
90+ = ExposedList [Module.Raw]
91+ | ExposedDict [(Text, [Module.Raw])]
92+
93+
94+
95+-- DEFAULTS
96+
97+
98+defaultSummary :: Text
99+defaultSummary =
100+ "helpful summary of your project, less than 80 characters"
101+
102+
103+
104+-- QUERIES
105+
106+
107+appSolution :: AppInfo -> Map Name Version
108+appSolution info =
109+ Map.unions
110+ [ _app_deps info
111+ , _app_test_deps info
112+ , _app_trans_deps info
113+ ]
114+
115+
116+isPlatformPackage :: Project -> Bool
117+isPlatformPackage project =
118+ case project of
119+ App _ ->
120+ False
121+
122+ Pkg info ->
123+ let
124+ (Pkg.Name user _) =
125+ _pkg_name info
126+ in
127+ user == "elm-lang" || user == "elm-explorations"
128+
129+
130+isPackageRoot :: Module.Raw -> Project -> Bool
131+isPackageRoot name project =
132+ case project of
133+ App _ ->
134+ False
135+
136+ Pkg info ->
137+ case _pkg_exposed info of
138+ ExposedList modules ->
139+ elem name modules
140+
141+ ExposedDict chunks ->
142+ elem name (concatMap snd chunks)
143+
144+
145+get :: (AppInfo -> a) -> (PkgInfo -> a) -> Project -> a
146+get appFunc pkgFunc project =
147+ case project of
148+ App info ->
149+ appFunc info
150+
151+ Pkg info ->
152+ pkgFunc info
153+
154+
155+getName :: Project -> Name
156+getName project =
157+ get (\_ -> Pkg.dummyName) _pkg_name project
158+
159+
160+getExposed :: PkgInfo -> [Module.Raw]
161+getExposed info =
162+ case _pkg_exposed info of
163+ ExposedList modules ->
164+ modules
165+
166+ ExposedDict chunks ->
167+ concatMap snd chunks
168+
169+
170+
171+-- WRITE
172+
173+
174+write :: FilePath -> Project -> IO ()
175+write root project =
176+ E.write (root </> "elm.json") (encode project)
177+
178+
179+
180+-- JSON ENCODE
181+
182+
183+encode :: Project -> E.Value
184+encode project =
185+ case project of
186+ App (AppInfo elm srcDirs deps tests trans) ->
187+ E.object
188+ [ "type" ==> E.text "browser"
189+ , "source-directories" ==> E.list (E.text . Text.pack) srcDirs
190+ , "elm-version" ==> encodeVersion elm
191+ , "dependencies" ==> encodeDeps encodeVersion deps
192+ , "test-dependencies" ==> encodeDeps encodeVersion tests
193+ , "do-not-edit-this-by-hand" ==>
194+ E.object [ "transitive-dependencies" ==> encodeDeps encodeVersion trans ]
195+ ]
196+
197+ Pkg (PkgInfo name summary license version exposed deps tests elm) ->
198+ E.object
199+ [ "type" ==> E.text "package"
200+ , "name" ==> E.text (Pkg.toText name)
201+ , "summary" ==> E.text summary
202+ , "license" ==> Licenses.encode license
203+ , "version" ==> E.text (Pkg.versionToText version)
204+ , "exposed-modules" ==> encodeExposed exposed
205+ , "elm-version" ==> encodeConstraint elm
206+ , "dependencies" ==> encodeDeps encodeConstraint deps
207+ , "test-dependencies" ==> encodeDeps encodeConstraint tests
208+ ]
209+
210+
211+(==>) :: a -> b -> (a, b)
212+(==>) a b =
213+ (a, b)
214+
215+
216+encodeExposed :: Exposed -> E.Value
217+encodeExposed exposed =
218+ case exposed of
219+ ExposedList modules ->
220+ E.list encodeModule modules
221+
222+ ExposedDict chunks ->
223+ E.object (map (fmap (E.list encodeModule)) chunks)
224+
225+
226+encodeModule :: Module.Raw -> E.Value
227+encodeModule name =
228+ E.name name
229+
230+
231+encodeDeps :: (a -> E.Value) -> Map Pkg.Name a -> E.Value
232+encodeDeps encodeValue deps =
233+ E.dict Pkg.toText encodeValue deps
234+
235+
236+encodeConstraint :: Con.Constraint -> E.Value
237+encodeConstraint constraint =
238+ E.text (Con.toText constraint)
239+
240+
241+encodeVersion :: Pkg.Version -> E.Value
242+encodeVersion version =
243+ E.text (Pkg.versionToText version)
244+
245+
246+
247+-- PARSE AND VERIFY
248+
249+
250+read :: FilePath -> Task.Task Project
251+read path =
252+ do bytes <- liftIO $ BS.readFile path
253+ case D.parse path "project" E.badContentToDocs decoder bytes of
254+ Left err ->
255+ throwBadJson (E.BadJson err)
256+
257+ Right project@(Pkg (PkgInfo _ _ _ _ _ deps tests _)) ->
258+ do checkOverlap "dependencies" "test-dependencies" deps tests
259+ return project
260+
261+ Right project@(App (AppInfo _ srcDirs deps tests trans)) ->
262+ do checkOverlap "dependencies" "test-dependencies" deps tests
263+ checkOverlap "dependencies" "transitive-dependencies" deps trans
264+ checkOverlap "test-dependencies" "transitive-dependencies" tests trans
265+ mapM_ doesDirectoryExist srcDirs
266+ return project
267+
268+
269+throwBadJson :: E.ElmJsonProblem -> Task.Task a
270+throwBadJson problem =
271+ Task.throw (Error.Assets (E.BadElmJson problem))
272+
273+
274+checkOverlap :: String -> String -> Map Name a -> Map Name a -> Task.Task ()
275+checkOverlap field1 field2 deps1 deps2 =
276+ case Map.keys (Map.intersection deps1 deps2) of
277+ [] ->
278+ return ()
279+
280+ dup : dups ->
281+ throwBadJson (E.BadDepDup field1 field2 dup dups)
282+
283+
284+doesDirectoryExist :: FilePath -> Task.Task ()
285+doesDirectoryExist dir =
286+ do exists <- liftIO $ Dir.doesDirectoryExist dir
287+ if exists
288+ then return ()
289+ else throwBadJson (E.BadSrcDir dir)
290+
291+
292+
293+-- JSON DECODE
294+
295+
296+type Decoder a =
297+ D.Decoder E.BadElmJsonContent a
298+
299+
300+decoder :: Decoder Project
301+decoder =
302+ do tipe <- D.field "type" D.text
303+ case tipe of
304+ "browser" ->
305+ D.map App appDecoder
306+
307+ "package" ->
308+ D.map Pkg pkgDecoder
309+
310+ other ->
311+ D.fail (E.BadType other)
312+
313+
314+appDecoder :: Decoder AppInfo
315+appDecoder =
316+ AppInfo
317+ <$> D.field "elm-version" versionDecoder
318+ <*> D.field "source-directories" (D.list dirDecoder)
319+ <*> D.field "dependencies" (depsDecoder versionDecoder)
320+ <*> D.field "test-dependencies" (depsDecoder versionDecoder)
321+ <*> D.at ["do-not-edit-this-by-hand", "transitive-dependencies"] (depsDecoder versionDecoder)
322+
323+
324+pkgDecoder :: Decoder PkgInfo
325+pkgDecoder =
326+ PkgInfo
327+ <$> D.field "name" pkgNameDecoder
328+ <*> D.field "summary" summaryDecoder
329+ <*> D.field "license" licenseDecoder
330+ <*> D.field "version" versionDecoder
331+ <*> D.field "exposed-modules" exposedDecoder
332+ <*> D.field "dependencies" (depsDecoder constraintDecoder)
333+ <*> D.field "test-dependencies" (depsDecoder constraintDecoder)
334+ <*> D.field "elm-version" constraintDecoder
335+
336+
337+
338+-- JSON DECODE HELPERS
339+
340+
341+pkgNameDecoder :: Decoder Pkg.Name
342+pkgNameDecoder =
343+ D.mapError E.BadPkgName Pkg.decoder
344+
345+
346+summaryDecoder :: Decoder Text
347+summaryDecoder =
348+ do summary <- D.text
349+ if Text.length summary < 80
350+ then D.succeed summary
351+ else D.fail E.BadSummaryTooLong
352+
353+
354+licenseDecoder :: Decoder Licenses.License
355+licenseDecoder =
356+ do txt <- D.text
357+ case Licenses.check txt of
358+ Left suggestions ->
359+ D.fail (E.BadLicense txt suggestions)
360+
361+ Right license ->
362+ D.succeed license
363+
364+
365+versionDecoder :: Decoder Version
366+versionDecoder =
367+ do txt <- D.text
368+ case Pkg.versionFromText txt of
369+ Just version ->
370+ D.succeed version
371+
372+ Nothing ->
373+ D.fail (E.BadVersion txt)
374+
375+
376+constraintDecoder :: Decoder Con.Constraint
377+constraintDecoder =
378+ do txt <- D.text
379+ case Con.fromText txt of
380+ Right constraint ->
381+ D.succeed constraint
382+
383+ Left msg ->
384+ D.fail (E.BadConstraint msg)
385+
386+
387+depsDecoder :: Decoder a -> Decoder (Map Name a)
388+depsDecoder valueDecoder =
389+ Map.fromList <$> (
390+ traverse validateKey =<< D.pairs valueDecoder
391+ )
392+
393+
394+validateKey :: (Text, a) -> Decoder (Name, a)
395+validateKey (key, value) =
396+ case Pkg.fromText key of
397+ Right name ->
398+ D.succeed (name, value)
399+
400+ Left _ ->
401+ D.fail (E.BadDependencyName key)
402+
403+
404+dirDecoder :: Decoder FilePath
405+dirDecoder =
406+ do maybeText <- D.maybe D.text
407+ case maybeText of
408+ Nothing ->
409+ D.fail E.BadDirectoryNotString
410+
411+ Just txt ->
412+ D.succeed (Text.unpack txt)
413+
414+
415+
416+-- EXPOSED MODULES DECODER
417+
418+
419+exposedDecoder :: Decoder Exposed
420+exposedDecoder =
421+ D.oneOf
422+ [ ExposedList <$> D.list moduleDecoder
423+ , do pairs <- D.pairs (D.list moduleDecoder)
424+ traverse_ (checkHeader . fst) pairs
425+ return (ExposedDict pairs)
426+ ]
427+
428+
429+moduleDecoder :: Decoder Module.Raw
430+moduleDecoder =
431+ D.mapError E.BadModuleName Module.decoder
432+
433+
434+checkHeader :: Text -> Decoder ()
435+checkHeader header =
436+ if Text.length header < 20 then
437+ D.succeed ()
438+ else
439+ D.fail (E.BadModuleHeaderTooLong header)
builder/src/Elm/Project/Licenses.hsnew file+183
@@ -0,0 +1,183 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Elm.Project.Licenses
4+ ( License
5+ , check
6+ , bsd3
7+ , encode
8+ )
9+ where
10+
11+
12+import Data.Map ((!))
13+import qualified Data.Map as Map
14+import qualified Data.Text as Text
15+
16+import qualified Elm.Utils as Utils
17+import qualified Json.Encode as Encode
18+
19+
20+
21+-- LICENCES
22+
23+
24+data License =
25+ License
26+ { _name :: Text.Text
27+ , _code :: Text.Text
28+ }
29+ deriving (Eq, Ord)
30+
31+
32+bsd3 :: License
33+bsd3 =
34+ osiApprovedSpdxLicenses ! "BSD-3-Clause"
35+
36+
37+encode :: License -> Encode.Value
38+encode (License _ code) =
39+ Encode.text code
40+
41+
42+
43+-- CHECK
44+
45+
46+check :: Text.Text -> Either [Text.Text] License
47+check rawName =
48+ case Map.lookup rawName osiApprovedSpdxLicenses of
49+ Just license ->
50+ Right license
51+
52+ Nothing ->
53+ let
54+ toPairs (License name code) =
55+ [ (code, code), (name, code) ]
56+
57+ pairs =
58+ concatMap toPairs (Map.elems osiApprovedSpdxLicenses)
59+
60+ toSuggestion (suggestion, _) =
61+ Text.unpack suggestion
62+ in
63+ Left $ map snd $
64+ Utils.nearbyNames toSuggestion (rawName, rawName) pairs
65+
66+
67+
68+
69+-- LIST OF LICENCES
70+
71+
72+(==>) :: Text.Text -> Text.Text -> (Text.Text, License)
73+(==>) code name =
74+ ( code, License name code )
75+
76+
77+--
78+-- OSI approved licenses in SPDX format.
79+-- <https://spdx.org/licenses/>
80+--
81+osiApprovedSpdxLicenses :: Map.Map Text.Text License
82+osiApprovedSpdxLicenses =
83+ Map.fromList
84+ [ "AFL-1.1" ==> "Academic Free License v1.1"
85+ , "AFL-1.2" ==> "Academic Free License v1.2"
86+ , "AFL-2.0" ==> "Academic Free License v2.0"
87+ , "AFL-2.1" ==> "Academic Free License v2.1"
88+ , "AFL-3.0" ==> "Academic Free License v3.0"
89+ , "APL-1.0" ==> "Adaptive Public License 1.0"
90+ , "Apache-1.1" ==> "Apache License 1.1"
91+ , "Apache-2.0" ==> "Apache License 2.0"
92+ , "APSL-1.0" ==> "Apple Public Source License 1.0"
93+ , "APSL-1.1" ==> "Apple Public Source License 1.1"
94+ , "APSL-1.2" ==> "Apple Public Source License 1.2"
95+ , "APSL-2.0" ==> "Apple Public Source License 2.0"
96+ , "Artistic-1.0" ==> "Artistic License 1.0"
97+ , "Artistic-1.0-Perl" ==> "Artistic License 1.0 (Perl)"
98+ , "Artistic-1.0-cl8" ==> "Artistic License 1.0 w/clause 8"
99+ , "Artistic-2.0" ==> "Artistic License 2.0"
100+ , "AAL" ==> "Attribution Assurance License"
101+ , "BSL-1.0" ==> "Boost Software License 1.0"
102+ , "BSD-2-Clause" ==> "BSD 2-clause \"Simplified\" License"
103+ , "BSD-3-Clause" ==> "BSD 3-clause \"New\" or \"Revised\" License"
104+ , "0BSD" ==> "BSD Zero Clause License"
105+ , "CECILL-2.1" ==> "CeCILL Free Software License Agreement v2.1"
106+ , "CNRI-Python" ==> "CNRI Python License"
107+ , "CDDL-1.0" ==> "Common Development and Distribution License 1.0"
108+ , "CPAL-1.0" ==> "Common Public Attribution License 1.0"
109+ , "CPL-1.0" ==> "Common Public License 1.0"
110+ , "CATOSL-1.1" ==> "Computer Associates Trusted Open Source License 1.1"
111+ , "CUA-OPL-1.0" ==> "CUA Office Public License v1.0"
112+ , "EPL-1.0" ==> "Eclipse Public License 1.0"
113+ , "ECL-1.0" ==> "Educational Community License v1.0"
114+ , "ECL-2.0" ==> "Educational Community License v2.0"
115+ , "EFL-1.0" ==> "Eiffel Forum License v1.0"
116+ , "EFL-2.0" ==> "Eiffel Forum License v2.0"
117+ , "Entessa" ==> "Entessa Public License v1.0"
118+ , "EUDatagrid" ==> "EU DataGrid Software License"
119+ , "EUPL-1.1" ==> "European Union Public License 1.1"
120+ , "Fair" ==> "Fair License"
121+ , "Frameworx-1.0" ==> "Frameworx Open License 1.0"
122+ , "AGPL-3.0" ==> "GNU Affero General Public License v3.0"
123+ , "GPL-2.0" ==> "GNU General Public License v2.0 only"
124+ , "GPL-3.0" ==> "GNU General Public License v3.0 only"
125+ , "LGPL-2.1" ==> "GNU Lesser General Public License v2.1 only"
126+ , "LGPL-3.0" ==> "GNU Lesser General Public License v3.0 only"
127+ , "LGPL-2.0" ==> "GNU Library General Public License v2 only"
128+ , "HPND" ==> "Historic Permission Notice and Disclaimer"
129+ , "IPL-1.0" ==> "IBM Public License v1.0"
130+ , "Intel" ==> "Intel Open Source License"
131+ , "IPA" ==> "IPA Font License"
132+ , "ISC" ==> "ISC License"
133+ , "LPPL-1.3c" ==> "LaTeX Project Public License v1.3c"
134+ , "LiLiQ-P-1.1" ==> "Licence Libre du Québec – Permissive version 1.1"
135+ , "LiLiQ-Rplus-1.1" ==> "Licence Libre du Québec – Réciprocité forte version 1.1"
136+ , "LiLiQ-R-1.1" ==> "Licence Libre du Québec – Réciprocité version 1.1"
137+ , "LPL-1.02" ==> "Lucent Public License v1.02"
138+ , "LPL-1.0" ==> "Lucent Public License Version 1.0"
139+ , "MS-PL" ==> "Microsoft Public License"
140+ , "MS-RL" ==> "Microsoft Reciprocal License"
141+ , "MirOS" ==> "MirOS Licence"
142+ , "MIT" ==> "MIT License"
143+ , "Motosoto" ==> "Motosoto License"
144+ , "MPL-1.0" ==> "Mozilla Public License 1.0"
145+ , "MPL-1.1" ==> "Mozilla Public License 1.1"
146+ , "MPL-2.0" ==> "Mozilla Public License 2.0"
147+ , "MPL-2.0-no-copyleft-exception" ==> "Mozilla Public License 2.0 (no copyleft exception)"
148+ , "Multics" ==> "Multics License"
149+ , "NASA-1.3" ==> "NASA Open Source Agreement 1.3"
150+ , "Naumen" ==> "Naumen Public License"
151+ , "NGPL" ==> "Nethack General Public License"
152+ , "Nokia" ==> "Nokia Open Source License"
153+ , "NPOSL-3.0" ==> "Non-Profit Open Software License 3.0"
154+ , "NTP" ==> "NTP License"
155+ , "OCLC-2.0" ==> "OCLC Research Public License 2.0"
156+ , "OGTSL" ==> "Open Group Test Suite License"
157+ , "OSL-1.0" ==> "Open Software License 1.0"
158+ , "OSL-2.0" ==> "Open Software License 2.0"
159+ , "OSL-2.1" ==> "Open Software License 2.1"
160+ , "OSL-3.0" ==> "Open Software License 3.0"
161+ , "OSET-PL-2.1" ==> "OSET Public License version 2.1"
162+ , "PHP-3.0" ==> "PHP License v3.0"
163+ , "PostgreSQL" ==> "PostgreSQL License"
164+ , "Python-2.0" ==> "Python License 2.0"
165+ , "QPL-1.0" ==> "Q Public License 1.0"
166+ , "RPSL-1.0" ==> "RealNetworks Public Source License v1.0"
167+ , "RPL-1.1" ==> "Reciprocal Public License 1.1"
168+ , "RPL-1.5" ==> "Reciprocal Public License 1.5"
169+ , "RSCPL" ==> "Ricoh Source Code Public License"
170+ , "OFL-1.1" ==> "SIL Open Font License 1.1"
171+ , "SimPL-2.0" ==> "Simple Public License 2.0"
172+ , "Sleepycat" ==> "Sleepycat License"
173+ , "SISSL" ==> "Sun Industry Standards Source License v1.1"
174+ , "SPL-1.0" ==> "Sun Public License v1.0"
175+ , "Watcom-1.0" ==> "Sybase Open Watcom Public License 1.0"
176+ , "UPL-1.0" ==> "Universal Permissive License v1.0"
177+ , "NCSA" ==> "University of Illinois/NCSA Open Source License"
178+ , "VSL-1.0" ==> "Vovida Software License v1.0"
179+ , "W3C" ==> "W3C Software Notice and License (2002-12-31)"
180+ , "Xnet" ==> "X.Net License"
181+ , "Zlib" ==> "zlib License"
182+ , "ZPL-2.0" ==> "Zope Public License 2.0"
183+ ]
builder/src/Elm/Project/Root.hsnew file+95
@@ -0,0 +1,95 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Elm.Project.Root
4+ ( get
5+ , unsafeGet
6+ , getWithReplFallback
7+ )
8+ where
9+
10+import Control.Monad.Trans (liftIO)
11+import qualified Data.Map as Map
12+import qualified System.Directory as Dir
13+import System.FilePath ((</>))
14+
15+import qualified Elm.Package as Pkg
16+import qualified Elm.Project.Constraint as Con
17+import qualified Elm.Project.Json as Project
18+import qualified Elm.Project.Licenses as Licenses
19+import Elm.Project.Json (Project(..), PkgInfo(..))
20+import Elm.Project.Summary (Summary)
21+import qualified Elm.PerUserCache as PerUserCache
22+import qualified File.IO as IO
23+import qualified Reporting.Error as Error
24+import qualified Reporting.Task as Task
25+import qualified Stuff.Verify as Verify
26+import qualified Json.Encode as Encode
27+
28+
29+
30+-- GET
31+
32+
33+get :: Task.Task Summary
34+get =
35+ do root <- moveToRoot
36+ project <- Project.read "elm.json"
37+ Verify.verify root project
38+
39+
40+unsafeGet :: Task.Task (FilePath, Project)
41+unsafeGet =
42+ (,) <$> moveToRoot <*> Project.read "elm.json"
43+
44+
45+
46+-- MOVE TO ROOT
47+
48+
49+moveToRoot :: Task.Task FilePath
50+moveToRoot =
51+ do maybeRoot <- liftIO $ IO.find "elm.json"
52+ case maybeRoot of
53+ Just root ->
54+ do liftIO $ Dir.setCurrentDirectory root
55+ return root
56+
57+ Nothing ->
58+ Task.throw Error.NoElmJson
59+
60+
61+
62+-- GET WITH FALLBACK
63+
64+
65+getWithReplFallback :: IO FilePath
66+getWithReplFallback =
67+ do maybeRoot <- IO.find "elm.json"
68+
69+ case maybeRoot of
70+ Just root ->
71+ do Dir.setCurrentDirectory root
72+ return root
73+
74+ Nothing ->
75+ do cache <- PerUserCache.getReplRoot
76+ let root = cache </> "tmp"
77+ Dir.createDirectoryIfMissing True root
78+ Dir.setCurrentDirectory root
79+ IO.removeDir "elm-stuff"
80+ Encode.write "elm.json" (Project.encode (Pkg replInfo))
81+ return root
82+
83+
84+replInfo :: PkgInfo
85+replInfo =
86+ PkgInfo
87+ { _pkg_name = Pkg.dummyName
88+ , _pkg_summary = "dummy code for the REPL"
89+ , _pkg_license = Licenses.bsd3
90+ , _pkg_version = Pkg.dummyVersion
91+ , _pkg_exposed = Project.ExposedList []
92+ , _pkg_deps = Map.singleton Pkg.core Con.anything
93+ , _pkg_test_deps = Map.empty
94+ , _pkg_elm_version = Con.defaultElm
95+ }
builder/src/Elm/Project/Summary.hsnew file+105
@@ -0,0 +1,105 @@
1+{-# OPTIONS_GHC -Wall #-}
2+module Elm.Project.Summary
3+ ( Summary(..)
4+ , ExposedModules
5+ , DepsGraph
6+ , init
7+ , cheapInit
8+ )
9+ where
10+
11+
12+import Prelude hiding (init)
13+import qualified Data.List as List
14+import qualified Data.Map as Map
15+import Data.Map (Map)
16+
17+import qualified Elm.Compiler.Module as Module
18+import Elm.Package (Package(..), Name, Version)
19+import Elm.Project.Json (Project(..), AppInfo(..), PkgInfo(..))
20+import qualified Elm.Project.Json as Project
21+
22+
23+
24+-- SUMMARY
25+
26+
27+data Summary =
28+ Summary
29+ { _root :: FilePath
30+ , _project :: Project
31+ , _exposed :: ExposedModules
32+ , _ifaces :: Module.Interfaces
33+ , _depsGraph :: DepsGraph
34+ }
35+
36+
37+type ExposedModules =
38+ Map.Map Module.Raw [Package]
39+
40+
41+type DepsGraph =
42+ Map.Map Name ( Version, [Name] )
43+
44+
45+
46+-- MAKE SUMMARY
47+
48+
49+init :: FilePath -> Project -> Map Name PkgInfo -> Module.Interfaces -> Summary
50+init root project deps ifaces =
51+ let
52+ exposed =
53+ Project.get
54+ (getExposed deps . _app_deps)
55+ (getExposed deps . _pkg_deps)
56+ project
57+
58+ privatizedInterfaces =
59+ Map.mapMaybeWithKey (privatize exposed) ifaces
60+
61+ depsGraph =
62+ Map.foldr toNode Map.empty deps
63+ in
64+ Summary root project exposed privatizedInterfaces depsGraph
65+
66+
67+privatize :: ExposedModules -> Module.Canonical -> Module.Interface -> Maybe Module.Interface
68+privatize exposed (Module.Canonical _ name) iface =
69+ case Map.lookup name exposed of
70+ Just [_] ->
71+ Just iface
72+
73+ _ ->
74+ Nothing
75+
76+
77+toNode :: PkgInfo -> DepsGraph -> DepsGraph
78+toNode (PkgInfo name _ _ version _ deps _ _) graph =
79+ Map.insert name (version, Map.keys deps) graph
80+
81+
82+
83+-- MAKE CHEAP SUMMARY
84+
85+
86+cheapInit :: FilePath -> PkgInfo -> Map Name PkgInfo -> Module.Interfaces -> Summary
87+cheapInit root info deps ifaces =
88+ Summary root (Pkg info) (getExposed deps (_pkg_deps info)) ifaces Map.empty
89+
90+
91+getExposed :: Map Name PkgInfo -> Map Name a -> ExposedModules
92+getExposed deps directs =
93+ Map.foldl insertExposed Map.empty (Map.intersection deps directs)
94+
95+
96+insertExposed :: ExposedModules -> PkgInfo -> ExposedModules
97+insertExposed exposed info =
98+ let
99+ home =
100+ Package (_pkg_name info) (_pkg_version info)
101+
102+ insertModule dict modul =
103+ Map.insertWith (++) modul [home] dict
104+ in
105+ List.foldl' insertModule exposed (Project.getExposed info)
builder/src/Elm/Publish.hsnew file+255
@@ -0,0 +1,255 @@
1+{-# OPTIONS_GHC -Wall #-}
2+module Elm.Publish (publish) where
3+
4+
5+import Control.Monad (when)
6+import Control.Monad.Except (catchError, runExceptT)
7+import Control.Monad.Trans (liftIO, lift)
8+import qualified Data.List as List
9+import qualified Data.Text as Text
10+import qualified System.Directory as Dir
11+import qualified System.Exit as Exit
12+import System.FilePath ((</>))
13+import qualified System.IO as IO
14+import qualified System.Process as Process
15+
16+import qualified Deps.Diff as Diff
17+import qualified Deps.Get as Get
18+import qualified Deps.Website as Website
19+import qualified Elm.Bump as Bump
20+import qualified Elm.Docs as Docs
21+import qualified Elm.Package as Pkg
22+import qualified Elm.Project as Project
23+import qualified Elm.Project.Json as Project
24+import qualified Elm.Project.Summary as Summary
25+import qualified Reporting.Error as Error
26+import qualified Reporting.Error.Publish as E
27+import qualified Reporting.Progress as Progress
28+import qualified Reporting.Task as Task
29+import qualified Stuff.Paths as Path
30+
31+
32+
33+-- PUBLISH
34+
35+
36+publish :: Summary.Summary -> Task.Task ()
37+publish summary@(Summary.Summary root project _ _ _) =
38+ case project of
39+ Project.App _ ->
40+ throw E.Application
41+
42+ Project.Pkg (Project.PkgInfo name smry _ version exposed _ _ _) ->
43+ do
44+ pkgs <- Get.all Get.RequireLatest
45+ let maybePublishedVersions = either (const Nothing) Just (Get.versions name pkgs)
46+
47+ Task.report (Progress.PublishStart name version maybePublishedVersions)
48+
49+ when (noExposed exposed) $ throw E.NoExposed
50+ when (badSummary smry) $ throw E.NoSummary
51+
52+ verifyReadme root
53+ verifyLicense root
54+ docs <- Task.silently (Project.generateDocs summary)
55+ verifyVersion name version docs maybePublishedVersions
56+ commitHash <- verifyTag name version
57+ verifyNoChanges commitHash version
58+ zipHash <- verifyZip name version
59+
60+ Website.register name version commitHash zipHash
61+
62+ Task.report Progress.PublishEnd
63+
64+
65+throw :: E.Error -> Task.Task a
66+throw err =
67+ Task.throw (Error.Publish err)
68+
69+
70+
71+-- VERIFY SUMMARY
72+
73+
74+badSummary :: Text.Text -> Bool
75+badSummary summary =
76+ Text.null summary || Project.defaultSummary == summary
77+
78+
79+noExposed :: Project.Exposed -> Bool
80+noExposed exposed =
81+ case exposed of
82+ Project.ExposedList modules ->
83+ null modules
84+
85+ Project.ExposedDict chunks ->
86+ all (null . snd) chunks
87+
88+
89+
90+-- VERIFY README
91+
92+
93+verifyReadme :: FilePath -> Task.Task ()
94+verifyReadme root =
95+ phase Progress.CheckReadme $
96+ do let readmePath = root </> "README.md"
97+ exists <- liftIO $ Dir.doesFileExist readmePath
98+ case exists of
99+ False ->
100+ throw E.NoReadme
101+
102+ True ->
103+ do size <- liftIO $ IO.withFile readmePath IO.ReadMode IO.hFileSize
104+ if size < 300
105+ then throw E.ShortReadme
106+ else return ()
107+
108+
109+
110+-- VERIFY LICENSE
111+
112+
113+verifyLicense :: FilePath -> Task.Task ()
114+verifyLicense root =
115+ phase Progress.CheckLicense $
116+ do let licensePath = root </> "LICENSE"
117+ exists <- liftIO $ Dir.doesFileExist licensePath
118+ if exists
119+ then return ()
120+ else throw E.NoLicense
121+
122+
123+
124+-- VERIFY GITHUB TAG
125+
126+
127+verifyTag :: Pkg.Name -> Pkg.Version -> Task.Task String
128+verifyTag name version =
129+ phase (Progress.CheckTag version) $
130+ Website.githubCommit name version `catchError` \_ ->
131+ throw (E.MissingTag version)
132+
133+
134+
135+-- VERIFY NO LOCAL CHANGES SINCE TAG
136+
137+
138+verifyNoChanges :: String -> Pkg.Version -> Task.Task ()
139+verifyNoChanges commitHash version =
140+ phase Progress.CheckChanges $
141+ do maybeGit <- liftIO $ Dir.findExecutable "git"
142+ case maybeGit of
143+ Nothing ->
144+ throw E.NoGit
145+
146+ Just git ->
147+ do -- https://stackoverflow.com/questions/3878624/how-do-i-programmatically-determine-if-there-are-uncommited-changes
148+ exitCode <- liftIO $
149+ Process.rawSystem git [ "diff-index", "--quiet", commitHash, "--" ]
150+
151+ case exitCode of
152+ Exit.ExitSuccess ->
153+ return ()
154+
155+ Exit.ExitFailure _ ->
156+ throw (E.LocalChanges version)
157+
158+
159+
160+-- VERIFY THAT ZIP BUILDS / COMPUTE HASH
161+
162+
163+verifyZip :: Pkg.Name -> Pkg.Version -> Task.Task Website.Sha
164+verifyZip name version =
165+ withTempDir $ \dir ->
166+ do hash <- phase Progress.CheckDownload $
167+ Website.githubDownload name version dir
168+
169+ phase Progress.CheckBuild $
170+ do runner <- Task.getSilentRunner
171+ result <- liftIO $ Dir.withCurrentDirectory dir $ runner $
172+ Task.silently $ Project.generateDocs =<< Project.getRoot
173+ either Task.throw (\_ -> return ()) result
174+
175+ return hash
176+
177+
178+withTempDir :: (FilePath -> Task.Task a) -> Task.Task a
179+withTempDir callback =
180+ do liftIO $ Dir.createDirectoryIfMissing True Path.prepublishDir
181+ result <- lift $ runExceptT $ callback Path.prepublishDir
182+ liftIO $ Dir.removeDirectoryRecursive Path.prepublishDir
183+ either Task.throw return result
184+
185+
186+
187+-- PHASE REPORTING
188+
189+
190+phase :: Progress.PublishPhase -> Task.Task a -> Task.Task a
191+phase publishPhase task =
192+ do Task.report $ Progress.PublishProgress publishPhase Nothing
193+
194+ result <- task `catchError` \err ->
195+ do Task.report $ Progress.PublishProgress publishPhase (Just Progress.Bad)
196+ Task.throw err
197+
198+ Task.report $ Progress.PublishProgress publishPhase (Just Progress.Good)
199+
200+ return result
201+
202+
203+
204+-- VERIFY VERSION
205+
206+
207+verifyVersion :: Pkg.Name -> Pkg.Version -> Docs.Documentation -> Maybe [Pkg.Version] -> Task.Task ()
208+verifyVersion name version docs maybePublishedVersions =
209+ let
210+ reportBumpPhase bumpPhase =
211+ Task.report $ Progress.PublishCheckBump version bumpPhase
212+ in
213+ do reportBumpPhase Progress.StatedVersion
214+ case maybePublishedVersions of
215+ Nothing ->
216+ if version == Pkg.initialVersion then
217+ reportBumpPhase Progress.GoodStart
218+ else
219+ throw (E.NotInitialVersion version)
220+
221+ Just publishedVersions ->
222+ if elem version publishedVersions then
223+ throw $ E.AlreadyPublished version
224+ else
225+ do (old, magnitude) <- verifyBump name version docs publishedVersions
226+ reportBumpPhase (Progress.GoodBump old magnitude)
227+
228+ `catchError` \err ->
229+ do reportBumpPhase Progress.BadBump
230+ Task.throw err
231+
232+
233+verifyBump :: Pkg.Name -> Pkg.Version -> Docs.Documentation -> [Pkg.Version] -> Task.Task (Pkg.Version, Diff.Magnitude)
234+verifyBump name statedVersion newDocs publishedVersions =
235+ let
236+ possibleBumps =
237+ Bump.toPossibleBumps publishedVersions
238+
239+ isTheBump (_ ,new, _) =
240+ statedVersion == new
241+ in
242+ case List.find isTheBump possibleBumps of
243+ Nothing ->
244+ throw $ E.InvalidBump statedVersion (last publishedVersions)
245+
246+ Just (old, new, magnitude) ->
247+ do oldDocs <- Get.docs name old
248+ let changes = Diff.diff oldDocs newDocs
249+ let realNew = Diff.bump changes old
250+ if new == realNew
251+ then
252+ return (old, magnitude)
253+ else
254+ throw $ E.BadBump old new magnitude realNew $
255+ Diff.toMagnitude changes
builder/src/File/Args.hsnew file+40
@@ -0,0 +1,40 @@
1+{-# OPTIONS_GHC -Wall #-}
2+module File.Args
3+ ( Args(..)
4+ , fromPaths
5+ , fromSummary
6+ )
7+ where
8+
9+
10+import Data.List.NonEmpty (NonEmpty((:|)))
11+import qualified Elm.Compiler.Module as Module
12+import qualified Elm.Project.Json as Project
13+import qualified Elm.Project.Summary as Summary
14+import qualified Reporting.Error as Error
15+import qualified Reporting.Task as Task
16+
17+
18+data Args roots
19+ = Pkg [Module.Raw]
20+ | Roots (NonEmpty roots)
21+
22+
23+fromPaths :: Summary.Summary -> [FilePath] -> Task.Task (Args FilePath)
24+fromPaths summary paths =
25+ case paths of
26+ [] ->
27+ fromSummary summary
28+
29+ first : rest ->
30+ return $ Roots (first :| rest)
31+
32+
33+fromSummary :: Summary.Summary -> Task.Task (Args a)
34+fromSummary (Summary.Summary _ project _ _ _) =
35+ case project of
36+ Project.Pkg info ->
37+ return $ Pkg (Project.getExposed info)
38+
39+ Project.App _ ->
40+ Task.throw Error.CannotMakeNothing
builder/src/File/Artifacts.hsnew file+140
@@ -0,0 +1,140 @@
1+module File.Artifacts
2+ ( ignore
3+ , write
4+ , writeDocs
5+ )
6+ where
7+
8+
9+import Control.Concurrent (forkIO)
10+import Control.Concurrent.MVar (newEmptyMVar, putMVar, readMVar)
11+import Control.Monad (foldM, void)
12+import Control.Monad.Except (liftIO)
13+import qualified Data.Binary as Binary
14+import qualified Data.List.NonEmpty as NonEmpty
15+import Data.List.NonEmpty (NonEmpty((:|)))
16+import Data.Map (Map)
17+import qualified Data.Map as Map
18+import qualified Data.Maybe as Maybe
19+import qualified Data.Text.Encoding as Text
20+
21+import qualified Elm.Compiler as Compiler
22+import qualified Elm.Compiler.Module as Module
23+import qualified Elm.Docs as Docs
24+import qualified Json.Encode as Encode
25+
26+import File.Compile (Answer(..))
27+import qualified Reporting.Error.Compile as E
28+import qualified Reporting.Error as Error
29+import qualified Reporting.Task as Task
30+import qualified Stuff.Paths as Path
31+
32+
33+
34+-- IGNORE
35+
36+
37+ignore :: Map Module.Raw Answer -> Task.Task (Map Module.Raw Compiler.Artifacts)
38+ignore answers =
39+ let
40+ ignorer _name result =
41+ return result
42+ in
43+ gather ignorer answers
44+
45+
46+
47+-- WRITE
48+
49+
50+write :: FilePath -> Map Module.Raw Answer -> Task.Task (Map Module.Raw Compiler.Artifacts)
51+write root answers =
52+ let
53+ writer name result@(Compiler.Artifacts elmi elmo _) =
54+ do mvar <- newEmptyMVar
55+ void $ forkIO $
56+ do Binary.encodeFile (Path.elmi root name) elmi
57+ Binary.encodeFile (Path.elmo root name) elmo
58+ putMVar mvar result
59+ return mvar
60+ in
61+ do mvars <- gather writer answers
62+ liftIO $ traverse readMVar mvars
63+
64+
65+writeDocs :: FilePath -> Map Module.Raw Compiler.Artifacts -> Task.Task Docs.Documentation
66+writeDocs path results =
67+ let
68+ getDocs (Compiler.Artifacts _ _ docs) =
69+ docs
70+ in
71+ case Maybe.mapMaybe getDocs (Map.elems results) of
72+ [] ->
73+ return Map.empty
74+
75+ docs ->
76+ do liftIO $ Encode.writeUgly path $ Encode.list Docs.encode docs
77+ return $ Docs.toDict docs
78+
79+
80+
81+-- GATHER
82+
83+
84+gather :: OnGood a -> Map Module.Raw Answer -> Task.Task (Map Module.Raw a)
85+gather onGood answers =
86+ do summary <- liftIO $
87+ foldM (gatherHelp onGood) (Right Map.empty) (Map.toList answers)
88+
89+ case summary of
90+ Left (err :| errors) ->
91+ Task.throw (Error.Compile err errors)
92+
93+ Right results ->
94+ return results
95+
96+
97+type OnGood a = Module.Raw -> Compiler.Artifacts -> IO a
98+
99+
100+gatherHelp :: OnGood a -> Summary a -> (Module.Raw, Answer) -> IO (Summary a)
101+gatherHelp onGood summary (name, answer) =
102+ case answer of
103+ Blocked ->
104+ return summary
105+
106+ Bad path time src errors ->
107+ do let err = E.Error name path time (Text.decodeUtf8 src) errors
108+ return (addErr err summary)
109+
110+ Good result ->
111+ do value <- onGood name result
112+ return (addOk name value summary)
113+
114+
115+
116+-- DICT RESULT
117+
118+
119+type Summary a =
120+ Either (NonEmpty E.Error) (Map Module.Raw a)
121+
122+
123+addOk :: Module.Raw -> a -> Summary a -> Summary a
124+addOk name result acc =
125+ case acc of
126+ Left _ ->
127+ acc
128+
129+ Right results ->
130+ Right (Map.insert name result results)
131+
132+
133+addErr :: E.Error -> Summary a -> Summary a
134+addErr err acc =
135+ case acc of
136+ Left errors ->
137+ Left (NonEmpty.cons err errors)
138+
139+ Right _ ->
140+ Left (err :| [])
builder/src/File/Compile.hsnew file+159
@@ -0,0 +1,159 @@
1+module File.Compile
2+ ( compile
3+ , Answer(..)
4+ )
5+ where
6+
7+import Control.Concurrent (forkIO)
8+import Control.Concurrent.MVar (MVar, newEmptyMVar, newMVar, putMVar, readMVar, takeMVar)
9+import Control.Monad (void)
10+import Control.Monad.Except (liftIO)
11+import qualified Data.ByteString as BS
12+import qualified Data.Map as Map
13+import qualified Data.Time.Clock as Time
14+
15+import qualified Elm.Compiler as Compiler
16+import qualified Elm.Compiler.Module as Module
17+
18+import Elm.Project.Json (Project)
19+import qualified Elm.Project.Json as Project
20+import qualified File.Plan as Plan
21+import qualified Reporting.Progress as Progress
22+import qualified Reporting.Task as Task
23+
24+
25+
26+-- COMPILE
27+
28+
29+compile :: Project -> Module.Interfaces -> Dict Plan.Info -> Task.Task (Dict Answer)
30+compile project ifaces modules =
31+ do Task.report (Progress.CompileStart (Map.size modules))
32+
33+ tell <- Task.getReporter
34+
35+ answers <- liftIO $
36+ do mvar <- newEmptyMVar
37+ iMVar <- newMVar ifaces
38+ answerMVars <- Map.traverseWithKey (compileModule tell project mvar iMVar) modules
39+ putMVar mvar answerMVars
40+ traverse readMVar answerMVars
41+
42+ Task.report Progress.CompileEnd
43+
44+ return answers
45+
46+
47+
48+-- ANSWERS
49+
50+
51+data Answer
52+ = Blocked
53+ | Bad FilePath Time.UTCTime BS.ByteString [Compiler.Error]
54+ | Good Compiler.Artifacts
55+
56+
57+type Dict a = Map.Map Module.Raw a
58+
59+
60+
61+-- COMPILE MODULE
62+
63+
64+compileModule
65+ :: (Progress.Progress -> IO ())
66+ -> Project
67+ -> MVar (Dict (MVar Answer))
68+ -> MVar Module.Interfaces
69+ -> Module.Raw
70+ -> Plan.Info
71+ -> IO (MVar Answer)
72+compileModule tell project answersMVar ifacesMVar name info =
73+ do mvar <- newEmptyMVar
74+
75+ void $ forkIO $
76+ do answers <- readMVar answersMVar
77+ blocked <- isBlocked answers info
78+ if blocked
79+ then putMVar mvar Blocked
80+ else
81+ do tell (Progress.CompileFileStart name)
82+ let pkg = Project.getName project
83+ let isExposed = Project.isPackageRoot name project
84+ let imports = makeImports project info
85+ ifaces <- readMVar ifacesMVar
86+ let context = Compiler.Context pkg isExposed imports ifaces
87+ let source = Plan._src info
88+ case Compiler.compile context source of
89+ (_warnings, Left errors) ->
90+ do tell (Progress.CompileFileEnd name Progress.Bad)
91+ let path = Plan._path info
92+ let time = Plan._time info
93+ putMVar mvar (Bad path time source errors)
94+
95+ (_warnings, Right result@(Compiler.Artifacts elmi _ _)) ->
96+ do tell (Progress.CompileFileEnd name Progress.Good)
97+ let canonicalName = Module.Canonical pkg name
98+ lock <- takeMVar ifacesMVar
99+ putMVar ifacesMVar (Map.insert canonicalName elmi lock)
100+ putMVar mvar (Good result)
101+
102+ return mvar
103+
104+
105+
106+-- IMPORTS
107+
108+
109+makeImports :: Project -> Plan.Info -> Dict Module.Canonical
110+makeImports project (Plan.Info _ _ _ clean dirty foreign) =
111+ let
112+ pkgName =
113+ Project.getName project
114+
115+ mkLocal name =
116+ ( name, Module.Canonical pkgName name )
117+
118+ mkForeign canonicalName@(Module.Canonical _ name) =
119+ ( name, canonicalName )
120+ in
121+ Map.fromList $
122+ map mkLocal clean
123+ ++ map mkLocal dirty
124+ ++ map mkForeign foreign
125+
126+
127+
128+-- INTERFACES
129+
130+
131+isBlocked :: Dict (MVar Answer) -> Plan.Info -> IO Bool
132+isBlocked answers info =
133+ anyBlock <$> traverse (get answers) (Plan._dirty info)
134+
135+
136+get :: Dict (MVar Answer) -> Module.Raw -> IO Answer
137+get names name =
138+ case Map.lookup name names of
139+ Nothing ->
140+ error "bug manifesting in File.Complie.get, please report at <TODO>!"
141+
142+ Just mvar ->
143+ readMVar mvar
144+
145+
146+anyBlock :: [Answer] -> Bool
147+anyBlock answers =
148+ case answers of
149+ [] ->
150+ False
151+
152+ Blocked : _ ->
153+ True
154+
155+ Bad _ _ _ _ : _ ->
156+ True
157+
158+ Good _ : otherAnswers ->
159+ anyBlock otherAnswers
builder/src/File/Crawl.hsnew file+294
@@ -0,0 +1,294 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module File.Crawl
4+ ( Result
5+ , Graph(..)
6+ , crawl
7+ , crawlFromSource
8+ )
9+ where
10+
11+import Control.Concurrent.Chan (Chan, newChan, readChan)
12+import Control.Monad (foldM)
13+import Control.Monad.Except (liftIO)
14+import qualified Data.ByteString as BS
15+import qualified Data.Graph as Graph
16+import qualified Data.List.NonEmpty as NonEmpty
17+import Data.List.NonEmpty (NonEmpty((:|)))
18+import qualified Data.Map as Map
19+import qualified Data.Set as Set
20+
21+import qualified Elm.Compiler.Module as Module
22+import qualified Elm.Compiler.Objects as Obj
23+import qualified Elm.Kernel as Kernel
24+import qualified Elm.Package as Pkg
25+
26+import qualified Elm.Project.Json as Project
27+import Elm.Project.Summary (Summary(..))
28+import qualified File.Args as Args
29+import qualified File.Find as Find
30+import qualified File.Header as Header
31+import qualified File.IO as IO
32+import qualified Reporting.Error as Error
33+import qualified Reporting.Error.Crawl as E
34+import qualified Reporting.Task as Task
35+
36+
37+
38+-- GRAPH
39+
40+
41+data Graph kernel problems =
42+ Graph
43+ { _args :: Args.Args Module.Raw
44+ , _locals :: Map.Map Module.Raw Header.Info
45+ , _kernels :: Map.Map Module.Raw kernel
46+ , _foreigns :: Map.Map Module.Raw Pkg.Package
47+ , _problems :: problems
48+ }
49+
50+
51+type Result =
52+ Graph Obj.Kernel ()
53+
54+
55+type WorkGraph =
56+ Graph (FilePath, Maybe FilePath) [E.Problem]
57+
58+
59+initWorkGraph :: Args.Args Module.Raw -> Map.Map Module.Raw Header.Info -> WorkGraph
60+initWorkGraph args locals =
61+ Graph args locals Map.empty Map.empty []
62+
63+
64+
65+-- CRAWL
66+
67+
68+crawl :: Summary -> Args.Args FilePath -> Task.Task Result
69+crawl summary args =
70+ case args of
71+ Args.Pkg names ->
72+ do let roots = map (Unvisited E.ElmJson) names
73+ let graph = initWorkGraph (Args.Pkg names) Map.empty
74+ depthFirstSearch summary roots graph
75+
76+ Args.Roots (path :| []) ->
77+ crawlHelp summary =<< Header.readOneFile summary path
78+
79+ Args.Roots paths ->
80+ do headers <- Header.readManyFiles summary paths
81+ let names = NonEmpty.map fst headers
82+ let unvisited = concatMap toUnvisited (NonEmpty.toList headers)
83+ let graph = initWorkGraph (Args.Roots names) (Map.fromList (NonEmpty.toList headers))
84+ depthFirstSearch summary unvisited graph
85+ where
86+ toUnvisited (name, Header.Info path _ _ deps) =
87+ map (Unvisited (E.Module path name)) deps
88+
89+
90+crawlFromSource :: Summary -> BS.ByteString -> Task.Task Result
91+crawlFromSource summary@(Summary _ project _ _ _) source =
92+ crawlHelp summary =<<
93+ Header.readSource project source
94+
95+
96+crawlHelp :: Summary -> ( Maybe Module.Raw, Header.Info ) -> Task.Task Result
97+crawlHelp summary ( maybeName, info@(Header.Info path _ _ deps) ) =
98+ do let name = maybe "Main" id maybeName
99+ let args = Args.Roots (name :| [])
100+ let toUnvisited dep = Unvisited (maybe (E.File path) (E.Module path) maybeName) dep
101+ let roots = map toUnvisited deps
102+ let graph = initWorkGraph args (Map.singleton name info)
103+ depthFirstSearch summary roots graph
104+
105+
106+
107+-- DEPTH FIRST SEARCH
108+
109+
110+depthFirstSearch :: Summary -> [Unvisited] -> WorkGraph -> Task.Task Result
111+depthFirstSearch summary unvisited startGraph =
112+ do chan <- liftIO newChan
113+
114+ (Graph args locals kernelPaths foreigns problems) <-
115+ dfs summary chan 0 Set.empty unvisited startGraph
116+
117+ case problems of
118+ [] ->
119+ do checkForCycles locals
120+ kernels <- visitKernels summary locals kernelPaths
121+ return $ Graph args locals kernels foreigns ()
122+
123+ problem : otherProblems ->
124+ Task.throw (Error.Crawl (E.DependencyProblems problem otherProblems))
125+
126+
127+
128+-- CHECK FOR CYCLES
129+
130+
131+checkForCycles :: Map.Map Module.Raw Header.Info -> Task.Task ()
132+checkForCycles locals =
133+ let
134+ toNode (name, Header.Info _ _ _ imports) =
135+ (name, name, imports)
136+
137+ components =
138+ Graph.stronglyConnComp (map toNode (Map.toList locals))
139+ in
140+ mapM_ checkComponent components
141+
142+
143+checkComponent :: Graph.SCC Module.Raw -> Task.Task ()
144+checkComponent scc =
145+ case scc of
146+ Graph.AcyclicSCC _ ->
147+ return ()
148+
149+ Graph.CyclicSCC names ->
150+ Task.throw (Error.Cycle names)
151+
152+
153+
154+-- CONCURRENT DEPTH FIRST SEARCH
155+
156+
157+dfs
158+ :: Summary
159+ -> Chan (Either E.Problem Asset)
160+ -> Int
161+ -> Set.Set Module.Raw
162+ -> [Unvisited]
163+ -> WorkGraph
164+ -> Task.Task WorkGraph
165+dfs summary chan oldPending oldSeen unvisited graph =
166+ do (seen, pending) <-
167+ foldM (spawnVisitor summary chan) (oldSeen, oldPending) unvisited
168+
169+ if pending == 0
170+ then return graph
171+ else
172+ do asset <- liftIO $ readChan chan
173+ case asset of
174+ Right (Local name info@(Header.Info path _ _ imports)) ->
175+ do let newGraph = graph { _locals = Map.insert name info (_locals graph) }
176+ let deps = map (Unvisited (E.Module path name)) imports
177+ dfs summary chan (pending - 1) seen deps newGraph
178+
179+ Right (Kernel name info) ->
180+ do let kernels = Map.insert name info (_kernels graph)
181+ let newGraph = graph { _kernels = kernels }
182+ dfs summary chan (pending - 1) seen [] newGraph
183+
184+ Right (Foreign name pkg) ->
185+ do let foreigns = Map.insert name pkg (_foreigns graph)
186+ let newGraph = graph { _foreigns = foreigns }
187+ dfs summary chan (pending - 1) seen [] newGraph
188+
189+ Left problem ->
190+ do let problems = problem : _problems graph
191+ let newGraph = graph { _problems = problems }
192+ dfs summary chan (pending - 1) seen [] newGraph
193+
194+
195+spawnVisitor
196+ :: Summary
197+ -> Chan (Either E.Problem Asset)
198+ -> (Set.Set Module.Raw, Int)
199+ -> Unvisited
200+ -> Task.Task (Set.Set Module.Raw, Int)
201+spawnVisitor summary chan (seen, n) unvisited@(Unvisited _ name) =
202+ if Set.member name seen then
203+ return (seen, n)
204+ else
205+ do Task.workerChan chan (toVisitor summary unvisited)
206+ return (Set.insert name seen, n + 1)
207+
208+
209+
210+-- VISITOR
211+
212+
213+data Unvisited =
214+ Unvisited
215+ { _origin :: E.Origin
216+ , _name :: Module.Raw
217+ }
218+
219+
220+data Asset
221+ = Local Module.Raw Header.Info
222+ | Kernel Module.Raw (FilePath, Maybe FilePath)
223+ | Foreign Module.Raw Pkg.Package
224+
225+
226+toVisitor :: Summary -> Unvisited -> Task.Task_ E.Problem Asset
227+toVisitor summary (Unvisited origin name) =
228+ do asset <- Find.find summary origin name
229+ case asset of
230+ Find.Local path ->
231+ uncurry Local <$> Header.readModule summary name path
232+
233+ Find.Foreign pkg ->
234+ return $ Foreign name pkg
235+
236+ Find.Kernel clientPath maybeServerPath ->
237+ return $ Kernel name (clientPath, maybeServerPath)
238+
239+
240+
241+-- VISIT KERNELS
242+
243+
244+visitKernels
245+ :: Summary
246+ -> Map.Map Module.Raw Header.Info
247+ -> Map.Map Module.Raw (FilePath, Maybe FilePath)
248+ -> Task.Task (Map.Map Module.Raw Obj.Kernel)
249+visitKernels summary locals kernelPaths =
250+ if Map.null kernelPaths then
251+ return Map.empty
252+ else
253+ -- TODO do this concurrently?
254+ -- Need better concurrency primitives in Task
255+ traverse (readKernel (toImportDict summary locals)) kernelPaths
256+
257+
258+readKernel :: ImportDict -> (FilePath, Maybe FilePath) -> Task.Task Obj.Kernel
259+readKernel importDict (clientPath, maybeServerPath) =
260+ Obj.Kernel
261+ <$> readKContent importDict clientPath
262+ <*> traverse (readKContent importDict) maybeServerPath
263+
264+
265+readKContent :: ImportDict -> FilePath -> Task.Task Kernel.KContent
266+readKContent importDict path =
267+ do source <- liftIO $ IO.readUtf8 path
268+ case Kernel.parse importDict source of
269+ Right content ->
270+ return content
271+
272+ Left _ ->
273+ Task.throw (Error.Crawl (E.BadKernelHeader path))
274+
275+
276+
277+-- IMPORT DICT
278+
279+
280+type ImportDict =
281+ Map.Map Module.Raw [Pkg.Name]
282+
283+
284+toImportDict :: Summary -> Map.Map Module.Raw info -> ImportDict
285+toImportDict (Summary _ project exposed _ _) locals =
286+ let
287+ localPkg =
288+ [Project.getName project]
289+
290+ addLocal name _ dict =
291+ Map.insertWith (++) name localPkg dict
292+ in
293+ Map.foldrWithKey addLocal (Map.map (map Pkg._name) exposed) locals
294+
builder/src/File/Find.hsnew file+99
@@ -0,0 +1,99 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module File.Find
4+ ( Asset(..)
5+ , find
6+ )
7+ where
8+
9+import Control.Monad.Except (liftIO)
10+import qualified Data.Map as Map
11+import qualified Data.Maybe as Maybe
12+import qualified System.Directory as Dir
13+import System.FilePath ((</>), (<.>))
14+
15+import qualified Elm.Compiler.Module as Module
16+import qualified Elm.Name as N
17+import qualified Elm.Package as Pkg
18+
19+import qualified Elm.Project.Json as Project
20+import qualified Elm.Project.Summary as Summary
21+import qualified Reporting.Error.Crawl as E
22+import qualified Reporting.Task as Task
23+
24+
25+
26+-- ASSET
27+
28+
29+data Asset
30+ = Local FilePath
31+ | Kernel FilePath (Maybe FilePath)
32+ | Foreign Pkg.Package
33+
34+
35+
36+-- FIND
37+
38+
39+find :: Summary.Summary -> E.Origin -> Module.Raw -> Task.Task_ E.Problem Asset
40+find (Summary.Summary root project exposed _ _) origin name =
41+ case project of
42+ Project.App info ->
43+ do let srcDirs = map (root </>) (Project._app_source_dirs info)
44+ findElm srcDirs exposed origin name
45+
46+ Project.Pkg _ ->
47+ if N.startsWith "Elm.Kernel." name then
48+ if Project.isPlatformPackage project then
49+ findKernel (root </> "src") origin name
50+ else
51+ Task.throw $ E.ModuleNameReservedForKernel origin name
52+
53+ else
54+ findElm [ root </> "src" ] exposed origin name
55+
56+
57+
58+-- FIND ELM
59+
60+
61+findElm :: [FilePath] -> Summary.ExposedModules -> E.Origin -> Module.Raw -> Task.Task_ E.Problem Asset
62+findElm srcDirs exposed origin name =
63+ do
64+ paths <- liftIO $ Maybe.catMaybes <$> mapM (elmExists name) srcDirs
65+
66+ case (paths, Map.lookup name exposed) of
67+ ([path], Nothing) ->
68+ return (Local path)
69+
70+ ([], Just [pkg]) ->
71+ return (Foreign pkg)
72+
73+ ([], Nothing) ->
74+ Task.throw $ E.ModuleNotFound origin name
75+
76+ (_, maybePkgs) ->
77+ Task.throw $ E.ModuleAmbiguous origin name paths (maybe [] id maybePkgs)
78+
79+
80+elmExists :: Module.Raw -> FilePath -> IO (Maybe FilePath)
81+elmExists name srcDir =
82+ do let path = srcDir </> Module.nameToSlashPath name <.> "elm"
83+ exists <- Dir.doesFileExist path
84+ return $ if exists then Just path else Nothing
85+
86+
87+
88+-- FIND KERNEL
89+
90+
91+findKernel :: FilePath -> E.Origin -> Module.Raw -> Task.Task_ E.Problem Asset
92+findKernel srcDir origin name =
93+ do let clientPath = srcDir </> Module.nameToSlashPath name <.> "js"
94+ let serverPath = srcDir </> Module.nameToSlashPath name <.> "server.js"
95+ client <- liftIO $ Dir.doesFileExist clientPath
96+ server <- liftIO $ Dir.doesFileExist serverPath
97+ if client
98+ then return $ Kernel clientPath (if server then Just serverPath else Nothing)
99+ else Task.throw $ E.ModuleNotFound origin name
builder/src/File/Hash.hsnew file+110
@@ -0,0 +1,110 @@
1+{-# OPTIONS_GHC -Wall #-}
2+module File.Hash
3+ ( Hasher
4+ , put
5+ , Digest
6+ , toString
7+ , putByteString
8+ , putBuilder
9+ , putFile
10+ )
11+ where
12+
13+
14+import Prelude hiding (appendFile)
15+import Control.Monad (foldM)
16+import qualified Control.Monad.State as State
17+import qualified Data.Binary.Get as Binary
18+import qualified Data.ByteString as BS
19+import qualified Data.ByteString.Builder as BS
20+import qualified Data.ByteString.Builder.Extra as BS (defaultChunkSize)
21+import qualified Data.ByteString.Lazy as LBS
22+import qualified Data.Digest.Pure.SHA as SHA
23+import qualified System.IO as IO
24+
25+
26+
27+-- HASHER
28+
29+
30+type Hasher = State.StateT State IO ()
31+
32+
33+type Digest = SHA.Digest SHA.SHA1State
34+
35+
36+put :: FilePath -> Hasher -> IO Digest
37+put path hasher =
38+ IO.withBinaryFile path IO.WriteMode $ \handle ->
39+ do let state = State handle 0 SHA.sha1Incremental
40+ (State _ len decoder) <- State.execStateT hasher state
41+ return (SHA.completeSha1Incremental decoder len)
42+
43+
44+toString :: Digest -> String
45+toString =
46+ SHA.showDigest
47+
48+
49+
50+-- HASH STATE
51+
52+
53+data State =
54+ State
55+ { _handle :: !IO.Handle
56+ , _length :: !Int
57+ , _decoder :: !(Binary.Decoder SHA.SHA1State)
58+ }
59+
60+
61+
62+-- PUBLIC API
63+
64+
65+putByteString :: BS.ByteString -> Hasher
66+putByteString chunk =
67+ do state <- State.get
68+ State.put =<< State.liftIO (appendByteString state chunk)
69+
70+
71+putBuilder :: BS.Builder -> Hasher
72+putBuilder builder =
73+ do state <- State.get
74+ State.put =<< State.liftIO (appendBuilder builder state)
75+
76+
77+putFile :: FilePath -> Hasher
78+putFile path =
79+ do state <- State.get
80+ State.put =<< State.liftIO (appendFile path state)
81+
82+
83+
84+-- PRIMITIVES
85+
86+
87+appendByteString :: State -> BS.ByteString -> IO State
88+appendByteString (State handle len decoder) chunk =
89+ do BS.hPut handle chunk
90+ return $ State handle (len + BS.length chunk) (Binary.pushChunk decoder chunk)
91+
92+
93+appendBuilder :: BS.Builder -> State -> IO State
94+appendBuilder builder state =
95+ foldM appendByteString state $
96+ LBS.toChunks (BS.toLazyByteString builder)
97+
98+
99+appendFile :: FilePath -> State -> IO State
100+appendFile path state =
101+ IO.withBinaryFile path IO.ReadMode $ \handle ->
102+ appendHelp handle state
103+
104+
105+appendHelp :: IO.Handle -> State -> IO State
106+appendHelp handle state =
107+ do chunk <- BS.hGet handle BS.defaultChunkSize
108+ if BS.null chunk
109+ then return state
110+ else appendHelp handle =<< appendByteString state chunk
builder/src/File/Header.hsnew file+190
@@ -0,0 +1,190 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module File.Header
4+ ( Info(..)
5+ , readModule
6+ , readOneFile
7+ , readManyFiles
8+ , readSource
9+ )
10+ where
11+
12+import Control.Monad.Except (liftIO)
13+import qualified Data.ByteString as BS
14+import Data.List.NonEmpty (NonEmpty((:|)))
15+import qualified Data.Map as Map
16+import Data.Semigroup ((<>))
17+import qualified Data.Time.Calendar as Day
18+import qualified Data.Time.Clock as Time
19+import qualified System.Directory as Dir
20+
21+import qualified Elm.Compiler.Module as Module
22+import qualified Elm.Header as Header
23+
24+import qualified Elm.Project.Json as Project
25+import Elm.Project.Json (Project)
26+import Elm.Project.Summary (Summary(..))
27+import qualified File.IO as IO
28+import qualified Reporting.Error as Error
29+import qualified Reporting.Error.Crawl as E
30+import qualified Reporting.Task as Task
31+
32+
33+
34+-- INFO
35+
36+
37+data Info =
38+ Info
39+ { _path :: FilePath
40+ , _time :: Time.UTCTime
41+ , _source :: BS.ByteString
42+ , _imports :: [Module.Raw]
43+ }
44+
45+
46+atRoot :: Task.Task_ E.Problem a -> Task.Task_ E.Error a
47+atRoot task =
48+ Task.mapError (\problem -> E.DependencyProblems problem []) task
49+
50+
51+-- READ MODULE
52+
53+
54+readModule :: Summary -> Module.Raw -> FilePath -> Task.Task_ E.Problem (Module.Raw, Info)
55+readModule summary expectedName path =
56+ do time <- liftIO $ Dir.getModificationTime path
57+ source <- liftIO $ IO.readUtf8 path
58+ (maybeName, info) <- parse (_project summary) path time source
59+ name <- checkName path expectedName maybeName
60+ return (name, info)
61+
62+
63+checkName :: FilePath -> Module.Raw -> Maybe Module.Raw -> Task.Task_ E.Problem Module.Raw
64+checkName path expectedName maybeName =
65+ case maybeName of
66+ Nothing ->
67+ Task.throw (E.ModuleNameMissing path expectedName)
68+
69+ Just actualName ->
70+ if expectedName == actualName
71+ then return expectedName
72+ else Task.throw (E.ModuleNameMismatch path expectedName actualName)
73+
74+
75+
76+-- READ ONE FILE
77+
78+
79+readOneFile :: Summary -> FilePath -> Task.Task (Maybe Module.Raw, Info)
80+readOneFile summary path =
81+ Task.mapError Error.Crawl $
82+ do (time, source) <- readOneHelp path
83+ atRoot $ parse (_project summary) path time source
84+
85+
86+readOneHelp :: FilePath -> Task.Task_ E.Error (Time.UTCTime, BS.ByteString)
87+readOneHelp path =
88+ do exists <- IO.exists path
89+ if exists
90+ then liftIO $ (,) <$> Dir.getModificationTime path <*> IO.readUtf8 path
91+ else Task.throw $ E.RootFileNotFound path
92+
93+
94+
95+-- READ MANY FILES
96+
97+
98+readManyFiles :: Summary -> NonEmpty FilePath -> Task.Task (NonEmpty (Module.Raw, Info))
99+readManyFiles summary files =
100+ Task.mapError Error.Crawl $
101+ do infos <- traverse (readManyFilesHelp summary) files
102+ let insert (name, info) dict = Map.insertWith (<>) name (info :| []) dict
103+ let nameTable = foldr insert Map.empty infos
104+ _ <- Map.traverseWithKey detectDuplicateNames nameTable
105+ return infos
106+
107+
108+readManyFilesHelp :: Summary -> FilePath -> Task.Task_ E.Error (Module.Raw, Info)
109+readManyFilesHelp summary path =
110+ do (time, source) <- readOneHelp path
111+ (maybeName, info) <- atRoot $ parse (_project summary) path time source
112+ case maybeName of
113+ Nothing ->
114+ Task.throw (E.RootNameless path)
115+
116+ Just name ->
117+ return (name, info)
118+
119+
120+detectDuplicateNames :: Module.Raw -> NonEmpty Info -> Task.Task_ E.Error ()
121+detectDuplicateNames name (info :| otherInfos) =
122+ case otherInfos of
123+ [] ->
124+ return ()
125+
126+ _ ->
127+ Task.throw (E.RootModuleNameDuplicate name (map _path (info : otherInfos)))
128+
129+
130+
131+
132+-- READ SOURCE
133+
134+
135+readSource :: Project -> BS.ByteString -> Task.Task (Maybe Module.Raw, Info)
136+readSource project source =
137+ Task.mapError Error.Crawl $
138+ atRoot $ parse project "elm" fakeTime source
139+
140+
141+fakeTime :: Time.UTCTime
142+fakeTime =
143+ Time.UTCTime (Day.fromGregorian 3000 1 1) 0
144+
145+
146+
147+-- PARSE HEADER
148+
149+
150+parse :: Project -> FilePath -> Time.UTCTime -> BS.ByteString -> Task.Task_ E.Problem (Maybe Module.Raw, Info)
151+parse project path time source =
152+ -- TODO get regions on data extracted here
153+ case Header.parse (Project.getName project) source of
154+ Right (maybeDecl, deps) ->
155+ do maybeName <- checkTag project path maybeDecl
156+ return ( maybeName, Info path time source deps )
157+
158+ Left msg ->
159+ Task.throw (E.BadHeader path source msg)
160+
161+
162+checkTag :: Project -> FilePath -> Maybe (Header.Tag, Module.Raw) -> Task.Task_ E.Problem (Maybe Module.Raw)
163+checkTag project path maybeDecl =
164+ case maybeDecl of
165+ Nothing ->
166+ return Nothing
167+
168+ Just (tag, name) ->
169+ let
170+ success =
171+ return (Just name)
172+ in
173+ case tag of
174+ Header.Normal ->
175+ success
176+
177+ Header.Port ->
178+ case project of
179+ Project.App _ ->
180+ success
181+
182+ Project.Pkg _ ->
183+ Task.throw (E.PortsInPackage path name)
184+
185+ Header.Effect ->
186+ if Project.isPlatformPackage project then
187+ success
188+
189+ else
190+ Task.throw (E.EffectsUnexpected path name)
builder/src/File/IO.hsnew file+249
@@ -0,0 +1,249 @@
1+module File.IO
2+ ( writeBinary, readBinary
3+ , writeUtf8, readUtf8
4+ , writeBuilder
5+ , Writer
6+ , put
7+ , putByteString
8+ , putBuilder
9+ , putFile
10+ , exists
11+ , remove, removeDir
12+ , find
13+ , andM
14+ )
15+ where
16+
17+
18+import Control.Exception (catch)
19+import Control.Monad.Trans (liftIO)
20+import qualified Data.Binary as Binary
21+import qualified Data.ByteString as BS
22+import qualified Data.ByteString.Internal as BS
23+import qualified Data.ByteString.Builder as B
24+import qualified Data.ByteString.Builder.Extra as B (defaultChunkSize)
25+import qualified Foreign.ForeignPtr as FPtr
26+import GHC.IO.Exception (IOException, IOErrorType(InvalidArgument))
27+import qualified System.Directory as Dir
28+import System.FilePath ((</>))
29+import qualified System.FilePath as FP
30+import qualified System.IO as IO
31+import System.IO.Error (ioeGetErrorType, annotateIOError, modifyIOError)
32+
33+import qualified Reporting.Error as Error
34+import qualified Reporting.Error.Assets as E
35+import qualified Reporting.Task as Task
36+
37+
38+
39+-- BINARY
40+
41+
42+writeBinary :: (Binary.Binary a) => FilePath -> a -> Task.Task ()
43+writeBinary path value =
44+ liftIO $
45+ do let dir = FP.dropFileName path
46+ Dir.createDirectoryIfMissing True dir
47+ Binary.encodeFile path value
48+
49+
50+readBinary :: (Binary.Binary a) => FilePath -> Task.Task a
51+readBinary path =
52+ do exists_ <- liftIO (Dir.doesFileExist path)
53+ if not exists_
54+ then throwCorruptBinary path
55+ else
56+ do result <- liftIO (Binary.decodeFileOrFail path)
57+ case result of
58+ Left _ ->
59+ throwCorruptBinary path
60+
61+ Right value ->
62+ return value
63+
64+
65+throwCorruptBinary :: FilePath -> Task.Task a
66+throwCorruptBinary filePath =
67+ Task.throw (Error.Assets (E.CorruptBinary filePath))
68+
69+
70+
71+-- WRITE UTF-8
72+
73+
74+writeUtf8 :: FilePath -> BS.ByteString -> IO ()
75+writeUtf8 filePath content =
76+ withUtf8 filePath IO.WriteMode $ \handle ->
77+ BS.hPut handle content
78+
79+
80+withUtf8 :: FilePath -> IO.IOMode -> (IO.Handle -> IO a) -> IO a
81+withUtf8 filePath mode callback =
82+ IO.withFile filePath mode $ \handle ->
83+ do IO.hSetEncoding handle IO.utf8
84+ callback handle
85+
86+
87+
88+-- READ UTF-8
89+
90+
91+readUtf8 :: FilePath -> IO BS.ByteString
92+readUtf8 filePath =
93+ withUtf8 filePath IO.ReadMode $ \handle ->
94+ modifyIOError (encodingError filePath) $
95+ do fileSize <- catch (IO.hFileSize handle) useZeroIfNotRegularFile
96+ let readSize = max 0 (fromIntegral fileSize) + 1
97+ hGetContentsSizeHint handle readSize (max 255 readSize)
98+
99+
100+useZeroIfNotRegularFile :: IOException -> IO Integer
101+useZeroIfNotRegularFile _ =
102+ return 0
103+
104+
105+hGetContentsSizeHint :: IO.Handle -> Int -> Int -> IO BS.ByteString
106+hGetContentsSizeHint handle =
107+ readChunks []
108+ where
109+ readChunks chunks readSize incrementSize =
110+ do fp <- BS.mallocByteString readSize
111+ readCount <- FPtr.withForeignPtr fp $ \buf -> IO.hGetBuf handle buf readSize
112+ let chunk = BS.PS fp 0 readCount
113+ if readCount < readSize && readSize > 0
114+ then return $! BS.concat (reverse (chunk:chunks))
115+ else readChunks (chunk:chunks) incrementSize (min 32752 (readSize + incrementSize))
116+
117+
118+encodingError :: FilePath -> IOError -> IOError
119+encodingError filePath ioErr =
120+ case ioeGetErrorType ioErr of
121+ InvalidArgument ->
122+ annotateIOError
123+ (userError "Bad encoding; the file must be valid UTF-8")
124+ ""
125+ Nothing
126+ (Just filePath)
127+
128+ _ ->
129+ ioErr
130+
131+
132+
133+-- WRITE BUILDER
134+
135+
136+writeBuilder :: FilePath -> B.Builder -> IO ()
137+writeBuilder path builder =
138+ IO.withBinaryFile path IO.WriteMode $ \handle ->
139+ do IO.hSetBuffering handle (IO.BlockBuffering Nothing)
140+ B.hPutBuilder handle builder
141+
142+
143+
144+-- WRITER
145+
146+
147+newtype Writer =
148+ Writer (IO.Handle -> IO ())
149+
150+
151+put :: FilePath -> Writer -> IO ()
152+put path (Writer callback) =
153+ IO.withBinaryFile path IO.WriteMode callback
154+
155+
156+putByteString :: BS.ByteString -> Writer
157+putByteString chunk =
158+ Writer $ \handle ->
159+ BS.hPut handle chunk
160+
161+
162+putBuilder :: B.Builder -> Writer
163+putBuilder builder =
164+ Writer $ \handle ->
165+ B.hPutBuilder handle builder
166+
167+
168+putFile :: FilePath -> Writer
169+putFile path =
170+ Writer $ \sink ->
171+ IO.withBinaryFile path IO.ReadMode $ \source ->
172+ putHelp source sink
173+
174+
175+putHelp :: IO.Handle -> IO.Handle -> IO ()
176+putHelp source sink =
177+ do chunk <- BS.hGet source B.defaultChunkSize
178+ if BS.null chunk
179+ then return ()
180+ else
181+ do BS.hPut sink chunk
182+ putHelp source sink
183+
184+
185+
186+-- EXISTS
187+
188+
189+exists :: FilePath -> Task.Task_ e Bool
190+exists filePath =
191+ liftIO $ Dir.doesFileExist filePath
192+
193+
194+
195+-- REMOVE FILES
196+
197+
198+remove :: FilePath -> Task.Task_ e ()
199+remove filePath =
200+ liftIO $
201+ do exists_ <- Dir.doesFileExist filePath
202+ if exists_
203+ then Dir.removeFile filePath
204+ else return ()
205+
206+
207+removeDir :: FilePath -> IO ()
208+removeDir path =
209+ do exists_ <- Dir.doesDirectoryExist path
210+ if exists_
211+ then Dir.removeDirectoryRecursive path
212+ else return ()
213+
214+
215+
216+-- FIND FILES
217+
218+
219+find :: FilePath -> IO (Maybe FilePath)
220+find name =
221+ do subDir <- Dir.getCurrentDirectory
222+ findHelp name (FP.splitDirectories subDir)
223+
224+
225+findHelp :: FilePath -> [String] -> IO (Maybe FilePath)
226+findHelp name dirs =
227+ if null dirs then
228+ return Nothing
229+
230+ else
231+ do exists_ <- Dir.doesFileExist (FP.joinPath dirs </> name)
232+ if exists_
233+ then return (Just (FP.joinPath dirs))
234+ else findHelp name (init dirs)
235+
236+
237+
238+-- HELPER
239+
240+
241+andM :: (Monad m) => [m Bool] -> m Bool
242+andM checks =
243+ case checks of
244+ [] ->
245+ return True
246+
247+ check : otherChecks ->
248+ do bool <- check
249+ if bool then andM otherChecks else return False
builder/src/File/Plan.hsnew file+196
@@ -0,0 +1,196 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module File.Plan
4+ ( plan
5+ , Info(..)
6+ )
7+ where
8+
9+import Control.Concurrent (forkIO)
10+import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan)
11+import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, readMVar)
12+import Control.Monad (foldM, void, when)
13+import Control.Monad.Except (liftIO)
14+import qualified Data.Binary as Binary
15+import qualified Data.ByteString as BS
16+import qualified Data.Map as Map
17+import qualified Data.Time.Clock as Time
18+import qualified System.Directory as Dir
19+
20+import qualified Elm.Compiler.Module as Module
21+import qualified Elm.Package as Pkg
22+
23+import qualified Elm.Project.Json as Project
24+import qualified Elm.Project.Summary as Summary
25+import qualified File.Crawl as Crawl
26+import qualified File.Header as Header
27+import qualified Reporting.Task as Task
28+import qualified Stuff.Paths
29+
30+
31+
32+-- PLAN
33+
34+
35+type Dict value = Map.Map Module.Raw value
36+
37+
38+plan :: Summary.Summary -> Crawl.Result -> Task.Task (Dict Info, Module.Interfaces)
39+plan (Summary.Summary root project _ ifaces _) (Crawl.Graph _ locals _ foreigns _) =
40+ liftIO $
41+ do queue <- newChan
42+ let env = Env queue root (Project.getName project)
43+
44+ mvar <- newEmptyMVar
45+ statusMVars <- Map.traverseWithKey (getStatus env mvar foreigns) locals
46+ putMVar mvar statusMVars
47+
48+ void $ forkIO $
49+ do graph <- Map.traverseMaybeWithKey (\_ -> readMVar) statusMVars
50+ writeChan queue (EndLoop graph)
51+
52+ ifaceLoader queue ifaces
53+
54+
55+data Env =
56+ Env
57+ { _queue :: Chan Msg
58+ , _root :: FilePath
59+ , _pkg :: Pkg.Name
60+ }
61+
62+
63+
64+-- STATUS
65+
66+
67+type Status = Maybe Info
68+ -- Nothing == clean
69+ -- Just info == dirty
70+
71+
72+data Info =
73+ Info
74+ { _path :: FilePath
75+ , _time :: Time.UTCTime
76+ , _src :: BS.ByteString
77+ , _clean :: [Module.Raw]
78+ , _dirty :: [Module.Raw]
79+ , _foreign :: [Module.Canonical] -- TODO is this needed?
80+ }
81+
82+
83+
84+-- GET STATUS
85+
86+
87+getStatus
88+ :: Env
89+ -> MVar (Dict (MVar Status))
90+ -> Dict Pkg.Package
91+ -> Module.Raw
92+ -> Header.Info
93+ -> IO (MVar Status)
94+getStatus env statusMVars foreigns name (Header.Info path time src deps) =
95+ do mvar <- newEmptyMVar
96+
97+ void $ forkIO $ putMVar mvar =<<
98+ do statuses <- readMVar statusMVars
99+ info <- foldM (addDep statuses foreigns) (Info path time src [] [] []) deps
100+
101+ let elmi = Stuff.Paths.elmi (_root env) name
102+
103+ case _dirty info of
104+ _ : _ ->
105+ do remove elmi
106+ return (Just info)
107+
108+ [] ->
109+ do fresh <- isFresh time elmi
110+ if fresh
111+ then
112+ do let canonical = Module.Canonical (_pkg env) name
113+ writeChan (_queue env) (Get canonical elmi)
114+ return Nothing
115+ else
116+ do remove elmi
117+ return (Just info)
118+
119+ return mvar
120+
121+
122+addDep :: Dict (MVar Status) -> Dict Pkg.Package -> Info -> Module.Raw -> IO Info
123+addDep locals foreigns info name =
124+ case Map.lookup name locals of
125+ Just mvar ->
126+ do status <- readMVar mvar
127+ case status of
128+ Nothing ->
129+ return $ info { _clean = name : _clean info }
130+
131+ Just _ ->
132+ return $ info { _dirty = name : _dirty info }
133+
134+ Nothing ->
135+ case Map.lookup name foreigns of
136+ Just (Pkg.Package pkg _vsn) ->
137+ return $ info { _foreign = Module.Canonical pkg name : _foreign info }
138+
139+ Nothing ->
140+ return info -- must be native
141+
142+
143+
144+-- IS FRESH
145+
146+
147+isFresh :: Time.UTCTime -> FilePath -> IO Bool
148+isFresh srcTime elmi =
149+ andM
150+ [ Dir.doesFileExist elmi
151+ , do elmiTime <- Dir.getModificationTime elmi
152+ return (elmiTime >= srcTime)
153+ ]
154+
155+
156+andM :: [IO Bool] -> IO Bool
157+andM checks =
158+ case checks of
159+ [] ->
160+ return True
161+
162+ check : otherChecks ->
163+ do bool <- check
164+ if bool then andM otherChecks else return False
165+
166+
167+remove :: FilePath -> IO ()
168+remove path =
169+ do exists <- Dir.doesFileExist path
170+ when exists (Dir.removeFile path)
171+
172+
173+
174+-- INTERFACE LOADER
175+
176+
177+data Msg
178+ = EndLoop (Dict Info)
179+ | Get Module.Canonical FilePath
180+
181+
182+ifaceLoader :: Chan Msg -> Module.Interfaces -> IO (Dict Info, Module.Interfaces)
183+ifaceLoader queue ifaces =
184+ do msg <- readChan queue
185+ case msg of
186+ EndLoop dirty ->
187+ return ( dirty, ifaces )
188+
189+ Get canonical elmi ->
190+ case Map.lookup canonical ifaces of
191+ Just _ ->
192+ ifaceLoader queue ifaces
193+
194+ Nothing ->
195+ do iface <- Binary.decodeFile elmi
196+ ifaceLoader queue $ Map.insert canonical iface ifaces
builder/src/File/Watcher.hsnew file+76
@@ -0,0 +1,76 @@
1+{-# OPTIONS_GHC -Wall #-}
2+module File.Watcher where
3+
4+import Control.Concurrent (forkIO, threadDelay)
5+import Control.Concurrent.Chan (newChan, readChan, writeChan)
6+import qualified System.FSNotify as Notify
7+
8+import qualified Elm.Project as Project
9+import Elm.Project (Project)
10+
11+
12+
13+-- GRAPH
14+
15+
16+data Graph =
17+ Graph
18+ { _elm :: Map.Map Module.Raw Node
19+ , _js :: Map.Map Module.Raw FilePath
20+ }
21+
22+
23+data Node =
24+ Node
25+ { _path :: FilePath
26+ , _time :: UTCTime
27+ , _needs :: Set.Set Module.Raw
28+ , _blocks :: Set.Set Module.Raw
29+ , _iface :: Maybe (UTCTime, Interface)
30+ }
31+
32+
33+
34+-- ABC
35+
36+
37+action :: Notify.Event -> IO ()
38+action event =
39+ case event of
40+ Notify.Added path time ->
41+ return ()
42+
43+ Notify.Modified path time ->
44+ return ()
45+
46+ Notify.Removed path time ->
47+ return ()
48+
49+
50+watcher :: Project -> IO ()
51+watcher project =
52+ let
53+ srcDir =
54+ Project.toSourceDir project
55+
56+ action event =
57+ case event of
58+ Notify.Added _ _ ->
59+ return ()
60+
61+ Notify.Modified path time ->
62+
63+ Notify.Removed path time ->
64+
65+ in
66+ do killer <- newChan
67+ mapM_ (watcherHelp killer action) [srcDir]
68+
69+
70+
71+watcherHelp :: Notify.Action -> FilePath -> IO ()
72+watcherHelp killer action dir =
73+ void $ forkIO $ Notify.withManager $ \manager ->
74+ do stop <- Notify.watchTree manager dir (const True) action
75+ _ <- readChan killer
76+ stop
builder/src/Generate/Functions.hsnew file+93
@@ -0,0 +1,93 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE QuasiQuotes #-}
4+module Generate.Functions (functions) where
5+
6+import qualified Data.ByteString.Builder as BS
7+import Text.RawString.QQ (r)
8+
9+
10+
11+-- FUNCTIONS
12+
13+
14+functions :: BS.Builder
15+functions = [r|
16+
17+function F(arity, fun, wrapper) {
18+ wrapper.a = arity;
19+ wrapper.f = fun;
20+ return wrapper;
21+}
22+
23+function F2(fun) {
24+ return F(2, fun, function(a) { return function(b) { return fun(a,b); }; })
25+}
26+function F3(fun) {
27+ return F(3, fun, function(a) {
28+ return function(b) { return function(c) { return fun(a, b, c); }; };
29+ });
30+}
31+function F4(fun) {
32+ return F(4, fun, function(a) { return function(b) { return function(c) {
33+ return function(d) { return fun(a, b, c, d); }; }; };
34+ });
35+}
36+function F5(fun) {
37+ return F(5, fun, function(a) { return function(b) { return function(c) {
38+ return function(d) { return function(e) { return fun(a, b, c, d, e); }; }; }; };
39+ });
40+}
41+function F6(fun) {
42+ return F(6, fun, function(a) { return function(b) { return function(c) {
43+ return function(d) { return function(e) { return function(f) {
44+ return fun(a, b, c, d, e, f); }; }; }; }; };
45+ });
46+}
47+function F7(fun) {
48+ return F(7, fun, function(a) { return function(b) { return function(c) {
49+ return function(d) { return function(e) { return function(f) {
50+ return function(g) { return fun(a, b, c, d, e, f, g); }; }; }; }; }; };
51+ });
52+}
53+function F8(fun) {
54+ return F(8, fun, function(a) { return function(b) { return function(c) {
55+ return function(d) { return function(e) { return function(f) {
56+ return function(g) { return function(h) {
57+ return fun(a, b, c, d, e, f, g, h); }; }; }; }; }; }; };
58+ });
59+}
60+function F9(fun) {
61+ return F(9, fun, function(a) { return function(b) { return function(c) {
62+ return function(d) { return function(e) { return function(f) {
63+ return function(g) { return function(h) { return function(i) {
64+ return fun(a, b, c, d, e, f, g, h, i); }; }; }; }; }; }; }; };
65+ });
66+}
67+
68+function A2(fun, a, b) {
69+ return fun.a === 2 ? fun.f(a, b) : fun(a)(b);
70+}
71+function A3(fun, a, b, c) {
72+ return fun.a === 3 ? fun.f(a, b, c) : fun(a)(b)(c);
73+}
74+function A4(fun, a, b, c, d) {
75+ return fun.a === 4 ? fun.f(a, b, c, d) : fun(a)(b)(c)(d);
76+}
77+function A5(fun, a, b, c, d, e) {
78+ return fun.a === 5 ? fun.f(a, b, c, d, e) : fun(a)(b)(c)(d)(e);
79+}
80+function A6(fun, a, b, c, d, e, f) {
81+ return fun.a === 6 ? fun.f(a, b, c, d, e, f) : fun(a)(b)(c)(d)(e)(f);
82+}
83+function A7(fun, a, b, c, d, e, f, g) {
84+ return fun.a === 7 ? fun.f(a, b, c, d, e, f, g) : fun(a)(b)(c)(d)(e)(f)(g);
85+}
86+function A8(fun, a, b, c, d, e, f, g, h) {
87+ return fun.a === 8 ? fun.f(a, b, c, d, e, f, g, h) : fun(a)(b)(c)(d)(e)(f)(g)(h);
88+}
89+function A9(fun, a, b, c, d, e, f, g, h, i) {
90+ return fun.a === 9 ? fun.f(a, b, c, d, e, f, g, h, i) : fun(a)(b)(c)(d)(e)(f)(g)(h)(i);
91+}
92+
93+|]
builder/src/Generate/Output.hsnew file+117
@@ -0,0 +1,117 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Generate.Output
4+ ( generate
5+ , generateReplFile
6+ )
7+ where
8+
9+
10+import Control.Monad.Trans (liftIO)
11+import qualified Data.ByteString.Builder as B
12+import qualified Data.List.NonEmpty as NonEmpty
13+import qualified Data.Map as Map
14+import Data.Monoid ((<>))
15+import System.FilePath ((</>))
16+
17+import qualified Elm.Compiler.Module as Module
18+import qualified Elm.Compiler.Objects as Obj
19+import qualified Elm.Name as N
20+import qualified Elm.Package as Pkg
21+
22+import qualified Elm.Project.Flags as Flags
23+import qualified Elm.Project.Json as Project
24+import qualified Elm.Project.Summary as Summary
25+import qualified File.Args as Args
26+import qualified File.Crawl as Crawl
27+import qualified File.IO as IO
28+import qualified Generate.Functions as Functions
29+import qualified Reporting.Task as Task
30+import qualified Stuff.Paths as Paths
31+
32+
33+
34+-- GENERATE
35+
36+
37+generate :: Flags.Options -> Summary.Summary -> Crawl.Result -> Task.Task ()
38+generate options summary graph@(Crawl.Graph args _ _ _ _) =
39+ case args of
40+ Args.Pkg _ ->
41+ return ()
42+
43+ Args.Roots names ->
44+ generateMonolith options summary graph (NonEmpty.toList names)
45+
46+
47+
48+-- GENERATE MONOLITH
49+
50+
51+generateMonolith :: Flags.Options -> Summary.Summary -> Crawl.Result -> [Module.Raw] -> Task.Task ()
52+generateMonolith (Flags.Options debug target output) summary@(Summary.Summary _ project _ ifaces _) graph names =
53+ do
54+ objectGraph <- organize summary graph
55+ let pkg = Project.getName project
56+ let roots = map (Module.Canonical pkg) names
57+ let (Right builder) = Obj.generate debug target ifaces objectGraph roots
58+ let monolith =
59+ "(function(scope){\n'use strict';" <> Functions.functions <> builder <> "}(this));"
60+
61+ liftIO $
62+ case output of
63+ Nothing ->
64+ IO.writeBuilder "elm.js" monolith
65+
66+ Just Flags.None ->
67+ return ()
68+
69+ Just (Flags.Custom maybeDir fileName) ->
70+ do path <- Flags.safeCustomPath maybeDir fileName
71+ IO.writeBuilder path monolith
72+
73+
74+
75+-- GENERATE REPL MONOLITH
76+
77+
78+generateReplFile :: Summary.Summary -> Crawl.Result -> N.Name -> Task.Task FilePath
79+generateReplFile summary@(Summary.Summary _ project _ ifaces _) graph name =
80+ do
81+ objectGraph <- organize summary graph
82+
83+ let home = Module.Canonical (Project.getName project) "ElmRepl"
84+ let builder = Obj.generateForRepl ifaces objectGraph home name
85+
86+ liftIO $ IO.writeBuilder Paths.temp $
87+ replRecovery <> "(function(){\n'use strict';" <> Functions.functions <> builder <> "}());"
88+
89+ return Paths.temp
90+
91+
92+replRecovery :: B.Builder
93+replRecovery =
94+ "process.on('uncaughtException', function(err) { process.stderr.write(err.toString()); process.exit(1); });"
95+
96+
97+
98+-- ORGANIZE
99+
100+
101+organize :: Summary.Summary -> Crawl.Result -> Task.Task Obj.Graph
102+organize (Summary.Summary root _ _ _ deps) (Crawl.Graph _ locals _ _ _) =
103+ do localObjs <- Obj.unions <$> traverse (loadModuleObj root) (Map.keys locals)
104+ foreignObjs <- Obj.unions <$> traverse loadPackageObj (Map.toList deps)
105+ return (Obj.union localObjs foreignObjs)
106+
107+
108+loadModuleObj :: FilePath -> Module.Raw -> Task.Task Obj.Graph
109+loadModuleObj root name =
110+ IO.readBinary (Paths.elmo root name)
111+
112+
113+loadPackageObj :: ( Pkg.Name, (Pkg.Version, deps) ) -> Task.Task Obj.Graph
114+loadPackageObj ( name, (version,_) ) =
115+ do dir <- Task.getPackageCacheDirFor name version
116+ IO.readBinary (dir </> "objs.dat")
117+
builder/src/Reporting/Error.hsnew file+153
@@ -0,0 +1,153 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Reporting.Error
4+ ( Error(..)
5+ , toString
6+ , toStderr
7+ )
8+ where
9+
10+
11+import qualified Text.PrettyPrint.ANSI.Leijen as P
12+
13+import qualified Elm.Compiler as Compiler
14+import qualified Elm.Compiler.Module as Module
15+import qualified Elm.Package as Pkg
16+import qualified Elm.Utils as Utils
17+import qualified Reporting.Error.Assets as Asset
18+import qualified Reporting.Error.Bump as Bump
19+import qualified Reporting.Error.Compile as Compile
20+import qualified Reporting.Error.Crawl as Crawl
21+import qualified Reporting.Error.Deps as Deps
22+import qualified Reporting.Error.Diff as Diff
23+import qualified Reporting.Error.Help as Help
24+import qualified Reporting.Error.Http as Http
25+import qualified Reporting.Error.Publish as Publish
26+
27+
28+
29+-- ALL POSSIBLE ERRORS
30+
31+
32+data Error
33+ = NoElmJson
34+ | Assets Asset.Error
35+ | Bump Bump.Error
36+ | Compile Compile.Error [Compile.Error]
37+ | Crawl Crawl.Error
38+ | Cycle [Module.Raw] -- TODO write docs to help with this scenario
39+ | Deps Deps.Error
40+ | Diff Diff.Error
41+ | Publish Publish.Error
42+ | BadHttp String Http.Error
43+
44+ -- install
45+ | NoSolution [Pkg.Name]
46+ | CannotMakeNothing
47+
48+
49+
50+-- RENDERERS
51+
52+
53+toString :: Error -> String
54+toString err =
55+ Help.toString (Help.reportToDoc (toReport err))
56+
57+
58+toStderr :: Error -> IO ()
59+toStderr err =
60+ Help.toStderr (Help.reportToDoc (toReport err))
61+
62+
63+toReport :: Error -> Help.Report
64+toReport err =
65+ case err of
66+ NoElmJson ->
67+ Help.report "WELCOME" Nothing
68+ "It looks like you are trying to start a new Elm project. Very exciting! :D"
69+ [ P.fillSep
70+ ["I","very","highly","recommend","working","through"
71+ ,P.green "<https://guide.elm-lang.org>","which","will","teach","you","the"
72+ ,"basics","of","Elm,","including","how","to","start","new","projects."
73+ ]
74+ , P.fillSep
75+ ["For","folks","who","have","already","built","stuff","with","Elm,","the"
76+ ,"problem","is","just","that","there","is","no",P.dullyellow "elm.json","yet."
77+ ,"If","you","want","to","work","from","an","example,","check","out","the"
78+ ,"one","at","<https://github.com/evancz/elm-todomvc/blob/master/elm.json>"
79+ ]
80+ , Help.reflow
81+ "Whatever your scenario, I hope you have a lovely time using Elm!"
82+ ]
83+
84+ Assets assetError ->
85+ Asset.toReport assetError
86+
87+ Bump bumpError ->
88+ Bump.toReport bumpError
89+
90+ Compile e es ->
91+ Help.compilerReport $ Compile.toDoc e es
92+
93+ Crawl crawlError ->
94+ Crawl.toReport crawlError
95+
96+ Cycle names ->
97+ Help.report "IMPORT CYCLE" Nothing
98+ "Your module imports form a cycle:"
99+ [ P.indent 4 (Utils.drawCycle names)
100+ , Help.reflow $
101+ "Learn more about why this is disallowed and how to break cycles here:"
102+ ++ Help.hintLink "import-cycles"
103+ ]
104+
105+ Deps depsError ->
106+ Deps.toReport depsError
107+
108+ Diff commandsError ->
109+ Diff.toReport commandsError
110+
111+ Publish publishError ->
112+ Publish.toReport publishError
113+
114+ BadHttp url httpError ->
115+ Http.toReport url httpError
116+
117+ NoSolution badPackages ->
118+ case badPackages of
119+ [] ->
120+ Help.report "UNSOLVABLE DEPENDENCIES" (Just "elm.json")
121+ "This usually happens if you try to modify dependency constraints by\
122+ \ hand. I recommend deleting any dependency you added recently (or all\
123+ \ of them if things are bad) and then adding them again with:"
124+ [ P.indent 4 $ P.green "elm install"
125+ , Help.reflow $
126+ "And do not be afaid to ask for help on Slack if you get stuck!"
127+ ]
128+
129+ _:_ ->
130+ Help.report "OLD DEPENDENCIES" (Just "elm.json")
131+ ( "You are using Elm " ++ Pkg.versionToString Compiler.version
132+ ++ ", but the following packages have not been updated for this version yet:"
133+ )
134+ [ P.vcat $ map (P.red . P.text . Pkg.toString) badPackages
135+ , Help.note
136+ "Please be kind to the relevant package authors! Having friendly interactions\
137+ \ with users is great motivation, and conversely, getting berated by strangers\
138+ \ on the internet sucks your soul dry. Furthermore, package authors are humans\
139+ \ with families, friends, jobs, vacations, responsibilities, goals, etc. They\
140+ \ face obstacles outside of their technical work you will never know about,\
141+ \ so please assume the best and try to be patient and supportive!"
142+ ]
143+
144+ CannotMakeNothing ->
145+ Help.report "NO INPUT" Nothing
146+ "What should I make though? I need more information, like:"
147+ [ P.vcat
148+ [ P.indent 4 $ P.green "elm make MyThing.elm"
149+ , P.indent 4 $ P.green "elm make This.elm That.elm"
150+ ]
151+ , Help.reflow
152+ "However many files you give, I will create one JS file out of them."
153+ ]
builder/src/Reporting/Error/Assets.hsnew file+226
@@ -0,0 +1,226 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Reporting.Error.Assets
4+ ( Error(..)
5+ , ElmJsonProblem(..)
6+ , toReport
7+ , BadElmJsonContent(..)
8+ , badContentToDocs
9+ )
10+ where
11+
12+
13+import Data.Monoid ((<>))
14+import qualified Data.Text as Text
15+import qualified Text.PrettyPrint.ANSI.Leijen as P
16+
17+import qualified Elm.Package as Pkg
18+import qualified Reporting.Error.Help as Help
19+
20+
21+
22+-- ERRORS
23+
24+
25+data Error
26+ = BadElmJson ElmJsonProblem
27+ | CorruptElmJson Pkg.Name Pkg.Version
28+ | CorruptDocumentation Pkg.Name Pkg.Version
29+ | CorruptBinary FilePath
30+
31+
32+
33+-- ELM JSON PROBLEM
34+
35+
36+data ElmJsonProblem
37+ = BadJson P.Doc
38+ | BadDepDup String String Pkg.Name [Pkg.Name]
39+ | BadSrcDir FilePath
40+
41+
42+
43+-- TO REPORT
44+
45+
46+toReport :: Error -> Help.Report
47+toReport err =
48+ case err of
49+ CorruptElmJson pkg vsn ->
50+ corruptJsonToReport "elm.json" pkg vsn
51+
52+ CorruptDocumentation pkg vsn ->
53+ corruptJsonToReport "docs.json" pkg vsn
54+
55+ CorruptBinary path ->
56+ Help.report "CORRUPT BINARY" (Just path)
57+ ("The binary data at " ++ path ++ " is corrupt.")
58+ [ Help.reflow $
59+ "Maybe a program is modifying your elm-stuff/ or ELM_HOME\
60+ \ directory in unexpected ways? Both of those are just caches, so\
61+ \ you can try deleting them and they will be rebuilt from scratch."
62+ ]
63+
64+ BadElmJson problem ->
65+ elmJsonProblemToReport problem
66+
67+
68+
69+-- CORRUPT JSON
70+
71+
72+corruptJsonToReport :: FilePath -> Pkg.Name -> Pkg.Version -> Help.Report
73+corruptJsonToReport path pkg vsn =
74+ Help.report "CORRUPT JSON" Nothing
75+ ( "The " ++ path ++ " for " ++ Pkg.toString pkg
76+ ++ " " ++ Pkg.versionToString vsn ++ " got corrupted somehow."
77+ )
78+ [ Help.reflow $
79+ "I removed it from my file cache, so if it was some transient\
80+ \ error it should be fixed if you try the same thing again.\
81+ \ Please report this if it seems like an Elm problem though!"
82+ ]
83+
84+
85+
86+-- BAD ELM JSON
87+
88+
89+elmJsonProblemToReport :: ElmJsonProblem -> Help.Report
90+elmJsonProblemToReport problem =
91+ case problem of
92+ BadJson jsonErrorDoc ->
93+ Help.compilerReport jsonErrorDoc
94+
95+ BadSrcDir dir ->
96+ Help.report "BAD JSON" (Just "elm.json")
97+ "The \"source-directories\" in your elm.json lists the following directory:"
98+ [ P.indent 4 (P.dullyellow (P.text dir))
99+ , Help.reflow "I cannot find that directory though! Is it missing? Is there a typo?"
100+ ]
101+
102+ BadDepDup field1 field2 dup dups ->
103+ let
104+ packagesAre =
105+ if null dups then "package is" else "packages are"
106+
107+ advice =
108+ if null dups then
109+ "Delete one of the entries."
110+ else
111+ "Delete duplicates until each package lives in ONE category."
112+ in
113+ Help.report "BAD JSON" (Just "elm.json")
114+ (
115+ if null dups then
116+ "The following package appears twice in your elm.json file:"
117+ else
118+ "The following packages appear twice in your elm.json file:"
119+ )
120+ [ P.dullyellow $ P.indent 4 $ P.vcat $
121+ map (P.text . Pkg.toString) (dup:dups)
122+ , Help.reflow $
123+ "The " ++ packagesAre ++ " available in \"" ++ field1
124+ ++ "\", so it is redundant to list it again in \"" ++ field2
125+ ++ "\". It is already available! " ++ advice
126+ , Help.reflow "More help at <https://github.com/elm-lang/elm-package/blob/master/TODO>"
127+ ]
128+
129+
130+
131+-- PROBLEM TO DOCS
132+
133+
134+data BadElmJsonContent
135+ = BadType Text.Text
136+ | BadPkgName String
137+ | BadVersion Text.Text
138+ | BadConstraint String
139+ | BadModuleName Text.Text
140+ | BadModuleHeaderTooLong Text.Text
141+ | BadDependencyName Text.Text
142+ | BadLicense Text.Text [Text.Text]
143+ | BadDirectoryNotString
144+ | BadSummaryTooLong
145+
146+
147+badContentToDocs :: BadElmJsonContent -> [P.Doc]
148+badContentToDocs badContent =
149+ case badContent of
150+ BadType _ ->
151+ ["The","only","valid","types","of","elm.json","are"
152+ ,P.green "\"browser\"","and",P.green "\"package\"" <> "."
153+ ]
154+
155+ BadPkgName problem ->
156+ map P.text (words problem)
157+
158+ BadVersion txt ->
159+ ["You","provided",P.red (P.text (show txt))
160+ ,"which","is","not","a","valid","version.","I","need","something","like"
161+ ,P.green "\"1.0.0\"","or",P.green "\"2.0.4\"" <> "."
162+ ]
163+
164+ BadConstraint problem ->
165+ map P.text (words ("It is not a valid constraint. " ++ problem))
166+
167+ BadModuleName name ->
168+ ["You","provided",P.text (show name),"which","is","not","a","valid","module","name."
169+ ,"I","need","something","like",P.green "\"Html.Events\"","or",P.green "\"Browser.Navigation\"" <> "."
170+ ]
171+
172+ BadModuleHeaderTooLong header ->
173+ ["The",P.text (show header),"header","is","too","long."
174+ ,"I","need","it","to","be",P.green "under",P.green "20",P.green "characters"
175+ ,"to","ensure","that","formatting","is","nice","on","the","package","website."
176+ ]
177+
178+ BadDependencyName name ->
179+ ["The",P.text (show name),"entry","is","not","a","valid","package","name."
180+ ,"I","recommend","deleting","it,","finding","the","package","you","want","on"
181+ ,"the","package","website,","and","installing","it","with","the"
182+ ,P.green "`elm install`","command","instead."
183+ ]
184+
185+ BadLicense _given suggestions ->
186+ case suggestions of
187+ [] ->
188+ ["I","need","an","OSI","approved","SPDX","license,","like"
189+ ,P.green "\"BSD-3-Clause\"","or",P.green "\"MIT\".","See"
190+ ,"<https://spdx.org/licenses/>","for","a","full","list","of","options."
191+ ]
192+
193+ code:codes ->
194+ ["I","need","an","OSI","approved","SPDX","license."]
195+ ++ ["Maybe you wanted"] ++ oneOf code codes ++ ["instead?"]
196+ ++ ["See","<https://spdx.org/licenses/>","for","a","full","list","of","options."]
197+
198+ BadDirectoryNotString ->
199+ ["I","need","a",P.green "STRING","that","points","to","a","directory","of","Elm","code."
200+ ]
201+
202+ BadSummaryTooLong ->
203+ ["Your","summary","is","too","long.","I","need","it","to"
204+ ,"be",P.green "under",P.green "80",P.green "characters."
205+ ]
206+
207+
208+oneOf :: Text.Text -> [Text.Text] -> [P.Doc]
209+oneOf code codes =
210+ let
211+ toDoc spdx =
212+ P.green (P.text (show spdx))
213+ in
214+ case codes of
215+ [] ->
216+ [toDoc code]
217+
218+ [code2] ->
219+ [toDoc code,"or",toDoc code2]
220+
221+ _:_ ->
222+ let
223+ these = code : init codes
224+ final = last codes
225+ in
226+ map (\spdx -> toDoc spdx <> ",") these ++ ["or",toDoc final]
builder/src/Reporting/Error/Bump.hsnew file+53
@@ -0,0 +1,53 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Reporting.Error.Bump
4+ ( Error(..)
5+ , toReport
6+ )
7+ where
8+
9+
10+import Data.Monoid ((<>))
11+import qualified Text.PrettyPrint.ANSI.Leijen as P
12+
13+import qualified Elm.Package as Pkg
14+import qualified Reporting.Error.Help as Help
15+
16+
17+
18+-- ERRORS
19+
20+
21+data Error
22+ = Application
23+ | Unbumpable Pkg.Version [Pkg.Version]
24+
25+
26+
27+-- TO REPORT
28+
29+
30+toReport :: Error -> Help.Report
31+toReport err =
32+ case err of
33+ Application ->
34+ Help.report "UNBUMPABLE" (Just "elm.json")
35+ "Your elm.json says this is a browser project. That means it cannot be published\
36+ \ on <https://package.elm-lang.org> and therefore has no version to bump!"
37+ []
38+
39+ Unbumpable vsn versions ->
40+ Help.docReport "UNBUMPABLE" (Just "elm.json")
41+ ( P.fillSep
42+ ["Your","elm.json","says","I","should","bump","relative","to","version"
43+ ,P.red (P.text (Pkg.versionToString vsn)) <> ","
44+ ,"but","I","cannot","find","that","version","on","<https://package.elm-lang.org>."
45+ ,"That","means","there","is","no","API","for","me","to","diff","against","and"
46+ ,"figure","out","if","these","are","MAJOR,","MINOR,","or","PATCH","changes."
47+ ]
48+ )
49+ [ P.fillSep $
50+ ["Try","bumping","again","after","changing","the",P.dullyellow "\"version\"","in","elm.json"]
51+ ++ if length versions == 1 then ["to:"] else ["to","one","of","these:"]
52+ , P.vcat $ map (P.green . P.text . Pkg.versionToString) versions
53+ ]
builder/src/Reporting/Error/Compile.hsnew file+72
@@ -0,0 +1,72 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Reporting.Error.Compile
4+ ( Error(..)
5+ , toDoc
6+ )
7+ where
8+
9+
10+import Data.List.NonEmpty (NonEmpty((:|)))
11+import qualified Data.List.NonEmpty as NonEmpty
12+import qualified Data.Text as Text
13+import qualified Data.Time.Clock as Time
14+import qualified Text.PrettyPrint.ANSI.Leijen as P
15+
16+import qualified Elm.Compiler as Compiler
17+import qualified Elm.Compiler.Module as Module
18+
19+
20+
21+-- ERRORS
22+
23+
24+data Error =
25+ Error
26+ { _name :: Module.Raw
27+ , _path :: FilePath
28+ , _time :: Time.UTCTime
29+ , _source :: Text.Text
30+ , _errors :: [Compiler.Error]
31+ }
32+
33+
34+
35+-- TO DOC
36+
37+
38+toDoc :: Error -> [Error] -> P.Doc
39+toDoc e es =
40+ case NonEmpty.sortWith _time (e :| es) of
41+ err :| errors ->
42+ P.vcat (toDocHelp err errors)
43+
44+
45+toDocHelp :: Error -> [Error] -> [P.Doc]
46+toDocHelp e1 errs =
47+ case errs of
48+ [] ->
49+ [errorToDoc e1]
50+
51+ e2 : otherErrors ->
52+ errorToDoc e1 : separator (_name e1) (_name e2) : toDocHelp e2 otherErrors
53+
54+
55+errorToDoc :: Error -> P.Doc
56+errorToDoc (Error _name path _time source errors) =
57+ Compiler.errorsToDoc path source errors
58+
59+
60+separator :: Module.Raw -> Module.Raw -> P.Doc
61+separator beforeName afterName =
62+ let
63+ before = Module.nameToString beforeName ++ " ↑ "
64+ after = " ↓ " ++ Module.nameToString afterName
65+ in
66+ P.dullred $ P.vcat $
67+ [ P.indent (80 - length before) (P.text before)
68+ , "====o======================================================================o===="
69+ , P.text after
70+ , P.empty
71+ , P.empty
72+ ]
builder/src/Reporting/Error/Crawl.hsnew file+318
@@ -0,0 +1,318 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Reporting.Error.Crawl
4+ ( Error(..)
5+ , Problem(..)
6+ , Origin(..)
7+ , toReport
8+ )
9+ where
10+
11+
12+import qualified Data.ByteString as BS
13+import qualified Data.Char as Char
14+import qualified Data.Map as Map
15+import qualified Data.Text.Encoding as Text
16+import qualified Text.PrettyPrint.ANSI.Leijen as P
17+import Text.PrettyPrint.ANSI.Leijen ((<+>), (<>))
18+
19+import qualified Elm.Compiler as Compiler
20+import qualified Elm.Compiler.Module as Module
21+import qualified Elm.Package as Pkg
22+import qualified Reporting.Error.Help as Help
23+
24+
25+
26+-- ERROR
27+
28+
29+data Error
30+ = RootFileNotFound FilePath
31+ | RootModuleNameDuplicate Module.Raw [FilePath]
32+ | RootNameless FilePath
33+ | DependencyProblems Problem [Problem]
34+ | BadKernelHeader FilePath
35+
36+
37+data Problem
38+ = ModuleNotFound Origin Module.Raw -- TODO suggest other names
39+ | ModuleAmbiguous Origin Module.Raw [FilePath] [Pkg.Package]
40+ | BadHeader FilePath BS.ByteString Compiler.Error
41+ | ModuleNameReservedForKernel Origin Module.Raw
42+ | ModuleNameMissing FilePath Module.Raw
43+ | ModuleNameMismatch
44+ FilePath
45+ Module.Raw -- expected
46+ Module.Raw -- actual
47+ | PortsInPackage FilePath Module.Raw
48+ | EffectsUnexpected FilePath Module.Raw
49+
50+
51+data Origin
52+ = ElmJson
53+ | File FilePath
54+ | Module FilePath Module.Raw
55+
56+
57+
58+-- TO REPORT
59+
60+
61+toReport :: Error -> Help.Report
62+toReport err =
63+ case err of
64+ RootFileNotFound path ->
65+ Help.report "FILE NOT FOUND" Nothing
66+ "You want me to compile this file:"
67+ [ P.indent 4 $ P.dullyellow $ P.text path
68+ , "I cannot find it though! Is there a typo?"
69+ ]
70+
71+ RootModuleNameDuplicate name paths ->
72+ Help.report "DUPLICATE NAMES" Nothing
73+ "I am trying to compile multiple modules with the same name:"
74+ [ P.indent 4 $ P.dullyellow $ P.vcat $
75+ map P.text paths
76+ , P.fillSep $
77+ [ "These", "modules", if length paths == 2 then "both" else "all", "claim"
78+ , "to", "be", "named", P.dullyellow (P.text (Module.nameToString name)) <> "."
79+ , "Change", "them", "to", "have", "unique", "names", "and", "you"
80+ , "should", "be", "all", "set!"
81+ ]
82+ ]
83+
84+ RootNameless path ->
85+ namelessToDoc path "Main"
86+
87+ DependencyProblems problem otherProblems ->
88+ case otherProblems of
89+ [] ->
90+ problemToReport problem
91+
92+ _ ->
93+ problemToReport problem
94+ -- error "TODO handle multiple dependency errors"
95+
96+ BadKernelHeader filePath ->
97+ Help.report "BAD KERNEL HEADER" Nothing
98+ "I ran into a bad header in this file:"
99+ [ P.indent 4 $ P.dullyellow $ P.text filePath
100+ , Help.reflow $
101+ "NOTE: Kernel code is only available to core Elm libraries to ensure\
102+ \ the portability and security of the Elm ecosystem. This restriction\
103+ \ also makes it possible to improve code gen (i.e. performance) without\
104+ \ disrupting the ecosystem."
105+ ]
106+
107+
108+
109+-- PROBLEM TO REPORT
110+
111+
112+problemToReport :: Problem -> Help.Report
113+problemToReport problem =
114+ case problem of
115+ ModuleNotFound origin name ->
116+ notFoundToDoc origin name
117+
118+ ModuleAmbiguous origin child paths pkgs ->
119+ ambiguousToDoc origin child paths pkgs
120+
121+ BadHeader path source compilerError ->
122+ Help.compilerReport $
123+ Compiler.errorsToDoc path (Text.decodeUtf8 source) [compilerError]
124+
125+ ModuleNameMissing path name ->
126+ namelessToDoc path name
127+
128+ ModuleNameMismatch path expected actual ->
129+ Help.report "MODULE NAME MISMATCH" (Just path)
130+ ( "The file at " ++ path ++ " has a typo in the module name. It says:"
131+ )
132+ [ P.indent 4 $ P.dullyellow $ "module" <+> P.red (P.text (Module.nameToString actual)) <+> "exposing (..)"
133+ , "Looks like a typo or copy/paste error. Instead it needs to say:"
134+ , P.indent 4 $ P.dullyellow $ "module" <+> P.green (P.text (Module.nameToString expected)) <+> "exposing (..)"
135+ , "Make the change and you should be all set!"
136+ ]
137+
138+ ModuleNameReservedForKernel origin name ->
139+ kernelNameToDoc origin name
140+
141+ PortsInPackage path name ->
142+ badTagToDoc path name "port" "port-modules" $
143+ "Packages cannot have any `port` modules."
144+
145+ EffectsUnexpected path name ->
146+ badTagToDoc path name "effect" "effect-modules" $
147+ "Creating `effect` modules is relatively experimental. There are a\
148+ \ couple in @elm-lang repos right now, but we have decided to be\
149+ \ very cautious in expanding its usage."
150+
151+
152+badTagToDoc :: FilePath -> Module.Raw -> String -> String -> String -> Help.Report
153+badTagToDoc path name tag hintName summary =
154+ Help.report
155+ ("UNEXPECTED " ++ map Char.toUpper tag ++ " MODULE")
156+ (Just path)
157+ summary
158+ [ P.fillSep $
159+ [ "Get", "rid", "of", "all", "the"
160+ , P.red (P.text tag)
161+ , "stuff", "in"
162+ , P.dullyellow (P.text (Module.nameToString name))
163+ , "to", "proceed."
164+ ]
165+ , Help.note $
166+ "You can learn the reasoning behind this design choice at "
167+ ++ Help.hintLink hintName
168+ ]
169+
170+
171+
172+-- HELPERS
173+
174+
175+namelessToDoc :: FilePath -> Module.Raw -> Help.Report
176+namelessToDoc path name =
177+ Help.report "UNNAMED MODULE" (Just path)
178+ ( "The `" ++ Module.nameToString name
179+ ++ "` module must start with a line like this:"
180+ )
181+ [ P.indent 4 $ P.dullyellow $ P.text $
182+ "module " ++ Module.nameToString name ++ " exposing (..)"
183+ , Help.reflow $
184+ "Try adding that as the first line of your file!"
185+ , Help.note $
186+ "It is best to replace (..) with an explicit list of types and\
187+ \ functions you want to expose. If you know a value is only used\
188+ \ WITHIN this module, it is extra easy to refactor. This kind of\
189+ \ information is great, especially as your project grows!"
190+ ]
191+
192+
193+notFoundToDoc :: Origin -> Module.Raw -> Help.Report
194+notFoundToDoc origin child =
195+ case origin of
196+ ElmJson ->
197+ Help.report "MODULE NOT FOUND" Nothing
198+ "Your elm.json says your project has the following module:"
199+ [ P.indent 4 $ P.dullyellow $ P.text $ Module.nameToString child
200+ , Help.reflow $
201+ "I cannot find it though! Is there a typo in the module name?"
202+ ]
203+
204+ File path ->
205+ Help.report "UNKNOWN IMPORT" (Just path)
206+ ("I cannot find a `" ++ Module.nameToString child ++ "` module to import.")
207+ (notFoundDetails child)
208+
209+ Module path parent ->
210+ Help.report "UNKNOWN IMPORT" (Just path)
211+ ("The " ++ Module.nameToString parent ++ " module has a bad import:")
212+ (notFoundDetails child)
213+
214+
215+notFoundDetails :: Module.Raw -> [P.Doc]
216+notFoundDetails child =
217+ let
218+ simulatedCode =
219+ P.indent 4 $ P.dullyellow $ P.text $ "import " ++ Module.nameToString child
220+ in
221+ case Map.lookup child Pkg.suggestions of
222+ Just pkg ->
223+ [ simulatedCode
224+ , Help.reflow $
225+ "Do you want the one from the " ++ Pkg.toString pkg
226+ ++ " package? If so, run this command to add that dependency to your elm.json file:"
227+ , P.indent 4 $ P.green $ P.text $ "elm install " ++ Pkg.toString pkg
228+ , Help.reflow $
229+ "If you want a local file, make sure the directory that contains the `"
230+ ++ Module.nameToString child
231+ ++ "` module is listed in your elm.json \"source-directories\" field."
232+ ]
233+
234+ Nothing ->
235+ [ simulatedCode
236+ , "I cannot find that module! Is there a typo in the module name?"
237+ , P.vcat
238+ [ P.fillSep
239+ ["Is","it","defined","in","a","package?","Did","you"
240+ ,P.green "elm install"
241+ ,"that","package","yet?"
242+ ]
243+ , Help.reflow $
244+ "Is it a local file? Maybe it lives in some directory, but I cannot find that\
245+ \ directory unless it is listed in the \"source-directories\" field of your elm.json!"
246+ ]
247+ ]
248+
249+
250+ambiguousToDoc :: Origin -> Module.Raw -> [FilePath] -> [Pkg.Package] -> Help.Report
251+ambiguousToDoc origin child paths pkgs =
252+ let
253+ pkgToString (Pkg.Package pkg vsn) =
254+ "exposed by " ++ Pkg.toString pkg ++ " " ++ Pkg.versionToString vsn
255+
256+ makeReport maybePath summary yellowString =
257+ Help.report "AMBIGUOUS IMPORT" maybePath summary
258+ [ P.indent 4 $ P.dullyellow $ P.text yellowString
259+ , Help.reflow $
260+ "I found multiple module with that name though:"
261+ , P.indent 4 $ P.dullyellow $ P.vcat $
262+ map P.text $ paths ++ map pkgToString pkgs
263+ , Help.reflow $
264+ if null paths then
265+ "It looks like the name clash is in your dependencies, which is\
266+ \ out of your control. Elm does not support this scenario right\
267+ \ now, but it may be worthwhile. Please open an issue describing\
268+ \ your scenario so we can gather more usage information!"
269+ else
270+ "Which is the right one? Try renaming your modules to have unique names."
271+ ]
272+ in
273+ case origin of
274+ ElmJson ->
275+ makeReport
276+ Nothing
277+ "Your elm.json wants the following module:"
278+ (Module.nameToString child)
279+
280+ File path ->
281+ makeReport
282+ (Just path)
283+ ("The file at " ++ path ++ " has an ambiguous import:")
284+ ("import " ++ Module.nameToString child)
285+
286+ Module path parent ->
287+ makeReport
288+ (Just path)
289+ ("The " ++ Module.nameToString parent ++ " module has an ambiguous import:")
290+ ("import " ++ Module.nameToString child)
291+
292+
293+kernelNameToDoc :: Origin -> Module.Raw -> Help.Report
294+kernelNameToDoc origin kernelName =
295+ let
296+ (maybePath, statement) =
297+ case origin of
298+ ElmJson ->
299+ ( Nothing
300+ , "Your elm.json says your project has the following module:"
301+ )
302+
303+ File path ->
304+ ( Just path
305+ , "This file is trying to import the following module:"
306+ )
307+
308+ Module path parent ->
309+ ( Just path
310+ , "Your " ++ Module.nameToString parent ++ " module is trying to import:"
311+ )
312+ in
313+ Help.report "BAD MODULE NAME" maybePath statement
314+ [ P.indent 4 $ P.dullyellow $ P.text $ Module.nameToString kernelName
315+ , Help.reflow $
316+ "But names like that are reserved for internal use.\
317+ \ Switch to a name outside of the Elm/Kernel/ namespace."
318+ ]
builder/src/Reporting/Error/Deps.hsnew file+100
@@ -0,0 +1,100 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Reporting.Error.Deps
4+ ( Error(..)
5+ , toReport
6+ )
7+ where
8+
9+import Text.PrettyPrint.ANSI.Leijen ((<>))
10+import qualified Text.PrettyPrint.ANSI.Leijen as P
11+
12+import qualified Elm.Compiler as Compiler
13+import qualified Elm.Package as Pkg
14+import qualified Elm.Project.Constraint as Con
15+import qualified Reporting.Error.Help as Help
16+
17+
18+
19+-- ERRORS
20+
21+
22+data Error
23+ = CorruptVersionCache Pkg.Name
24+ | PackageNotFound Pkg.Name [Pkg.Name]
25+ -- verify
26+ | AppBadElm Pkg.Version
27+ | PkgBadElm Con.Constraint
28+ | BadDeps
29+ | BuildFailure Pkg.Name Pkg.Version
30+
31+
32+
33+-- TO DOC
34+
35+
36+toReport :: Error -> Help.Report
37+toReport err =
38+ case err of
39+ CorruptVersionCache pkg ->
40+ Help.report "CORRUPT CACHE" Nothing
41+ ( "I ran into an unknown package while exploring dependencies:"
42+ )
43+ [ P.indent 4 $ P.dullyellow $ P.text $ Pkg.toString pkg
44+ , Help.reflow $
45+ "This suggests that your ELM_HOME directory has been corrupted.\
46+ \ Maybe some program is messing with it? It is just cached files,\
47+ \ so you can delete it and see if that fixes the issue."
48+ ]
49+
50+ PackageNotFound package suggestions ->
51+ Help.report "PACKAGE NOT FOUND" Nothing
52+ ( "Could not find any packages named " ++ Pkg.toString package
53+ )
54+ [ P.text $ "Maybe you want one of these instead?"
55+ , P.indent 4 $ P.vcat $ map (P.text . Pkg.toString) suggestions
56+ ]
57+
58+ AppBadElm version ->
59+ Help.report "ELM VERSION MISMATCH" Nothing
60+ "Your elm.json says this application needs a different version of Elm."
61+ [ P.fillSep
62+ [ "It", "requires"
63+ , P.green (P.text (Pkg.versionToString version)) <> ","
64+ , "but", "you", "are", "using"
65+ , P.red (P.text (Pkg.versionToString Compiler.version))
66+ , "right", "now."
67+ ]
68+ ]
69+
70+ PkgBadElm constraint ->
71+ Help.report "ELM VERSION MISMATCH" Nothing
72+ "Your elm.json says this package needs a version of Elm in this range:"
73+ [ P.indent 4 $ P.dullyellow $ P.text $ Con.toString constraint
74+ , P.fillSep
75+ [ "But", "you", "are", "using", "Elm"
76+ , P.red (P.text (Pkg.versionToString Compiler.version))
77+ , "right", "now."
78+ ]
79+ ]
80+
81+ BadDeps ->
82+ Help.report "CLASHING PACKAGE DEPENDENCIES" Nothing
83+ "The dependencies in your elm.json are not compatible."
84+ [ Help.reflow $
85+ "Did you change them by hand? Try to change it back! It is much\
86+ \ better to add dependencies with commands like this:"
87+ , P.indent 4 $ P.dullyellow $ P.text $ "elm install elm-lang/http"
88+ , Help.reflow $
89+ "Please ask for help on the Elm slack <http://elmlang.herokuapp.com/> if\
90+ \ you are running into something that seems trickier than this."
91+ ]
92+
93+ BuildFailure pkg vsn ->
94+ Help.report "CORRUPT DEPENDENCY" Nothing
95+ "I ran into a problem while building the following package:"
96+ [ P.indent 4 $ P.dullyellow $ P.text $ Pkg.toString pkg ++ " " ++ Pkg.versionToString vsn
97+ , Help.reflow $
98+ "This probably means the downloaded files got corrupted somehow.\
99+ \ Maybe try deleting your ELM_HOME and see if that resolves the issue?"
100+ ]
builder/src/Reporting/Error/Diff.hsnew file+78
@@ -0,0 +1,78 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Reporting.Error.Diff
4+ ( Error(..)
5+ , toReport
6+ )
7+ where
8+
9+import qualified Data.List as List
10+import qualified Text.PrettyPrint.ANSI.Leijen as P
11+
12+import qualified Elm.Package as Pkg
13+import qualified Reporting.Error.Help as Help
14+
15+
16+
17+-- ERRORS
18+
19+
20+data Error
21+ = Application
22+ | UnknownPackage Pkg.Name [Pkg.Name]
23+ | UnknownVersion Pkg.Name Pkg.Version [Pkg.Version]
24+
25+
26+
27+-- TO DOC
28+
29+
30+toReport :: Error -> Help.Report
31+toReport err =
32+ case err of
33+ Application ->
34+ Help.report "CANNOT DIFF APPLICATIONS" Nothing
35+ "I cannot perform diffs on applications, only packages! If you are\
36+ \ just curious to see a diff, try running this command:"
37+ [ P.indent 4 $ P.green $ "elm diff elm-lang/html 5.1.1 6.0.0"
38+ ]
39+
40+ UnknownPackage pkg suggestions ->
41+ Help.report "UNKNOWN PACKAGE" Nothing
42+ ( "You are trying to diff against this package:"
43+ )
44+ [ P.indent 4 $ P.red $ P.text $ Pkg.toString pkg
45+ , Help.stack $
46+ case suggestions of
47+ [] ->
48+ [ "I cannot find that package though! Maybe there is a typo?"
49+ ]
50+
51+ [suggestion] ->
52+ [ "I cannot find that package though! Maybe you want this one instead?"
53+ , P.indent 4 $ P.dullyellow $ P.text (Pkg.toString suggestion)
54+ ]
55+
56+ _ ->
57+ [ "I cannot find that package though! Maybe you want one of these instead?"
58+ , P.indent 4 $ P.dullyellow $ P.vcat $ map (P.text . Pkg.toString) suggestions
59+ ]
60+ ]
61+
62+ UnknownVersion _pkg vsn realVersions ->
63+ Help.docReport "UNKNOWN VERSION" Nothing
64+ ( P.fillSep $
65+ [ "Version", P.red (P.text (Pkg.versionToString vsn))
66+ , "has", "never", "been", "published,", "so", "I"
67+ , "cannot", "diff", "against", "it."
68+ ]
69+ )
70+ [ "Here are all the versions that HAVE been published:"
71+ , P.indent 4 $ P.dullyellow $ P.vcat $
72+ let
73+ sameMajor v1 v2 = Pkg._major v1 == Pkg._major v2
74+ mkRow vsns = P.hsep $ map (P.text . Pkg.versionToString) vsns
75+ in
76+ map mkRow $ List.groupBy sameMajor (List.sort realVersions)
77+ , "Want one of those instead?"
78+ ]
builder/src/Reporting/Error/Help.hsnew file+149
@@ -0,0 +1,149 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Reporting.Error.Help
4+ ( Report
5+ , report
6+ , docReport
7+ , compilerReport
8+ , reportToDoc
9+ , hintLink
10+ , stack
11+ , reflow
12+ , note
13+ , toString
14+ , toStdout
15+ , toStderr
16+ )
17+ where
18+
19+import qualified Data.List as List
20+import GHC.IO.Handle (hIsTerminalDevice)
21+import System.IO (Handle, hPutStr, stderr, stdout)
22+import qualified Text.PrettyPrint.ANSI.Leijen as P
23+import Text.PrettyPrint.ANSI.Leijen ((<>),(<+>))
24+
25+import qualified Elm.Compiler as Compiler
26+import qualified Elm.Package as Pkg
27+
28+
29+
30+-- REPORT
31+
32+
33+data Report
34+ = FromCompiler P.Doc
35+ | Report
36+ { _title :: String
37+ , _path :: Maybe FilePath
38+ , _start :: P.Doc
39+ , _others :: [P.Doc]
40+ }
41+
42+
43+report :: String -> Maybe FilePath -> String -> [P.Doc] -> Report
44+report title maybePath startString others =
45+ Report title maybePath (P.text startString) others
46+
47+
48+docReport :: String -> Maybe FilePath -> P.Doc -> [P.Doc] -> Report
49+docReport =
50+ Report
51+
52+
53+compilerReport :: P.Doc -> Report
54+compilerReport =
55+ FromCompiler
56+
57+
58+reportToDoc :: Report -> P.Doc
59+reportToDoc report_ =
60+ case report_ of
61+ FromCompiler doc ->
62+ doc
63+
64+ Report title maybePath start others ->
65+ let
66+ makeDashes n =
67+ replicate (max 1 (80 - n)) '-'
68+
69+ errorBarEnd =
70+ case maybePath of
71+ Nothing ->
72+ makeDashes (4 + length title)
73+
74+ Just path ->
75+ makeDashes (5 + length title + length path) ++ " " ++ path
76+
77+ errorBar =
78+ P.dullcyan $
79+ "--" <+> P.text title <+> P.text errorBarEnd
80+ in
81+ stack (errorBar : start : others ++ [""])
82+
83+
84+
85+-- HINT LINKS
86+
87+
88+hintLink :: String -> String
89+hintLink hintName =
90+ "<https://github.com/elm-lang/elm-compiler/blob/"
91+ ++ Pkg.versionToString Compiler.version
92+ ++ "/hints/" ++ hintName ++ ".md>"
93+
94+
95+
96+-- HELPERS
97+
98+
99+stack :: [P.Doc] -> P.Doc
100+stack allDocs =
101+ case allDocs of
102+ [] ->
103+ error "Do not use `stack` on empty lists."
104+
105+ doc : docs ->
106+ List.foldl' verticalAppend doc docs
107+
108+
109+verticalAppend :: P.Doc -> P.Doc -> P.Doc
110+verticalAppend a b =
111+ a <> P.hardline <> P.hardline <> b
112+
113+
114+reflow :: String -> P.Doc
115+reflow paragraph =
116+ P.fillSep (map P.text (words paragraph))
117+
118+
119+note :: String -> P.Doc
120+note details =
121+ P.fillSep $
122+ (P.underline "Note" <> ":") : map P.text (words details)
123+
124+
125+
126+-- OUTPUT
127+
128+
129+toString :: P.Doc -> String
130+toString doc =
131+ P.displayS (P.renderPretty 1 80 (P.plain doc)) ""
132+
133+
134+toStdout :: P.Doc -> IO ()
135+toStdout doc =
136+ toHandle stdout doc
137+
138+
139+toStderr :: P.Doc -> IO ()
140+toStderr doc =
141+ toHandle stderr doc
142+
143+
144+toHandle :: Handle -> P.Doc -> IO ()
145+toHandle handle doc =
146+ do isTerminal <- hIsTerminalDevice handle
147+ if isTerminal
148+ then P.displayIO handle (P.renderPretty 1 80 doc)
149+ else hPutStr handle (toString doc)
builder/src/Reporting/Error/Http.hsnew file+102
@@ -0,0 +1,102 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Reporting.Error.Http
4+ ( Error(..)
5+ , toReport
6+ , BadJson(..)
7+ , badJsonToDocs
8+ )
9+ where
10+
11+
12+import qualified Data.Text as Text
13+import Text.PrettyPrint.ANSI.Leijen ((<>))
14+import qualified Text.PrettyPrint.ANSI.Leijen as P
15+
16+import qualified Reporting.Error.Help as Help
17+
18+
19+
20+-- ERRORS
21+
22+
23+data Error
24+ = Unknown String
25+ | BadJson P.Doc
26+ | BadZipData
27+ | BadZipSha String String
28+
29+
30+
31+-- TO REPORT
32+
33+
34+toReport :: String -> Error -> Help.Report
35+toReport url err =
36+ let
37+ urlDoc =
38+ P.indent 4 $ P.dullyellow $ "<" <> P.text url <> ">"
39+ in
40+ case err of
41+ Unknown message ->
42+ Help.report "HTTP PROBLEM" Nothing "The following HTTP request failed:"
43+ [ urlDoc
44+ , Help.stack
45+ [ "Here is the error message I was able to extract:"
46+ , P.indent 4 $ Help.reflow message
47+ ]
48+ ]
49+
50+ BadJson jsonErrorDoc ->
51+ Help.compilerReport jsonErrorDoc
52+
53+ BadZipData ->
54+ Help.report "CORRUPT ZIP" Nothing "I could not unzip the file downloaded from:"
55+ [ urlDoc
56+ , Help.reflow $
57+ "If it is a transient issue, it should be fixed if you try this\
58+ \ again. If it seems like an Elm problem, please report it though!"
59+ ]
60+
61+ BadZipSha expectedHash actualHash ->
62+ Help.report "CORRUPT ZIP" Nothing "I got an unexpected zip file from:"
63+ [ urlDoc
64+ , Help.reflow $
65+ "I was expecting the hash of content to be " ++ expectedHash
66+ ++ ", but it is " ++ actualHash
67+ , Help.reflow $
68+ "Most likely the package author may have moved the version\
69+ \ tag, so report it to them and see if that is the issue."
70+ ]
71+
72+
73+
74+-- BAD JSON
75+
76+
77+data BadJson
78+ = BadNewPkg Text.Text
79+ | BadAllPkg Text.Text String
80+ | BadAllVsn Text.Text
81+
82+
83+badJsonToDocs :: BadJson -> [P.Doc]
84+badJsonToDocs badJson =
85+ case badJson of
86+ BadNewPkg txt ->
87+ ["I","ran","into",P.red (P.text (show txt)) <> ","
88+ ,"but","I","need","entries","like"
89+ ,P.green "\"elm-lang/core@6.0.0\"" <> "."
90+ ]
91+
92+ BadAllPkg txt suggestion ->
93+ ["The",P.red (P.text (show txt)),"value","is","not","a","valid","package","name."
94+ ]
95+ ++ map P.text (words suggestion)
96+
97+ BadAllVsn txt ->
98+ ["You","provided",P.red (P.text (show txt))
99+ ,"which","is","not","a","valid","version.","I","need","something","like"
100+ ,P.green "\"1.0.0\"","or",P.green "\"2.0.4\"" <> "."
101+ ]
102+
builder/src/Reporting/Error/Publish.hsnew file+243
@@ -0,0 +1,243 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Reporting.Error.Publish
4+ ( Error(..)
5+ , toReport
6+ )
7+ where
8+
9+
10+import qualified Text.PrettyPrint.ANSI.Leijen as P
11+import Text.PrettyPrint.ANSI.Leijen ((<>))
12+
13+import Deps.Diff (Magnitude, magnitudeToString)
14+import qualified Elm.Package as Pkg
15+import qualified Reporting.Error.Help as Help
16+
17+
18+
19+-- ERRORS
20+
21+
22+data Error
23+ = Application
24+ | NotInitialVersion Pkg.Version
25+ | AlreadyPublished Pkg.Version
26+ | InvalidBump Pkg.Version Pkg.Version
27+ | BadBump Pkg.Version Pkg.Version Magnitude Pkg.Version Magnitude
28+ | NoSummary
29+ | NoExposed
30+ | NoReadme
31+ | ShortReadme
32+ | NoLicense
33+ | MissingTag Pkg.Version
34+ | NoGit
35+ | LocalChanges Pkg.Version
36+
37+
38+
39+-- TO REPORT
40+
41+
42+toReport :: Error -> Help.Report
43+toReport err =
44+ case err of
45+ Application ->
46+ Help.report "UNPUBLISHABLE" Nothing "I cannot publish applications, only packages!" []
47+
48+ NotInitialVersion vsn ->
49+ Help.docReport "INVALID VERSION" Nothing
50+ ( P.fillSep
51+ ["I","cannot","publish"
52+ ,P.red (P.text (Pkg.versionToString vsn))
53+ ,"as","the","initial","version."
54+ ]
55+ )
56+ [ P.fillSep
57+ ["Change","it","to",P.green "1.0.0","which","is"
58+ ,"the","initial","version","for","all","Elm","packages."
59+ ]
60+ ]
61+
62+ AlreadyPublished vsn ->
63+ Help.docReport "ALREADY PUBLISHED" Nothing
64+ ( P.vcat
65+ [ P.fillSep
66+ [ "Version", P.green (P.text (Pkg.versionToString vsn))
67+ , "has", "already", "been", "published.", "You", "cannot"
68+ , "publish", "it", "again!"
69+ ]
70+ , "Try using the `bump` command:"
71+ ]
72+ )
73+ [ P.dullyellow $ P.indent 4 $ P.text "elm bump"
74+ , Help.reflow $
75+ "It computes the version number based on API changes, ensuring\
76+ \ that no breaking changes end up in PATCH releases!"
77+ ]
78+
79+ InvalidBump statedVersion latestVersion ->
80+ Help.docReport "INVALID VERSION" (Just "elm.json")
81+ ( P.fillSep $
82+ ["Your","elm.json","says","the","next","version","should","be"
83+ ,P.red (P.text (Pkg.versionToString statedVersion)) <> ","
84+ ,"but","that","is","not","valid","based","on","the","previously"
85+ ,"published","versions."
86+ ]
87+ )
88+ [ P.fillSep $
89+ ["Change","the","version","back","to"
90+ ,P.green (P.text (Pkg.versionToString latestVersion))
91+ ,"which","is","the","most","recently","published","version."
92+ ,"From","there,","have","Elm","bump","the","version","by","running:"
93+ ]
94+ , P.indent 4 $ P.green "elm bump"
95+ , Help.reflow $
96+ "If you want more insight on the API changes Elm detects, you\
97+ \ can run `elm diff` at this point as well."
98+ ]
99+
100+ BadBump old new magnitude realNew realMagnitude ->
101+ Help.docReport "INVALID VERSION" (Just "elm.json")
102+ (
103+ P.fillSep $
104+ ["Your","elm.json","says","the","next","version","should","be"
105+ ,P.red (P.text (Pkg.versionToString new)) <> ","
106+ ,"indicating","a",P.text (magnitudeToString magnitude)
107+ ,"change","to","the","public","API."
108+ ,"This","does","not","match","the","API","diff","given","by:"
109+ ]
110+ )
111+ [ P.indent 4 $ P.text $
112+ "elm diff " ++ Pkg.versionToString old
113+
114+ , P.fillSep $
115+ ["This","command","says","this","is","a"
116+ ,P.text (magnitudeToString realMagnitude)
117+ ,"change,","so","the","next","version","should","be"
118+ ,P.green (P.text (Pkg.versionToString realNew)) <> "."
119+ ,"Double","check","everything","to","make","sure","you"
120+ ,"are","publishing","what","you","want!"
121+ ]
122+ , Help.reflow $
123+ "Also, next time use `elm bump` and I'll figure all this out for you!"
124+ ]
125+
126+ NoSummary ->
127+ Help.docReport "NO SUMMARY" (Just "elm.json")
128+ ( P.fillSep $
129+ [ "To", "publish", "a", "package,", "your", "elm.json", "must"
130+ , "have", "a", P.dullyellow "\"summary\"", "field", "that", "gives"
131+ , "a", "consice", "overview", "of", "your", "project."
132+ ]
133+ )
134+ [ Help.reflow $
135+ "The summary must be less than 80 characters. It should describe\
136+ \ the concrete use of your package as clearly and as plainly as possible."
137+ ]
138+
139+ NoExposed ->
140+ Help.docReport "NO EXPOSED MODULES" (Just "elm.json")
141+ ( P.fillSep $
142+ [ "To", "publish", "a", "package,", "the"
143+ , P.dullyellow "\"exposed-modules\"", "field", "of", "your"
144+ , "elm.json", "must", "list", "at", "least", "one", "module."
145+ ]
146+ )
147+ [ Help.reflow $
148+ "What is the point of a package that has no modules?!"
149+ ]
150+
151+ NoReadme ->
152+ badReadmeReport "NO README" $
153+ "Every published package must have a helpful README.md\
154+ \ file, but I do not see one in your project."
155+
156+ ShortReadme ->
157+ badReadmeReport "SHORT README" $
158+ "This README.md is too short. Having more details will help\
159+ \ people assess your package quickly and fairly."
160+
161+ NoLicense ->
162+ Help.report "NO LICENSE FILE" (Just "LICENSE")
163+ "By publishing a package you are inviting the Elm community to build\
164+ \ upon your work. But without knowing your license, we have no idea if\
165+ \ that is legal!"
166+ [ Help.reflow $
167+ "Once you pick an OSI approved license from <https://spdx.org/licenses/>,\
168+ \ you must share that choice in two places. First, the license\
169+ \ identifier must appear in your elm.json file. Second, the full\
170+ \ license text must appear in the root of your project in a file\
171+ \ named LICENSE. Add that file and you will be all set!"
172+ ]
173+
174+ MissingTag version ->
175+ let vsn = Pkg.versionToString version in
176+ Help.docReport "NO TAG" Nothing
177+ ( P.fillSep $
178+ [ "Packages", "must", "be", "tagged", "in", "git,", "but", "I"
179+ , "cannot", "find", "a", P.green (P.text vsn), "tag."
180+ ]
181+ )
182+ [ P.vcat
183+ [ "These tags make it possible to find this specific version on GitHub."
184+ , "To tag the most recent commit and push it to GitHub, run this:"
185+ ]
186+ , P.indent 4 $ P.dullyellow $ P.vcat $ map P.text $
187+ [ "git tag -a " ++ vsn ++ " -m \"release version " ++ vsn ++ "\""
188+ , "git push origin " ++ vsn
189+ ]
190+ , "The -m flag is for a helpful message. Try to make it more informative!"
191+ ]
192+
193+ NoGit ->
194+ Help.report "NO GIT" Nothing
195+ "I searched your PATH environment variable for `git` and could not\
196+ \ find it. Is it available through your PATH?"
197+ [ Help.reflow $
198+ "Who cares about this? Well, I currently use `git` to check if there\
199+ \ are any local changes in your code. Local changes are a good sign\
200+ \ that some important improvements have gotten mistagged, so this\
201+ \ check can be extremely helpful for package authors!"
202+ , Help.note $
203+ "We plan to do this without the `git` binary in a future release."
204+ ]
205+
206+ LocalChanges version ->
207+ let vsn = Pkg.versionToString version in
208+ Help.docReport "LOCAL CHANGES" Nothing
209+ ( P.fillSep $
210+ [ "The", "code", "tagged", "as", P.green (P.text vsn), "in"
211+ , "git", "does", "not", "match", "the", "code", "in", "your"
212+ , "working", "directory.", "This", "means", "you", "have"
213+ , "commits", "or", "local", "changes", "that", "are", "not"
214+ , "going", "to", "be", "published!"
215+ ]
216+ )
217+ [ Help.note $
218+ "If you are sure everything is in order, you can run `git checkout "
219+ ++ vsn ++ "` and publish your code from there."
220+ ]
221+
222+
223+badReadmeReport :: String -> String -> Help.Report
224+badReadmeReport title summary =
225+ Help.report title (Just "README.md") summary
226+ [ Help.reflow $
227+ "When people look at your README, they are wondering:"
228+ , P.vcat
229+ [ " - What does this package even do?"
230+ , " - Will it help me solve MY problems?"
231+ ]
232+ , Help.reflow $
233+ "So I recommend starting your README with a small example of the\
234+ \ most common usage scenario. Show people what they can expect if\
235+ \ they learn more!"
236+ , Help.note $
237+ "By publishing your package, you are inviting people to invest time\
238+ \ in understanding your work. Spending an hour to communicate your\
239+ \ knowledge more clearly can save the community days or weeks of time\
240+ \ in aggregate, and saving time in aggregate is the whole point of\
241+ \ publishing packages! People really appreciate it, and it makes the\
242+ \ whole ecosystem feel nicer!"
243+ ]
builder/src/Reporting/Progress.hsnew file+98
@@ -0,0 +1,98 @@
1+{-# OPTIONS_GHC -Wall #-}
2+module Reporting.Progress
3+ ( Reporter(..)
4+ , makeReporter
5+ , Msg(..)
6+ , Progress(..)
7+ , Outcome(..)
8+ , PublishPhase(..)
9+ , BumpPhase(..)
10+ )
11+ where
12+
13+
14+import Control.Concurrent.Chan (Chan, writeChan)
15+import Control.Concurrent.MVar (MVar, readMVar)
16+import qualified Elm.Compiler.Module as Module
17+import Deps.Diff (Magnitude)
18+import Elm.Package (Name, Version)
19+import Reporting.Error (Error)
20+
21+
22+
23+-- REPORTER
24+
25+
26+data Reporter =
27+ Reporter
28+ { _tell :: Progress -> IO ()
29+ , _end :: Maybe Error -> IO ()
30+ }
31+
32+
33+makeReporter :: Chan Msg -> MVar () -> Reporter
34+makeReporter chan mvar =
35+ let
36+ tell progress =
37+ writeChan chan (Progress progress)
38+
39+ end maybeError =
40+ do writeChan chan (End maybeError)
41+ readMVar mvar
42+ in
43+ Reporter tell end
44+
45+
46+
47+-- MESSAGES
48+
49+
50+data Msg = Progress Progress | End (Maybe Error)
51+
52+
53+data Progress
54+ -- download packages
55+ = DownloadSkip
56+ | DownloadStart [(Name, Version)]
57+ | DownloadPkgStart Name Version
58+ | DownloadPkgEnd Name Version Outcome
59+ | DownloadEnd Outcome
60+
61+ -- build dependencies
62+ | BuildDepsStart Int
63+ | BuildDepsProgress
64+ | BuildDepsEnd
65+
66+ -- compile files
67+ | CompileStart Int
68+ | CompileFileStart Module.Raw
69+ | CompileFileEnd Module.Raw Outcome
70+ | CompileEnd
71+
72+ -- publish
73+ | PublishStart Name Version (Maybe [Version])
74+ | PublishCheckBump Version BumpPhase
75+ | PublishProgress PublishPhase (Maybe Outcome)
76+ | PublishEnd
77+
78+ -- solver
79+ | UnableToLoadLatestPackages
80+
81+
82+data Outcome = Good | Bad
83+
84+
85+data PublishPhase
86+ = CheckReadme
87+ | CheckLicense
88+ | CheckTag Version
89+ | CheckDownload
90+ | CheckBuild
91+ | CheckChanges
92+
93+
94+data BumpPhase
95+ = StatedVersion
96+ | GoodStart
97+ | GoodBump Version Magnitude
98+ | BadBump
builder/src/Reporting/Progress/Bar.hsnew file+43
@@ -0,0 +1,43 @@
1+{-# OPTIONS_GHC -Wall #-}
2+module Reporting.Progress.Bar
3+ ( render
4+ , clear
5+ )
6+ where
7+
8+
9+
10+-- PROGRESS BAR
11+
12+
13+width :: Float
14+width =
15+ 50.0
16+
17+
18+render :: Int -> Int -> Int -> String
19+render successes failures total =
20+ let
21+ numDone =
22+ successes + failures
23+
24+ fraction =
25+ fromIntegral numDone / fromIntegral total
26+
27+ unitsDone =
28+ truncate (fraction * width)
29+
30+ unitsLeft =
31+ truncate width - unitsDone
32+ in
33+ "\r["
34+ ++ replicate unitsDone '='
35+ ++ replicate unitsLeft ' '
36+ ++ "] - "
37+ ++ show numDone ++ " / " ++ show total
38+
39+
40+clear :: String
41+clear =
42+ '\r' : replicate (length (render 49999 50000 99999)) ' ' ++ "\r"
43+
builder/src/Reporting/Progress/Repl.hsnew file+45
@@ -0,0 +1,45 @@
1+{-# OPTIONS_GHC -Wall #-}
2+module Reporting.Progress.Repl
3+ ( create
4+ )
5+ where
6+
7+
8+import Control.Concurrent (forkIO)
9+import Control.Concurrent.Chan (Chan, newChan, readChan)
10+import Control.Concurrent.MVar (newEmptyMVar, putMVar)
11+
12+import qualified Reporting.Error as Error
13+import qualified Reporting.Progress as Progress
14+
15+
16+
17+-- CREATE
18+
19+
20+create :: IO Progress.Reporter
21+create =
22+ do chan <- newChan
23+ mvar <- newEmptyMVar
24+ _ <- forkIO (loop chan >>= putMVar mvar)
25+ return (Progress.makeReporter chan mvar)
26+
27+
28+
29+-- LOOP
30+
31+
32+loop :: Chan Progress.Msg -> IO ()
33+loop chan =
34+ do msg <- readChan chan
35+ case msg of
36+ Progress.Progress _ ->
37+ loop chan
38+
39+ Progress.End maybeError ->
40+ case maybeError of
41+ Just err ->
42+ Error.toStderr err
43+
44+ Nothing ->
45+ return ()
builder/src/Reporting/Progress/Terminal.hsnew file+363
@@ -0,0 +1,363 @@
1+{-# OPTIONS_GHC -Wall #-}
2+module Reporting.Progress.Terminal
3+ ( create
4+ , newPackageOverview
5+ )
6+ where
7+
8+
9+import Control.Concurrent (forkIO)
10+import Control.Concurrent.Chan (Chan, newChan, readChan)
11+import Control.Concurrent.MVar (newEmptyMVar, putMVar)
12+import qualified System.Info as System
13+import System.IO (hFlush, hPutStr, stdout)
14+import Text.PrettyPrint.ANSI.Leijen ((<>), (<+>))
15+import qualified Text.PrettyPrint.ANSI.Leijen as P
16+import qualified Elm.Package as Pkg
17+import Elm.Package (Name, Version)
18+
19+import qualified Deps.Diff as Diff
20+import qualified Reporting.Error as Error
21+import qualified Reporting.Error.Help as Help
22+import Reporting.Progress (Msg(..), Progress(..), Outcome(..), PublishPhase(..), BumpPhase(..))
23+import qualified Reporting.Progress as Progress
24+import qualified Reporting.Progress.Bar as Bar
25+
26+
27+
28+-- CREATE
29+
30+
31+create :: IO Progress.Reporter
32+create =
33+ do chan <- newChan
34+ mvar <- newEmptyMVar
35+ _ <- forkIO (loop chan (State 0 0 0) >>= putMVar mvar)
36+ return (Progress.makeReporter chan mvar)
37+
38+
39+
40+-- STATE
41+
42+
43+data State =
44+ State
45+ { _total :: !Int
46+ , _good :: !Int
47+ , _bad :: !Int
48+ }
49+
50+
51+
52+-- LOOP
53+
54+
55+loop :: Chan Msg -> State -> IO ()
56+loop chan state =
57+ do msg <- readChan chan
58+ case msg of
59+ End (Just err) ->
60+ Error.toStderr err
61+
62+ End Nothing ->
63+ return ()
64+
65+ Progress progress ->
66+ loopHelp chan progress state
67+
68+
69+loopHelp :: Chan Msg -> Progress -> State -> IO ()
70+loopHelp chan progress state@(State total good bad) =
71+ case progress of
72+
73+
74+ -- DOWNLOADS
75+
76+ DownloadSkip ->
77+ do putStrLn "Dependencies loaded from local cache."
78+ loop chan state
79+
80+ DownloadStart [] ->
81+ loop chan state
82+
83+ DownloadStart _ ->
84+ do putStrLn "Starting downloads...\n"
85+ loop chan state
86+
87+ DownloadPkgStart _name _version ->
88+ loop chan state
89+
90+ DownloadPkgEnd name version outcome ->
91+ do Help.toStdout (makeBullet name version outcome)
92+ loop chan state
93+
94+ DownloadEnd Bad ->
95+ do putStrLn ""
96+ loop chan state
97+
98+ DownloadEnd Good ->
99+ do putStrLn ""
100+ loop chan state
101+
102+
103+ -- BUILD DEPS
104+
105+ BuildDepsStart size ->
106+ do hPutStr stdout "Verifying dependencies..."
107+ hFlush stdout
108+ loop chan (State size 0 0)
109+
110+ BuildDepsProgress ->
111+ do let n = good + 1
112+ let msg = "\rBuilding dependencies (" ++ show n ++ "/" ++ show total ++ ")"
113+ hPutStr stdout msg
114+ hFlush stdout
115+ loop chan (State total n 0)
116+
117+ BuildDepsEnd ->
118+ do putStrLn "\rDependencies ready! "
119+ loop chan (State 0 0 0)
120+
121+
122+ -- COMPILE
123+
124+ CompileStart size ->
125+ loop chan (State size 0 0)
126+
127+ CompileFileStart _ ->
128+ loop chan state
129+
130+ CompileFileEnd _ Good ->
131+ do hPutStr stdout $ Bar.render (good + 1) bad total
132+ hFlush stdout
133+ loop chan (State total (good + 1) bad)
134+
135+ CompileFileEnd _ Bad ->
136+ do hPutStr stdout $ Bar.render good (bad + 1) total
137+ hFlush stdout
138+ loop chan (State total good (bad + 1))
139+
140+ CompileEnd ->
141+ let
142+ message =
143+ case (bad, total) of
144+ (0, 0) -> "Success!"
145+ (0, 1) -> "Success! Compiled 1 module."
146+ (0, n) -> "Success! Compiled " ++ show n ++ " modules."
147+ (1, _) -> "Detected errors in 1 module."
148+ (n, _) -> "Detected errors in " ++ show n ++ " modules."
149+ in
150+ do hPutStr stdout $ Bar.clear
151+ putStrLn message
152+ loop chan (State 0 0 0)
153+
154+
155+ -- PUBLISH
156+
157+ PublishStart name version maybePublishedVersions ->
158+ case maybePublishedVersions of
159+ Nothing ->
160+ do putStrLn newPackageOverview
161+ putStrLn "I will now verify that everything is in order..."
162+ putStrLn ""
163+ loop chan state
164+
165+ Just _ ->
166+ do putStrLn $ unwords [ "Verifying", Pkg.toString name, Pkg.versionToString version, "..." ]
167+ putStrLn ""
168+ loop chan state
169+
170+ PublishCheckBump version bumpPhase ->
171+ do Help.toStdout $ bumpPhaseToChecklistDoc version bumpPhase
172+ hFlush stdout
173+ loop chan state
174+
175+ PublishProgress phase status ->
176+ do Help.toStdout $ toChecklistDoc status (toChecklistMessages phase)
177+ hFlush stdout
178+ loop chan state
179+
180+ PublishEnd ->
181+ do putStrLn "Success!"
182+ loop chan state
183+
184+
185+ -- SOLVER
186+
187+ UnableToLoadLatestPackages ->
188+ do putStrLn ""
189+ Help.toStdout $ P.dullyellow (P.text "WARNING:") <+> P.text "I normally check <https://package.elm-lang.org> for new packages\n"
190+ putStrLn "here, but my request failed. Are you offline? I will try to continue anyway.\n"
191+ loop chan state
192+
193+
194+
195+-- BULLETS
196+
197+
198+makeBullet :: Name -> Version -> Outcome -> P.Doc
199+makeBullet name version outcome =
200+ let
201+ nm =
202+ P.text (Pkg.toString name)
203+
204+ vsn =
205+ P.text (Pkg.versionToString version)
206+
207+ bullet =
208+ case outcome of
209+ Good -> goodMark
210+ Bad -> badMark
211+ in
212+ P.indent 2 $ bullet <+> nm <+> vsn <> P.text "\n"
213+
214+
215+goodMark :: P.Doc
216+goodMark =
217+ P.green $ P.text $
218+ if System.os == "windows" then "+" else "●"
219+
220+
221+badMark :: P.Doc
222+badMark =
223+ P.red $ P.text $
224+ if System.os == "windows" then "X" else "✗"
225+
226+
227+waitingMark :: P.Doc
228+waitingMark =
229+ P.dullyellow $ P.text $
230+ if System.os == "windows" then "-" else "→"
231+
232+
233+
234+-- OVERVIEW OF VERSIONING
235+
236+
237+newPackageOverview :: String
238+newPackageOverview =
239+ unlines
240+ [ "This package has never been published before. Here's how things work:"
241+ , ""
242+ , " - Versions all have exactly three parts: MAJOR.MINOR.PATCH"
243+ , ""
244+ , " - All packages start with initial version " ++ Pkg.versionToString Pkg.initialVersion
245+ , ""
246+ , " - Versions are incremented based on how the API changes:"
247+ , ""
248+ , " PATCH = the API is the same, no risk of breaking code"
249+ , " MINOR = values have been added, existing values are unchanged"
250+ , " MAJOR = existing values have been changed or removed"
251+ , ""
252+ , " - I will bump versions for you, automatically enforcing these rules"
253+ , ""
254+ ]
255+
256+
257+
258+-- CHECKLIST
259+
260+
261+toChecklistDoc :: Maybe Outcome -> ChecklistMessages -> P.Doc
262+toChecklistDoc status (ChecklistMessages waiting success failure) =
263+ let
264+ padded message =
265+ message ++ replicate (length waiting - length message) ' '
266+ in
267+ case status of
268+ Nothing ->
269+ P.text " " <> waitingMark <+> P.text waiting
270+
271+ Just Good ->
272+ P.text "\r " <> goodMark <+> P.text (padded success ++ "\n")
273+
274+ Just Bad ->
275+ P.text "\r " <> badMark <+> P.text (padded failure ++ "\n\n")
276+
277+
278+data ChecklistMessages =
279+ ChecklistMessages
280+ { _waiting :: String
281+ , _success :: String
282+ , _failure :: String
283+ }
284+
285+
286+toChecklistMessages :: PublishPhase -> ChecklistMessages
287+toChecklistMessages phase =
288+ case phase of
289+ CheckReadme ->
290+ ChecklistMessages
291+ "Looking for README.md"
292+ "Found README.md"
293+ "Problem with your README.md"
294+
295+ CheckLicense ->
296+ ChecklistMessages
297+ "Looking for LICENSE"
298+ "Found LICENSE"
299+ "Problem with your LICENSE"
300+
301+ CheckTag version ->
302+ ChecklistMessages
303+ ("Is version " ++ Pkg.versionToString version ++ " tagged on GitHub?")
304+ ("Version " ++ Pkg.versionToString version ++ " is tagged on GitHub")
305+ ("Version " ++ Pkg.versionToString version ++ " is not tagged on GitHub!")
306+
307+ CheckDownload ->
308+ ChecklistMessages
309+ "Downloading code from GitHub..."
310+ "Code downloaded successfully from GitHub"
311+ "Could not download code from GitHub!"
312+
313+ CheckBuild ->
314+ ChecklistMessages
315+ "Building downloaded code and generated docs..."
316+ "Downloaded code compiles successfully / docs generated\n"
317+ "Cannot compile downloaded code!"
318+
319+ CheckChanges ->
320+ ChecklistMessages
321+ "Checking for uncommitted changes..."
322+ "No uncommitted changes in local code"
323+ "Your local code is different than the code tagged on GitHub"
324+
325+
326+
327+-- BUMP PHASE
328+
329+
330+bumpPhaseToChecklistDoc :: Pkg.Version -> BumpPhase -> P.Doc
331+bumpPhaseToChecklistDoc version bumpPhase =
332+ let
333+ mkMsgs success =
334+ ChecklistMessages
335+ ("Checking semantic versioning rules. Is " ++ Pkg.versionToString version ++ " correct?")
336+ success
337+ ("Version " ++ Pkg.versionToString version ++ " is not correct!")
338+ in
339+
340+ case bumpPhase of
341+ StatedVersion ->
342+ toChecklistDoc Nothing $ mkMsgs ""
343+
344+ GoodStart ->
345+ toChecklistDoc (Just Good) $ mkMsgs $
346+ "All packages start at version " ++ Pkg.versionToString Pkg.initialVersion
347+
348+ GoodBump old magnitude ->
349+ toChecklistDoc (Just Good) $ mkMsgs $
350+ toCheckVersionSuccessMessage old version magnitude
351+
352+ BadBump ->
353+ toChecklistDoc (Just Bad) $ mkMsgs ""
354+
355+
356+toCheckVersionSuccessMessage :: Pkg.Version -> Pkg.Version -> Diff.Magnitude -> String
357+toCheckVersionSuccessMessage oldVersion newVersion magnitude =
358+ let
359+ old = Pkg.versionToString oldVersion
360+ new = Pkg.versionToString newVersion
361+ mag = Diff.magnitudeToString magnitude
362+ in
363+ "Version number " ++ new ++ " verified (" ++ mag ++ " change, " ++ old ++ " => " ++ new ++ ")"
builder/src/Reporting/Task.hsnew file+191
@@ -0,0 +1,191 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Reporting.Task
4+ ( Task, Task_, run, throw, mapError
5+ , Env
6+ , getPackageCacheDir
7+ , getPackageCacheDirFor
8+ , report
9+ , getReporter
10+ , withApproval
11+ , getApproval
12+ , silently
13+ , getSilentRunner
14+ , workerChan
15+ , runHttp
16+ )
17+ where
18+
19+import Control.Concurrent (forkIO)
20+import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan)
21+import Control.Monad (forever, join, replicateM_)
22+import Control.Monad.Except (ExceptT, runExceptT, throwError, withExceptT)
23+import Control.Monad.Reader (ReaderT, runReaderT, ask, asks)
24+import Control.Monad.Trans (liftIO)
25+import qualified Network.HTTP.Client as Http
26+import qualified Network.HTTP.Client.TLS as Http
27+import System.Directory (createDirectoryIfMissing)
28+import System.FilePath ((</>))
29+import qualified System.IO as IO
30+
31+import Elm.Package (Name, Version)
32+import qualified Elm.Package as Pkg
33+import qualified Elm.PerUserCache as PerUserCache
34+import qualified Reporting.Error as Error
35+import qualified Reporting.Progress as Progress
36+
37+
38+
39+-- TASKS
40+
41+
42+type Task =
43+ Task_ Error.Error
44+
45+
46+type Task_ e =
47+ ExceptT e (ReaderT Env IO)
48+
49+
50+data Env =
51+ Env
52+ { _cacheDir :: FilePath
53+ , _worker :: IO () -> IO ()
54+ , _httpManager :: Http.Manager
55+ , _tell :: Progress.Progress -> IO ()
56+ }
57+
58+
59+run :: Progress.Reporter -> Task a -> IO (Maybe a)
60+run (Progress.Reporter tell end) task =
61+ do root <- PerUserCache.getPackageRoot
62+ pool <- initPool 4
63+ httpManager <- Http.newManager Http.tlsManagerSettings
64+ let env = Env root pool httpManager tell
65+ result <- runReaderT (runExceptT task) env
66+ case result of
67+ Left err ->
68+ do end (Just err)
69+ return Nothing
70+
71+ Right answer ->
72+ do end Nothing
73+ return (Just answer)
74+
75+
76+throw :: e -> Task_ e a
77+throw =
78+ throwError
79+
80+
81+mapError :: (x -> y) -> Task_ x a -> Task_ y a
82+mapError =
83+ withExceptT
84+
85+
86+
87+-- CACHING
88+
89+
90+getPackageCacheDir :: Task_ e FilePath
91+getPackageCacheDir =
92+ asks _cacheDir
93+
94+
95+getPackageCacheDirFor :: Name -> Version -> Task_ e FilePath
96+getPackageCacheDirFor name version =
97+ do cacheDir <- getPackageCacheDir
98+ let dir = cacheDir </> Pkg.toFilePath name </> Pkg.versionToString version
99+ liftIO (createDirectoryIfMissing True dir)
100+ return dir
101+
102+
103+
104+-- REPORTER
105+
106+
107+report :: Progress.Progress -> Task_ e ()
108+report progress =
109+ do tell <- asks _tell
110+ liftIO (tell progress)
111+
112+
113+getReporter :: Task (Progress.Progress -> IO ())
114+getReporter =
115+ asks _tell
116+
117+
118+
119+-- APPROVAL
120+
121+
122+withApproval :: Task_ e () -> Task_ e ()
123+withApproval task =
124+ do approval <- getApproval
125+ if approval then task else return ()
126+
127+
128+getApproval :: Task_ e Bool
129+getApproval =
130+ do liftIO $ IO.hFlush IO.stdout
131+ input <- liftIO getLine
132+ case input of
133+ "" -> return True
134+ "Y" -> return True
135+ "y" -> return True
136+ "n" -> return False
137+ _ ->
138+ do liftIO $ putStr "Must type 'y' for yes or 'n' for no: "
139+ getApproval
140+
141+
142+
143+-- RUNNER
144+
145+
146+silently :: Task_ e a -> Task_ e a
147+silently task =
148+ do runner <- getSilentRunner
149+ result <- liftIO (runner task)
150+ either throw return result
151+
152+
153+getSilentRunner :: Task_ x (Task_ e a -> IO (Either e a))
154+getSilentRunner =
155+ do env <- ask
156+ let silentEnv = env { _tell = \_ -> return () }
157+ return $ \task -> runReaderT (runExceptT task) silentEnv
158+
159+
160+
161+-- THREAD POOL
162+
163+
164+workerChan :: Chan (Either e a) -> Task_ e a -> Task_ x ()
165+workerChan chan task =
166+ do env <- ask
167+
168+ liftIO $ _worker env $
169+ do result <- runReaderT (runExceptT task) env
170+ writeChan chan result
171+
172+
173+initPool :: Int -> IO (IO () -> IO ())
174+initPool size =
175+ do chan <- newChan
176+
177+ replicateM_ size $ forkIO $ forever $
178+ join (readChan chan)
179+
180+ return $ writeChan chan
181+
182+
183+
184+-- HTTP
185+
186+
187+runHttp :: (Http.Manager -> (Progress.Progress -> IO ()) -> IO (Either Error.Error a)) -> Task a
188+runHttp fetcher =
189+ do (Env _ _ manager tell) <- ask
190+ result <- liftIO $ fetcher manager tell
191+ either throwError return result
builder/src/Reporting/Task/Http.hsnew file+185
@@ -0,0 +1,185 @@
1+{-# OPTIONS_GHC -Wall #-}
2+{-# LANGUAGE GADTs #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+module Reporting.Task.Http
5+ ( Fetch
6+ , Handler
7+ , run
8+ , package
9+ , anything
10+ , andThen
11+ , parallel
12+ , report
13+ )
14+ where
15+
16+
17+import Control.Concurrent (forkIO)
18+import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan)
19+import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, readMVar)
20+import Control.Exception (Exception, SomeException, catch)
21+import Control.Monad (forever, join, replicateM_, void)
22+import qualified Network.HTTP as Http (urlEncodeVars)
23+import qualified Network.HTTP.Client as Http
24+
25+import qualified Elm.Compiler as Compiler
26+import qualified Elm.Package as Pkg
27+import qualified Reporting.Error as Error
28+import qualified Reporting.Error.Http as E
29+import qualified Reporting.Progress as Progress
30+import qualified Reporting.Task as Task
31+
32+
33+
34+-- FETCH
35+
36+
37+data Fetch a where
38+ Package :: String -> [(String,String)] -> Handler a -> Fetch a
39+ Anything :: String -> Handler a -> Fetch a
40+ AndThen :: Fetch a -> (a -> Fetch b) -> Fetch b
41+ Parallel :: [Fetch a] -> Fetch [a]
42+ Report :: Progress.Progress -> (Progress.Outcome -> Progress.Progress) -> Fetch a -> Fetch a
43+
44+
45+type Handler a =
46+ Http.Request -> Http.Manager -> IO (Either E.Error a)
47+
48+
49+package :: String -> [(String,String)] -> Handler a -> Fetch a
50+package =
51+ Package
52+
53+
54+anything :: String -> Handler a -> Fetch a
55+anything =
56+ Anything
57+
58+
59+andThen :: Fetch a -> (a -> Fetch b) -> Fetch b
60+andThen =
61+ AndThen
62+
63+
64+parallel :: [Fetch a] -> Fetch [a]
65+parallel =
66+ Parallel
67+
68+
69+report :: Progress.Progress -> (Progress.Outcome -> Progress.Progress) -> Fetch a -> Fetch a
70+report =
71+ Report
72+
73+
74+
75+-- RUN
76+
77+
78+run :: Fetch a -> Task.Task a
79+run fetch =
80+ Task.runHttp $ \manager tell ->
81+ do chan <- newChan
82+ replicateM_ 4 $ forkIO $ forever $ join (readChan chan)
83+ readMVar =<< runHelp chan manager tell fetch
84+
85+
86+runHelp :: Chan (IO ()) -> Http.Manager -> (Progress.Progress -> IO ()) -> Fetch a -> IO (MVar (Either Error.Error a))
87+runHelp chan manager tell fetch =
88+ case fetch of
89+ Package path params handler ->
90+ do mvar <- newEmptyMVar
91+ let url = makePackageUrl path params
92+ writeChan chan $ putMVar mvar =<< fetchSafe url manager handler
93+ return mvar
94+
95+ Anything url handler ->
96+ do mvar <- newEmptyMVar
97+ writeChan chan $ putMVar mvar =<< fetchSafe url manager handler
98+ return mvar
99+
100+ AndThen subFetch callback ->
101+ do subMVar <- runHelp chan manager tell subFetch
102+ mvar <- newEmptyMVar
103+ void $ forkIO $
104+ do result <- readMVar subMVar
105+ putMVar mvar =<<
106+ case result of
107+ Left err ->
108+ return (Left err)
109+ Right value ->
110+ readMVar =<< runHelp chan manager tell (callback value)
111+ return mvar
112+
113+ Parallel fetches ->
114+ do mvars <- mapM (runHelp chan manager tell) fetches
115+ mvar <- newEmptyMVar
116+ void $ forkIO $
117+ do results <- mapM readMVar mvars
118+ putMVar mvar (sequence results)
119+ return mvar
120+
121+ Report start toEnd subFetch ->
122+ do tell start
123+ subMVar <- runHelp chan manager tell subFetch
124+ mvar <- newEmptyMVar
125+ void $ forkIO $
126+ do result <- readMVar subMVar
127+ tell $ toEnd $
128+ either (\_ -> Progress.Bad) (\_ -> Progress.Good) result
129+ putMVar mvar result
130+ return mvar
131+
132+
133+
134+-- PACKAGE URLS
135+
136+
137+packageDomain :: String
138+packageDomain =
139+ "http://localhost:8000"
140+
141+
142+makePackageUrl :: String -> [(String,String)] -> String
143+makePackageUrl path params =
144+ let
145+ query =
146+ if null params then
147+ ""
148+ else
149+ "?" ++ Http.urlEncodeVars (versionParam : params)
150+ in
151+ packageDomain ++ "/" ++ path ++ query
152+
153+
154+versionParam :: (String, String)
155+versionParam =
156+ ( "elm-package-version"
157+ , Pkg.versionToString Compiler.version
158+ )
159+
160+
161+
162+-- HTTP HELP
163+
164+
165+fetchSafe :: String -> Http.Manager -> Handler a -> IO (Either Error.Error a)
166+fetchSafe url manager handler =
167+ fetchUnsafe url manager handler
168+ `catch` \e -> handleAnyError url (e :: SomeException)
169+
170+
171+fetchUnsafe :: String -> Http.Manager -> Handler a -> IO (Either Error.Error a)
172+fetchUnsafe url manager handler =
173+ do request <- Http.parseUrlThrow url
174+ result <- handler request manager
175+ case result of
176+ Right value ->
177+ return (Right value)
178+
179+ Left problem ->
180+ return (Left (Error.BadHttp url problem))
181+
182+
183+handleAnyError :: (Exception e) => String -> e -> IO (Either Error.Error a)
184+handleAnyError url exception =
185+ return $ Left $ Error.BadHttp url $ E.Unknown $ show exception
builder/src/Stuff/Paths.hsnew file+93
@@ -0,0 +1,93 @@
1+{-# OPTIONS_GHC -Wall #-}
2+module Stuff.Paths
3+ ( docs
4+ , solution
5+ , summary
6+ , prepublishDir
7+ , removeStuff
8+ , elmi
9+ , elmo
10+ , temp
11+ )
12+ where
13+
14+
15+import Control.Monad.Trans (liftIO)
16+import qualified System.Directory as Dir
17+import System.FilePath ((</>), (<.>))
18+
19+import qualified Elm.Compiler as Compiler
20+import qualified Elm.Compiler.Module as Module
21+import qualified Elm.Package as Pkg
22+import qualified Reporting.Task as Task
23+
24+
25+
26+-- PATHS
27+
28+
29+stuff :: FilePath
30+stuff =
31+ "elm-stuff" </> Pkg.versionToString Compiler.version
32+
33+
34+docs :: FilePath
35+docs =
36+ stuff </> "docs.json"
37+
38+
39+solution :: FilePath
40+solution =
41+ stuff </> "solution.dat"
42+
43+
44+summary :: FilePath
45+summary =
46+ stuff </> "summary.dat"
47+
48+
49+prepublishDir :: FilePath
50+prepublishDir =
51+ stuff </> "prepublish"
52+
53+
54+
55+-- REMOVE STUFF
56+
57+
58+removeStuff :: FilePath -> Task.Task_ e ()
59+removeStuff root =
60+ liftIO $
61+ do let dir = root </> "elm-stuff"
62+ exists <- Dir.doesDirectoryExist dir
63+ if exists
64+ then Dir.removeDirectoryRecursive dir
65+ else return ()
66+
67+
68+
69+-- ELMI and ELMO
70+
71+
72+elmi :: FilePath -> Module.Raw -> FilePath
73+elmi root name =
74+ toArtifactPath root name "elmi"
75+
76+
77+elmo :: FilePath -> Module.Raw -> FilePath
78+elmo root name =
79+ toArtifactPath root name "elmo"
80+
81+
82+toArtifactPath :: FilePath -> Module.Raw -> String -> FilePath
83+toArtifactPath root name ext =
84+ root </> stuff </> Module.nameToHyphenPath name <.> ext
85+
86+
87+
88+-- TEMP
89+
90+
91+temp :: FilePath
92+temp =
93+ stuff </> "temp.js"
builder/src/Stuff/Verify.hsnew file+96
@@ -0,0 +1,96 @@
1+{-# OPTIONS_GHC -Wall #-}
2+module Stuff.Verify
3+ ( verify
4+ )
5+ where
6+
7+
8+import Prelude hiding (read)
9+import qualified Data.Map as Map
10+import Data.Map (Map)
11+
12+import Elm.Package (Name, Version)
13+
14+import qualified Deps.Verify as Verify
15+import Elm.Project.Json (Project(..), PkgInfo(..))
16+import qualified Elm.Project.Json as Project
17+import qualified Elm.Project.Constraint as Con
18+import qualified Elm.Project.Summary as Summary
19+import qualified File.IO as IO
20+import qualified Reporting.Task as Task
21+import qualified Stuff.Paths as Path
22+
23+
24+
25+-- VERIFY
26+
27+
28+verify :: FilePath -> Project -> Task.Task Summary.Summary
29+verify root project =
30+ do fresh <-
31+ IO.andM
32+ [ IO.exists Path.solution
33+ , IO.exists Path.summary
34+ , checkSolution project
35+ ]
36+
37+ if fresh
38+ then read root project
39+ else rebuildCache root project
40+
41+
42+rebuildCache :: FilePath -> Project -> Task.Task Summary.Summary
43+rebuildCache root project =
44+ do IO.remove Path.solution
45+ IO.remove Path.summary
46+
47+ (solution, summary) <- Verify.verify root project
48+
49+ IO.writeBinary Path.solution solution
50+ write summary
51+ return summary
52+
53+
54+
55+-- READ / WRITE
56+
57+
58+read :: FilePath -> Project -> Task.Task Summary.Summary
59+read root project =
60+ do (exposed, ifaces, graph) <- IO.readBinary Path.summary
61+ return (Summary.Summary root project exposed ifaces graph)
62+
63+
64+write :: Summary.Summary -> Task.Task ()
65+write (Summary.Summary _ _ exposed ifaces graph) =
66+ IO.writeBinary Path.summary ( exposed, ifaces, graph )
67+
68+
69+
70+-- CHECK SOLUTION
71+
72+
73+checkSolution :: Project -> Task.Task Bool
74+checkSolution project =
75+ checkSolutionHelp project <$> IO.readBinary Path.solution
76+
77+
78+checkSolutionHelp :: Project -> Map Name Version -> Bool
79+checkSolutionHelp project solution =
80+ case project of
81+ App info ->
82+ solution == Project.appSolution info
83+
84+ Pkg info ->
85+ allGood (_pkg_test_deps info) solution
86+ && allGood (_pkg_deps info) solution
87+
88+
89+allGood :: Map Name Con.Constraint -> Map Name Version -> Bool
90+allGood cons vsns =
91+ let
92+ bools =
93+ Map.intersectionWith Con.satisfies cons vsns
94+ in
95+ Map.size cons == Map.size bools
96+ && Map.foldr (&&) True bools