forked from commercialhaskell/stack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSetup.hs
More file actions
575 lines (529 loc) · 22 KB
/
Setup.hs
File metadata and controls
575 lines (529 loc) · 22 KB
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
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE DeriveDataTypeable #-}
module Stack.Setup
( setupEnv
, ensureGHC
, SetupOpts (..)
) where
import Control.Applicative
import Control.Exception (Exception)
import Data.Maybe (mapMaybe, catMaybes)
import Control.Monad (liftM, when)
import Control.Monad.Catch (MonadThrow, throwM, MonadMask)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Reader (MonadReader, ReaderT (..), asks)
import Data.Conduit (($$))
import qualified Data.Conduit.List as CL
import Data.Aeson
import Data.IORef
import Data.Monoid
import qualified Data.Yaml as Yaml
import Data.Text (Text)
import Data.Typeable (Typeable)
import Network.HTTP.Client.Conduit
import Path
import Path.IO
import System.Directory
import System.Exit (ExitCode (ExitSuccess))
import System.FilePath (searchPathSeparator)
import System.IO.Temp (withSystemTempDirectory)
import System.Process (rawSystem)
import Stack.GhcPkg (getGlobalDB)
import Stack.Types
import Distribution.System (OS (..), Arch (..), Platform (..))
import Stack.Build.Types
import qualified Data.ByteString.Char8 as S8
import Control.Monad.Logger
import qualified Data.Text as T
import Data.List (intercalate)
import qualified Data.Map as Map
import Data.Map (Map)
import qualified Data.Set as Set
import Data.Set (Set)
import System.Process.Read
import qualified System.FilePath as FP
import Network.HTTP.Download (download)
import Prelude -- Fix AMP warning
data SetupOpts = SetupOpts
{ soptsInstallIfMissing :: !Bool
, soptsUseSystem :: !Bool
, soptsExpected :: !Version
, soptsStackYaml :: !(Maybe (Path Abs File))
-- ^ If we got the desired GHC version from that file
, soptsForceReinstall :: !Bool
}
deriving Show
data SetupException = UnsupportedSetupCombo OS Arch
| MissingDependencies [String]
| UnknownGHCVersion Version (Set MajorVersion)
| UnknownOSKey Text
deriving Typeable
instance Exception SetupException
instance Show SetupException where
show (UnsupportedSetupCombo os arch) = concat
[ "I don't know how to install GHC for "
, show (os, arch)
, ", please install manually"
]
show (MissingDependencies tools) =
"The following executables are missing and must be installed:" ++
intercalate ", " tools
show (UnknownGHCVersion version known) = concat
[ "No information found for GHC version "
, versionString version
, ". Known GHC major versions: "
, intercalate ", " (map show $ Set.toList known)
]
show (UnknownOSKey oskey) =
"Unable to find installation URLs for OS key: " ++
T.unpack oskey
-- | Modify the environment variables (like PATH) appropriately, possibly doing installation too
setupEnv :: (MonadIO m, MonadMask m, MonadLogger m, MonadReader env m, HasBuildConfig env, HasHttpManager env)
=> Bool -- ^ allow system GHC
-> Bool -- ^ install if missing?
-> m BuildConfig
setupEnv useSystem installIfMissing = do
bconfig <- asks getBuildConfig
let platform = getPlatform bconfig
sopts = SetupOpts
{ soptsInstallIfMissing = installIfMissing
, soptsUseSystem = useSystem
, soptsExpected = bcGhcVersion bconfig
, soptsStackYaml = Just $ bcStackYaml bconfig
, soptsForceReinstall = False
}
mghcBin <- ensureGHC sopts
menv0 <- getMinimalEnvOverride
-- Modify the initial environment to include the GHC path, if a local GHC
-- is being used
let env0 = case mghcBin of
Nothing -> unEnvOverride menv0
Just ghcBin ->
let x = unEnvOverride menv0
mpath = Map.lookup "PATH" x
path = T.intercalate (T.singleton searchPathSeparator)
$ map T.pack ghcBin ++ maybe [] return mpath
in Map.insert "PATH" path x
-- Remove potentially confusing environment variables
env1 = Map.delete "GHC_PACKAGE_PATH"
$ Map.delete "HASKELL_PACKAGE_SANDBOX"
$ Map.delete "HASKELL_PACKAGE_SANDBOXES"
env0
-- extra installation bin directories
mkDirs <- runReaderT extraBinDirs bconfig
let mpath = Map.lookup "PATH" env1
depsPath = mkPath (mkDirs False) mpath
localsPath = mkPath (mkDirs True) mpath
deps <- runReaderT packageDatabaseDeps bconfig
depsExists <- liftIO $ doesDirectoryExist $ toFilePath deps
localdb <- runReaderT packageDatabaseLocal bconfig
localdbExists <- liftIO $ doesDirectoryExist $ toFilePath localdb
globalDB <- mkEnvOverride platform env1 >>= getGlobalDB
let mkGPP locals = T.pack $ intercalate [searchPathSeparator] $ concat
[ [toFilePath localdb | locals && localdbExists]
, [toFilePath deps | depsExists]
, [toFilePath globalDB]
]
envRef <- liftIO $ newIORef Map.empty
let getEnvOverride' es = do
m <- readIORef envRef
case Map.lookup es m of
Just eo -> return eo
Nothing -> do
eo <- mkEnvOverride platform
$ Map.insert "PATH" (if esIncludeLocals es then localsPath else depsPath)
$ (if esIncludeGhcPackagePath es
then Map.insert "GHC_PACKAGE_PATH" (mkGPP (esIncludeLocals es))
else id)
-- For reasoning and duplication, see: https://github.com/fpco/stack/issues/70
$ Map.insert "HASKELL_PACKAGE_SANDBOX" (T.pack $ toFilePath deps)
$ Map.insert "HASKELL_PACKAGE_SANDBOXES"
(T.pack $ if esIncludeLocals es
then intercalate [searchPathSeparator]
[ toFilePath localdb
, toFilePath deps
, ""
]
else intercalate [searchPathSeparator]
[ toFilePath deps
, ""
])
$ env1
!() <- atomicModifyIORef envRef $ \m' ->
(Map.insert es eo m', ())
return eo
return bconfig { bcConfig = (bcConfig bconfig) { configEnvOverride = getEnvOverride' } }
where
mkPath dirs mpath = T.pack $ intercalate [searchPathSeparator]
(map toFilePath dirs ++ maybe [] (return . T.unpack) mpath)
-- | Ensure GHC is installed and provide the PATHs to add if necessary
ensureGHC :: (MonadIO m, MonadMask m, MonadLogger m, MonadReader env m, HasConfig env, HasHttpManager env)
=> SetupOpts
-> m (Maybe [FilePath])
ensureGHC sopts = do
-- Check the available GHCs
menv0 <- getMinimalEnvOverride
msystem <-
if soptsUseSystem sopts
then getSystemGHC menv0
else return Nothing
let needLocal = case msystem of
Nothing -> True
Just system ->
-- we allow a newer version of GHC within the same major series
getMajorVersion system /= getMajorVersion expected ||
expected > system
-- If we need to install a GHC, try to do so
if needLocal
then do
config <- asks getConfig
let tools =
case configPlatform config of
Platform _ Windows ->
[ ($(mkPackageName "ghc"), Just expected)
, ($(mkPackageName "git"), Nothing)
]
_ ->
[ ($(mkPackageName "ghc"), Just expected)
]
-- Avoid having to load it twice
siRef <- liftIO $ newIORef Nothing
manager <- asks getHttpManager
let getSetupInfo' = liftIO $ do
msi <- readIORef siRef
case msi of
Just si -> return si
Nothing -> do
si <- getSetupInfo manager
writeIORef siRef $ Just si
return si
installed <- runReaderT listInstalled config
idents <- mapM (ensureTool sopts installed getSetupInfo' msystem) tools
paths <- runReaderT (mapM binDirs $ catMaybes idents) config
-- TODO: strip the trailing slash for prettier PATH output
return $ Just $ map toFilePath $ concat paths
else return Nothing
where
expected = soptsExpected sopts
-- | Get the major version of the system GHC, if available
getSystemGHC :: (MonadIO m) => EnvOverride -> m (Maybe Version)
getSystemGHC menv = do
exists <- doesExecutableExist menv "ghc"
if exists
then do
eres <- liftIO $ tryProcessStdout menv "ghc" ["--numeric-version"]
return $ do
Right bs <- Just eres
parseVersion $ S8.takeWhile isValidChar bs
else return Nothing
where
isValidChar '.' = True
isValidChar c = '0' <= c && c <= '9'
data DownloadPair = DownloadPair Version Text
deriving Show
instance FromJSON DownloadPair where
parseJSON = withObject "DownloadPair" $ \o -> DownloadPair
<$> o .: "version"
<*> o .: "url"
data SetupInfo = SetupInfo
{ siSevenzExe :: Text
, siSevenzDll :: Text
, siPortableGit :: DownloadPair
, siGHCs :: Map Text (Map MajorVersion DownloadPair)
}
deriving Show
instance FromJSON SetupInfo where
parseJSON = withObject "SetupInfo" $ \o -> SetupInfo
<$> o .: "sevenzexe"
<*> o .: "sevenzdll"
<*> o .: "portable-git"
<*> o .: "ghc"
-- | Download the most recent SetupInfo
getSetupInfo :: (MonadIO m, MonadThrow m) => Manager -> m SetupInfo
getSetupInfo manager = do
bss <- liftIO $ flip runReaderT manager
$ withResponse req $ \res -> responseBody res $$ CL.consume
let bs = S8.concat bss
either throwM return $ Yaml.decodeEither' bs
where
req = "https://raw.githubusercontent.com/fpco/stackage-content/master/stack/stack-setup.yaml"
markInstalled :: (MonadIO m, MonadReader env m, HasConfig env, MonadThrow m)
=> PackageIdentifier -- ^ e.g., ghc-7.8.4, git-2.4.0.1
-> m ()
markInstalled ident = do
dir <- asks $ configLocalPrograms . getConfig
fpRel <- parseRelFile $ packageIdentifierString ident ++ ".installed"
liftIO $ writeFile (toFilePath $ dir </> fpRel) "installed"
unmarkInstalled :: (MonadIO m, MonadReader env m, HasConfig env, MonadThrow m)
=> PackageIdentifier
-> m ()
unmarkInstalled ident = do
dir <- asks $ configLocalPrograms . getConfig
fpRel <- parseRelFile $ packageIdentifierString ident ++ ".installed"
removeFileIfExists $ dir </> fpRel
listInstalled :: (MonadIO m, MonadReader env m, HasConfig env, MonadThrow m)
=> m [PackageIdentifier]
listInstalled = do
dir <- asks $ configLocalPrograms . getConfig
liftIO $ createDirectoryIfMissing True $ toFilePath dir
(_, files) <- listDirectory dir
return $ mapMaybe toIdent files
where
toIdent fp = do
x <- T.stripSuffix ".installed" $ T.pack $ toFilePath $ filename fp
parsePackageIdentifierFromString $ T.unpack x
installDir :: (MonadReader env m, HasConfig env, MonadThrow m, MonadLogger m)
=> PackageIdentifier
-> m (Path Abs Dir)
installDir ident = do
config <- asks getConfig
reldir <- parseRelDir $ packageIdentifierString ident
return $ configLocalPrograms config </> reldir
-- | Binary directories for the given installed package
binDirs :: (MonadReader env m, HasConfig env, MonadThrow m, MonadLogger m)
=> PackageIdentifier
-> m [Path Abs Dir]
binDirs ident = do
config <- asks getConfig
dir <- installDir ident
case (configPlatform config, packageNameString $ packageIdentifierName ident) of
(Platform _ Windows, "ghc") -> return
[ dir </> $(mkRelDir "bin")
, dir </> $(mkRelDir "mingw") </> $(mkRelDir "bin")
]
(Platform _ Windows, "git") -> return
[ dir </> $(mkRelDir "cmd")
, dir </> $(mkRelDir "usr") </> $(mkRelDir "bin")
]
(_, "ghc") -> return
[ dir </> $(mkRelDir "bin")
]
(Platform _ x, tool) -> do
$logWarn $ "binDirs: unexpected OS/tool combo: " <> T.pack (show (x, tool))
return []
ensureTool :: (MonadIO m, MonadMask m, MonadLogger m, MonadReader env m, HasConfig env, HasHttpManager env)
=> SetupOpts
-> [PackageIdentifier] -- ^ already installed
-> m SetupInfo
-> Maybe Version -- ^ installed GHC
-> (PackageName, Maybe Version)
-> m (Maybe PackageIdentifier)
ensureTool sopts installed getSetupInfo' msystem (name, mversion)
| not $ null available = return $ Just $ PackageIdentifier name $ maximum available
| not $ soptsInstallIfMissing sopts =
if name == $(mkPackageName "ghc")
then throwM $ GHCVersionMismatch msystem (soptsExpected sopts) (soptsStackYaml sopts)
else do
$logWarn $ "Continuing despite missing tool: " <> T.pack (packageNameString name)
return Nothing
| otherwise = do
si <- getSetupInfo'
(pair@(DownloadPair version _), installer) <-
case packageNameString name of
"git" -> do
let pair = siPortableGit si
return (pair, installGitWindows)
"ghc" -> do
osKey <- getOSKey
pairs <-
case Map.lookup osKey $ siGHCs si of
Nothing -> throwM $ UnknownOSKey osKey
Just pairs -> return pairs
version <-
case mversion of
Nothing -> error "invariant violated: ghc must have a version"
Just version -> return version
pair <-
case Map.lookup (getMajorVersion version) pairs of
Nothing -> throwM $ UnknownGHCVersion version (Map.keysSet pairs)
Just pair -> return pair
platform <- asks $ configPlatform . getConfig
let installer =
case platform of
Platform _ Windows -> installGHCWindows
_ -> installGHCPosix
return (pair, installer)
x -> error $ "Invariant violated: ensureTool on " ++ x
let ident = PackageIdentifier name version
(file, at) <- downloadPair pair ident
dir <- installDir ident
unmarkInstalled ident
installer si file at dir ident
markInstalled ident
return $ Just ident
where
available
| soptsForceReinstall sopts = []
| otherwise = filter goodVersion
$ map packageIdentifierVersion
$ filter (\pi' -> packageIdentifierName pi' == name) installed
goodVersion =
case mversion of
Nothing -> const True
Just expected -> \actual ->
getMajorVersion expected == getMajorVersion actual &&
actual >= expected
getOSKey :: (MonadReader env m, MonadThrow m, HasConfig env) => m Text
getOSKey = do
platform <- asks $ configPlatform . getConfig
case platform of
Platform I386 Linux -> return "linux32"
Platform X86_64 Linux -> return "linux64"
Platform I386 OSX -> return "macosx"
Platform X86_64 OSX -> return "macosx"
Platform I386 FreeBSD -> return "freebsd32"
Platform X86_64 FreeBSD -> return "freebsd64"
Platform I386 Windows -> return "windows32"
-- Note: we always use 32-bit Windows as the 64-bit version has problems
Platform X86_64 Windows -> return "windows32"
Platform arch os -> throwM $ UnsupportedSetupCombo os arch
downloadPair :: (MonadIO m, MonadMask m, MonadLogger m, MonadReader env m, HasConfig env, HasHttpManager env)
=> DownloadPair
-> PackageIdentifier
-> m (Path Abs File, ArchiveType)
downloadPair (DownloadPair _ url) ident = do
config <- asks getConfig
at <-
case extension of
".tar.xz" -> return TarXz
".tar.bz2" -> return TarBz2
".7z.exe" -> return SevenZ
_ -> error $ "Unknown extension: " ++ extension
relfile <- parseRelFile $ packageIdentifierString ident ++ extension
let path = configLocalPrograms config </> relfile
chattyDownload url path
return (path, at)
where
extension =
loop $ T.unpack url
where
loop fp
| ext `elem` [".tar", ".bz2", ".xz", ".exe", ".7z"] = loop fp' ++ ext
| otherwise = ""
where
(fp', ext) = FP.splitExtension fp
data ArchiveType
= TarBz2
| TarXz
| SevenZ
installGHCPosix :: (MonadIO m, MonadMask m, MonadLogger m, MonadReader env m, HasConfig env, HasHttpManager env)
=> SetupInfo
-> Path Abs File
-> ArchiveType
-> Path Abs Dir
-> PackageIdentifier
-> m ()
installGHCPosix _ archiveFile archiveType destDir ident = do
menv <- getMinimalEnvOverride
zipTool <-
case archiveType of
TarXz -> return "xz"
TarBz2 -> return "bzip2"
SevenZ -> error "Don't know how to deal with .7z files on non-Windows"
checkDependencies $ zipTool : ["make", "tar"]
withSystemTempDirectory "stack-setup" $ \root' -> do
root <- parseAbsDir root'
dir <- liftM (root Path.</>) $ parseRelDir $ packageIdentifierString ident
$logInfo $ "Unpacking " <> T.pack (toFilePath archiveFile)
runIn root "tar" menv ["xf", toFilePath archiveFile] Nothing
$logInfo "Configuring"
runIn dir (toFilePath $ dir Path.</> $(mkRelFile "configure"))
menv ["--prefix=" ++ toFilePath destDir] Nothing
$logInfo "Installing"
runIn dir "make" menv ["install"] Nothing
$logInfo $ "GHC installed to " <> T.pack (toFilePath destDir)
where
-- | Check if given processes appear to be present, throwing an exception if
-- missing.
checkDependencies :: (MonadIO m, MonadThrow m, MonadReader env m, HasConfig env)
=> [String] -> m ()
checkDependencies tools = do
menv <- getMinimalEnvOverride
missing <- liftM catMaybes $ mapM (check menv) tools
if null missing
then return ()
else throwM $ MissingDependencies missing
where
check menv tool = do
exists <- doesExecutableExist menv tool
return $ if exists then Nothing else Just tool
installGHCWindows :: (MonadIO m, MonadMask m, MonadLogger m, MonadReader env m, HasConfig env, HasHttpManager env)
=> SetupInfo
-> Path Abs File
-> ArchiveType
-> Path Abs Dir
-> PackageIdentifier
-> m ()
installGHCWindows si archiveFile archiveType destDir _ = do
case archiveType of
TarXz -> return ()
_ -> error $ "GHC on Windows must be a .tar.xz file"
tarFile <-
case T.stripSuffix ".xz" $ T.pack $ toFilePath archiveFile of
Nothing -> error $ "Invalid GHC filename: " ++ show archiveFile
Just x -> parseAbsFile $ T.unpack x
config <- asks getConfig
run7z <- setup7z si config
run7z (parent archiveFile) archiveFile
run7z (parent archiveFile) tarFile
$logInfo $ "GHC installed to " <> T.pack (toFilePath destDir)
installGitWindows :: (MonadIO m, MonadMask m, MonadLogger m, MonadReader env m, HasConfig env, HasHttpManager env)
=> SetupInfo
-> Path Abs File
-> ArchiveType
-> Path Abs Dir
-> PackageIdentifier
-> m ()
installGitWindows si archiveFile archiveType destDir _ = do
case archiveType of
SevenZ -> return ()
_ -> error $ "Git on Windows must be a 7z archive"
config <- asks getConfig
run7z <- setup7z si config
run7z destDir archiveFile
-- | Download 7z as necessary, and get a function for unpacking things.
--
-- Returned function takes an unpack directory and archive.
setup7z :: (MonadReader env m, HasHttpManager env, MonadThrow m, MonadIO m, MonadIO n, MonadLogger m)
=> SetupInfo
-> Config
-> m (Path Abs Dir -> Path Abs File -> n ())
setup7z si config = do
chattyDownload (siSevenzDll si) dll
chattyDownload (siSevenzExe si) exe
return $ \outdir archive -> liftIO $ do
ec <- rawSystem (toFilePath exe)
[ "x"
, "-o" ++ toFilePath outdir
, "-y"
, toFilePath archive
]
when (ec /= ExitSuccess)
$ error $ "Problem while decompressing " ++ toFilePath archive
where
dir = configLocalPrograms config </> $(mkRelDir "7z")
exe = dir </> $(mkRelFile "7z.exe")
dll = dir </> $(mkRelFile "7z.dll")
chattyDownload :: (MonadReader env m, HasHttpManager env, MonadIO m, MonadLogger m, MonadThrow m)
=> Text -- ^ URL
-> Path Abs File -- ^ destination
-> m ()
chattyDownload url path = do
req <- parseUrl $ T.unpack url
$logInfo $ T.concat
[ "Downloading from "
, url
, " to "
, T.pack $ toFilePath path
]
x <- download req path -- TODO add progress indicator
if x
then $logInfo "Download complete"
else $logInfo "File already downloaded"