-
Notifications
You must be signed in to change notification settings - Fork 131
/
Copy pathBuild.hs
480 lines (413 loc) Β· 19.6 KB
/
Build.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
module Spago.Build
( build
, test
, run
, repl
, bundleApp
, bundleModule
, docs
, search
, script
) where
import Spago.Prelude hiding (link)
import Spago.Env
import qualified Crypto.Hash as Hash
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Data.Text as Text
import System.Directory (getCurrentDirectory)
import System.FilePath (splitDirectories)
import qualified System.FilePath.Glob as Glob
import qualified System.IO as Sys
import qualified System.IO.Temp as Temp
import qualified System.IO.Utf8 as Utf8
import qualified Turtle
import qualified System.Process as Process
import qualified Web.Browser as Browser
import qualified Spago.Command.Path as Path
import qualified Spago.RunEnv as Run
import qualified Spago.Config as Config
import qualified Spago.FetchPackage as Fetch
import qualified Spago.Messages as Messages
import qualified Spago.Packages as Packages
import qualified Spago.Purs as Purs
import qualified Spago.Templates as Templates
import qualified Spago.Watch as Watch
prepareBundleDefaults
:: Maybe ModuleName
-> Maybe TargetPath
-> (ModuleName, TargetPath)
prepareBundleDefaults maybeModuleName maybeTargetPath = (moduleName, targetPath)
where
moduleName = fromMaybe (ModuleName "Main") maybeModuleName
targetPath = fromMaybe (TargetPath "index.js") maybeTargetPath
-- eventually running some other action after the build
build :: HasBuildEnv env => Maybe (RIO Env ()) -> RIO env ()
build maybePostBuild = do
logDebug "Running `spago build`"
BuildOptions{..} <- view (the @BuildOptions)
Config{..} <- view (the @Config)
deps <- Packages.getProjectDeps
let partitionedGlobs@(Packages.Globs{..}) = Packages.getGlobs deps depsOnly (toList configSourcePaths)
allPsGlobs = Packages.getGlobsSourcePaths partitionedGlobs <> sourcePaths
allJsGlobs = Packages.getJsGlobs deps depsOnly (toList configSourcePaths) <> sourcePaths
checkImports = do
maybeGraph <- view (the @Graph)
for_ maybeGraph $ \(Purs.ModuleGraph moduleGraph) -> do
let
matchesGlob :: Sys.FilePath -> SourcePath -> Bool
matchesGlob path sourcePath =
Glob.match (Glob.compile (Text.unpack (unSourcePath sourcePath))) path
isProjectFile :: Sys.FilePath -> Bool
isProjectFile path =
any (matchesGlob path) (fromMaybe [] projectGlobs)
projectModules :: [ModuleName]
projectModules =
map fst
$ filter (\(_, Purs.ModuleGraphNode{..}) -> isProjectFile (Text.unpack graphNodePath))
$ Map.toList moduleGraph
getImports :: ModuleName -> Set ModuleName
getImports = maybe Set.empty (Set.fromList . graphNodeDepends) . flip Map.lookup moduleGraph
-- All package modules that are imported from our project files
importedPackageModules :: Set ModuleName
importedPackageModules =
Set.difference
(foldMap getImports projectModules)
(Set.fromList projectModules)
getPackageFromPath :: Text -> Maybe PackageName
getPackageFromPath path =
fmap fst
$ find (\(_, sourcePath) -> matchesGlob (Text.unpack path) sourcePath)
$ Map.toList depsGlobs
defaultPackages :: Set PackageName
defaultPackages = Set.singleton (PackageName "psci-support")
importedPackages :: Set PackageName
importedPackages =
Set.fromList
$ mapMaybe (getPackageFromPath . graphNodePath <=< flip Map.lookup moduleGraph)
$ Set.toList importedPackageModules
dependencyPackages :: Set PackageName
dependencyPackages = dependencies
let
unusedPackages =
fmap packageName
$ Set.toList
$ Set.difference dependencyPackages
$ Set.union defaultPackages importedPackages
transitivePackages =
fmap packageName
$ Set.toList
$ Set.difference importedPackages dependencyPackages
unless (null unusedPackages || depsOnly == DepsOnly) $ do
logWarn $ display $ Messages.unusedDependency unusedPackages
unless (null transitivePackages) $ do
die [ display $ Messages.sourceImportsTransitiveDependency transitivePackages ]
buildBackend globs = do
case alternateBackend of
Nothing ->
Purs.compile globs pursArgs
Just backend -> do
when (isJust $ Purs.findFlag 'g' "codegen" pursArgs) $
die
[ "Can't pass `--codegen` option to build when using a backend"
, "Hint: No need to pass `--codegen corefn` explicitly when using the `backend` option."
, "Remove the argument to solve the error"
]
Purs.compile globs $ pursArgs ++ [ PursArg "--codegen", PursArg "corefn" ]
logDebug $ display $ "Compiling with backend \"" <> backend <> "\""
let backendCmd = backend -- In future there will be some arguments here
logDebug $ "Running command `" <> display backendCmd <> "`"
shell backendCmd empty >>= \case
ExitSuccess -> pure ()
ExitFailure n -> die [ "Backend " <> displayShow backend <> " exited with error:" <> repr n ]
checkImports
buildAction globs = do
env <- Run.getEnv
let action = buildBackend globs >> (runRIO env $ fromMaybe (pure ()) maybePostBuild)
runCommands "Before" beforeCommands
action `onException` (runCommands "Else" elseCommands)
runCommands "Then" thenCommands
case shouldWatch of
BuildOnce -> buildAction allPsGlobs
Watch -> do
(psMatches, psMismatches) <- partitionGlobs $ unwrap <$> allPsGlobs
(jsMatches, jsMismatches) <- partitionGlobs $ unwrap <$> allJsGlobs
case NonEmpty.nonEmpty (psMismatches <> jsMismatches) of
Nothing -> pure ()
Just mismatches -> logWarn $ display $ Messages.globsDoNotMatchWhenWatching $ NonEmpty.nub $ Text.pack <$> mismatches
absolutePSGlobs <- traverse makeAbsolute psMatches
absoluteJSGlobs <- traverse makeAbsolute jsMatches
Watch.watch
(Set.fromAscList $ fmap (Glob.compile . collapse) . removeDotSpago $ absolutePSGlobs <> absoluteJSGlobs)
shouldClear
allowIgnored
(buildAction (wrap <$> psMatches))
where
runCommands :: HasLogFunc env => Text -> [Text] -> RIO env ()
runCommands label = traverse_ runCommand
where
runCommand command = shell command empty >>= \case
ExitSuccess -> pure ()
ExitFailure n -> die [ repr label <> " command failed. exit code: " <> repr n ]
partitionGlobs :: [Sys.FilePath] -> RIO env ([Sys.FilePath], [Sys.FilePath])
partitionGlobs = foldrM go ([],[])
where
go sourcePath (matches, mismatches) = do
let parentDir = Watch.globToParent $ Glob.compile sourcePath
paths <- liftIO $ Glob.glob parentDir
pure $ if null paths
then (matches, parentDir : mismatches)
else (sourcePath : matches, mismatches)
wrap = SourcePath . Text.pack
unwrap = Text.unpack . unSourcePath
removeDotSpago = filter (\glob -> ".spago" `notElem` (splitDirectories glob))
collapse = Turtle.encodeString . Turtle.collapse . Turtle.decodeString
-- | Start a repl
repl
:: (HasEnv env)
=> [PackageName]
-> [SourcePath]
-> [PursArg]
-> Packages.DepsOnly
-> RIO env ()
repl newPackages sourcePaths pursArgs depsOnly = do
logDebug "Running `spago repl`"
purs <- Run.getPurs NoPsa
Config.ensureConfig >>= \case
Right config -> Run.withInstallEnv' (Just config) (replAction purs)
Left err -> do
logDebug err
GlobalCache cacheDir _ <- view (the @GlobalCache)
Temp.withTempDirectory cacheDir "spago-repl-tmp" $ \dir -> do
Turtle.cd (Turtle.decodeString dir)
writeTextFile ".purs-repl" Templates.pursRepl
let dependencies = Set.fromList $ [ PackageName "effect", PackageName "console", PackageName "psci-support" ] <> newPackages
config <- Run.withPursEnv NoPsa $ do
Config.makeTempConfig dependencies Nothing Set.empty Nothing
Run.withInstallEnv' (Just config) (replAction purs)
where
replAction purs = do
Config{..} <- view (the @Config)
deps <- Packages.getProjectDeps
-- we check that psci-support is in the deps, see #550
unless (Set.member (PackageName "psci-support") (Set.fromList (map fst deps))) $ do
die
[ "The package called 'psci-support' needs to be installed for the repl to work properly."
, "Run `spago install psci-support` to add it to your dependencies."
]
let
globs =
Packages.getGlobsSourcePaths $ Packages.getGlobs deps depsOnly (toList $ configSourcePaths <> Set.fromList sourcePaths)
Fetch.fetchPackages deps
runRIO purs $ Purs.repl globs pursArgs
-- | Test the project: compile and run "Test.Main"
-- (or the provided module name) with node
test :: HasBuildEnv env => Maybe ModuleName -> [BackendArg] -> RIO env ()
test maybeModuleName extraArgs = do
logDebug "Running `Spago.Build.test`"
let moduleName = fromMaybe (ModuleName "Test.Main") maybeModuleName
Config.Config { alternateBackend } <- view (the @Config)
-- We check if the test module is included in the build and spit out a nice error if it isn't (see #383)
maybeGraph <- view (the @Graph)
for_ maybeGraph $ \(ModuleGraph moduleMap) -> when (isNothing $ Map.lookup moduleName moduleMap) $
die [ "Module '" <> (display . unModuleName) moduleName <> "' not found! Are you including it in your build?" ]
sourceDir <- Turtle.pwd
let dirs = RunDirectories sourceDir sourceDir
runBackend alternateBackend dirs moduleName (Just "Tests succeeded.") "Tests failed: " extraArgs
-- | Run the project: compile and run "Main"
-- (or the provided module name) with node
run :: HasBuildEnv env => Maybe ModuleName -> [BackendArg] -> RIO env ()
run maybeModuleName extraArgs = do
Config.Config { alternateBackend } <- view (the @Config)
let moduleName = fromMaybe (ModuleName "Main") maybeModuleName
sourceDir <- Turtle.pwd
let dirs = RunDirectories sourceDir sourceDir
runBackend alternateBackend dirs moduleName Nothing "Running failed; " extraArgs
-- | Run the select module as a script: init, compile, and run the provided module
script
:: (HasEnv env)
=> Text
-> Maybe Text
-> [PackageName]
-> ScriptBuildOptions
-> RIO env ()
script modulePath tag packageDeps opts = do
logDebug "Running `spago script`"
absoluteModulePath <- fmap Text.pack (makeAbsolute (Text.unpack modulePath))
currentDir <- Turtle.pwd
-- This is the part where we make sure that the script reuses the same folder
-- if run with the same options more than once. We do that by making a folder
-- in the system temp directory, and naming it with the hash of the script
-- path together with the command options
let sha256 :: String -> String
sha256 = show . (Hash.hash :: ByteString -> Hash.Digest Hash.SHA256) . Turtle.fromString
systemTemp <- liftIO $ Temp.getCanonicalTemporaryDirectory
let stableHash = sha256 (Text.unpack absoluteModulePath <> show tag <> show packageDeps <> show opts)
let scriptDirPath = Turtle.decodeString (systemTemp </> "spago-script-tmp-" <> stableHash)
logDebug $ "Found a system temp directory: " <> displayShow systemTemp
-- We now create and cd into this new temp directory
logWarn $ "Creating semi-temp directory to run the script: " <> displayShow scriptDirPath
Turtle.mktree scriptDirPath
Turtle.cd scriptDirPath
let dependencies = Set.fromList $ [ PackageName "effect", PackageName "console", PackageName "prelude" ] <> packageDeps
config <- Run.withPursEnv NoPsa $ do
Config.makeTempConfig dependencies Nothing (Set.fromList [ SourcePath absoluteModulePath ]) tag
let runDirs :: RunDirectories
runDirs = RunDirectories scriptDirPath currentDir
Run.withBuildEnv' (Just config) NoPsa buildOpts (runAction runDirs)
where
buildOpts = fromScriptOptions defaultBuildOptions opts
runAction dirs = runBackend Nothing dirs (ModuleName "Main") Nothing "Script failed to run; " []
data RunDirectories = RunDirectories { sourceDir :: FilePath, executeDir :: FilePath }
-- | Run the project with node (or the chosen alternate backend):
-- compile and run the provided ModuleName
runBackend
:: HasBuildEnv env
=> Maybe Text
-> RunDirectories
-> ModuleName
-> Maybe Text
-> Text
-> [BackendArg]
-> RIO env ()
runBackend maybeBackend RunDirectories{ sourceDir, executeDir } moduleName maybeSuccessMessage failureMessage extraArgs = do
logDebug $ display $ "Running with backend: " <> fromMaybe "nodejs" maybeBackend
BuildOptions{ pursArgs } <- view (the @BuildOptions)
let postBuild = maybe (nodeAction $ Path.getOutputPath pursArgs) backendAction maybeBackend
build (Just postBuild)
where
fromFilePath = Text.pack . Turtle.encodeString
runJsSource = fromFilePath (sourceDir Turtle.</> ".spago/run.js")
nodeArgs = Text.intercalate " " $ map unBackendArg extraArgs
nodeContents outputPath' =
fold
[ "#!/usr/bin/env node\n\n"
, "require('"
, Text.replace "\\" "/" (fromFilePath sourceDir)
, "/"
, Text.pack outputPath'
, "/"
, unModuleName moduleName
, "').main()"
]
nodeCmd = "node " <> runJsSource <> " " <> nodeArgs
nodeAction outputPath' = do
logDebug $ "Writing " <> displayShow @Text runJsSource
writeTextFile runJsSource (nodeContents outputPath')
void $ chmod executable $ pathFromText runJsSource
-- cd to executeDir in case it isn't the same as sourceDir
logDebug $ "Executing from: " <> displayShow @FilePath executeDir
Turtle.cd executeDir
-- We build a process by hand here because we need to forward the stdin to the backend process
let processWithStdin = (Process.shell (Text.unpack nodeCmd)) { Process.std_in = Process.Inherit }
Turtle.system processWithStdin empty >>= \case
ExitSuccess -> maybe (pure ()) (logInfo . display) maybeSuccessMessage
ExitFailure n -> die [ display failureMessage <> "exit code: " <> repr n ]
backendAction backend = do
let args :: [Text] = ["--run", unModuleName moduleName <> ".main"] <> fmap unBackendArg extraArgs
logDebug $ display $ "Running command `" <> backend <> " " <> Text.unwords args <> "`"
Turtle.proc backend args empty >>= \case
ExitSuccess -> maybe (pure ()) (logInfo . display) maybeSuccessMessage
ExitFailure n -> die [ display failureMessage <> "Backend " <> displayShow backend <> " exited with error:" <> repr n ]
-- | Bundle the project to a js file
bundleApp
:: HasEnv env
=> WithMain
-> Maybe ModuleName
-> Maybe TargetPath
-> NoBuild
-> BuildOptions
-> UsePsa
-> RIO env ()
bundleApp withMain maybeModuleName maybeTargetPath noBuild buildOpts usePsa =
let (moduleName, targetPath) = prepareBundleDefaults maybeModuleName maybeTargetPath
bundleAction = Purs.bundle withMain (withSourceMap buildOpts) moduleName targetPath
in case noBuild of
DoBuild -> Run.withBuildEnv usePsa buildOpts $ build (Just bundleAction)
NoBuild -> Run.getEnv >>= (flip runRIO) bundleAction
-- | Bundle into a CommonJS module
bundleModule
:: HasEnv env
=> Maybe ModuleName
-> Maybe TargetPath
-> NoBuild
-> BuildOptions
-> UsePsa
-> RIO env ()
bundleModule maybeModuleName maybeTargetPath noBuild buildOpts usePsa = do
logDebug "Running `bundleModule`"
let (moduleName, targetPath) = prepareBundleDefaults maybeModuleName maybeTargetPath
jsExport = Text.unpack $ "\nmodule.exports = PS[\""<> unModuleName moduleName <> "\"];"
bundleAction = do
logInfo "Bundling first..."
Purs.bundle WithoutMain (withSourceMap buildOpts) moduleName targetPath
-- Here we append the CommonJS export line at the end of the bundle
try (with
(appendonly $ pathFromText $ unTargetPath targetPath)
(\fileHandle -> Utf8.withHandle fileHandle (Sys.hPutStrLn fileHandle jsExport)))
>>= \case
Right _ -> logInfo $ display $ "Make module succeeded and output file to " <> unTargetPath targetPath
Left (n :: SomeException) -> die [ "Make module failed: " <> repr n ]
case noBuild of
DoBuild -> Run.withBuildEnv usePsa buildOpts $ build (Just bundleAction)
NoBuild -> Run.getEnv >>= (flip runRIO) bundleAction
docsSearchTemplate :: (HasType LogFunc env, HasType PursCmd env) => RIO env Text
docsSearchTemplate = ifM (Purs.hasMinPursVersion "0.14.0")
(pure Templates.docsSearch0011)
(pure Templates.docsSearch0010)
docsSearchAppTemplate :: (HasType LogFunc env, HasType PursCmd env) => RIO env Text
docsSearchAppTemplate = ifM (Purs.hasMinPursVersion "0.14.0")
(pure Templates.docsSearchApp0011)
(pure Templates.docsSearchApp0010)
-- | Generate docs for the `sourcePaths` and run `purescript-docs-search build-index` to patch them.
docs
:: HasBuildEnv env
=> Maybe Purs.DocsFormat
-> NoSearch
-> OpenDocs
-> RIO env ()
docs format noSearch open = do
logDebug "Running `spago docs`"
BuildOptions { sourcePaths, depsOnly } <- view (the @BuildOptions)
Config{..} <- view (the @Config)
deps <- Packages.getProjectDeps
logInfo "Generating documentation for the project. This might take a while..."
Purs.docs docsFormat $ Packages.getGlobsSourcePaths (Packages.getGlobs deps depsOnly (toList configSourcePaths)) <> sourcePaths
when isHTMLFormat $ do
when (noSearch == AddSearch) $ do
logInfo "Making the documentation searchable..."
writeTextFile ".spago/purescript-docs-search" =<< docsSearchTemplate
writeTextFile ".spago/docs-search-app.js" =<< docsSearchAppTemplate
let cmd = "node .spago/purescript-docs-search build-index --package-name " <> surroundQuote name
logDebug $ "Running `" <> display cmd <> "`"
shell cmd empty >>= \case
ExitSuccess -> pure ()
ExitFailure n -> logWarn $ "Failed while trying to make the documentation searchable: " <> repr n
link <- linkToIndexHtml
let linkText = "Link: " <> link
logInfo $ display linkText
when (open == DoOpenDocs) $ do
logInfo "Opening in browser..."
() <$ openLink link
where
docsFormat = fromMaybe Purs.Html format
isHTMLFormat = docsFormat == Purs.Html
linkToIndexHtml = do
currentDir <- liftIO $ Text.pack <$> getCurrentDirectory
return ("file://" <> currentDir <> "/generated-docs/html/index.html")
openLink link = liftIO $ Browser.openBrowser (Text.unpack link)
-- | Start a search REPL.
search :: HasBuildEnv env => RIO env ()
search = do
Config{..} <- view (the @Config)
deps <- Packages.getProjectDeps
logInfo "Building module metadata..."
Purs.compile (Packages.getGlobsSourcePaths (Packages.getGlobs deps Packages.AllSources (toList configSourcePaths)))
[ PursArg "--codegen"
, PursArg "docs"
]
writeTextFile ".spago/purescript-docs-search" =<< docsSearchTemplate
let cmd = "node .spago/purescript-docs-search search --package-name " <> surroundQuote name
logDebug $ "Running `" <> display cmd <> "`"
viewShell $ callCommand $ Text.unpack cmd