ghcup-hs/lib/GHCup.hs

528 lines
16 KiB
Haskell
Raw Normal View History

2020-01-14 21:55:34 +00:00
{-# LANGUAGE DataKinds #-}
2020-02-19 19:54:23 +00:00
{-# LANGUAGE DeriveGeneric #-}
2020-01-14 21:55:34 +00:00
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
2020-02-19 19:54:23 +00:00
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-}
2020-01-14 21:55:34 +00:00
{-# LANGUAGE TemplateHaskell #-}
2020-02-19 19:54:23 +00:00
{-# LANGUAGE TypeFamilies #-}
2020-02-24 13:56:13 +00:00
{-# LANGUAGE TypeApplications #-}
2020-01-14 21:55:34 +00:00
-- TODO: handle SIGTERM, SIGUSR
module GHCup where
2020-01-16 22:27:38 +00:00
2020-03-03 00:59:19 +00:00
import GHCup.Download
import GHCup.Errors
import GHCup.Platform
2020-03-01 00:05:02 +00:00
import GHCup.Types
import GHCup.Types.JSON ( )
2020-03-03 00:59:19 +00:00
import GHCup.Types.Optics
import GHCup.Utils
import GHCup.Utils.File
import GHCup.Utils.Prelude
2020-03-01 00:05:02 +00:00
2020-01-14 21:55:34 +00:00
import Control.Applicative
2020-03-01 00:05:02 +00:00
import Control.Exception.Safe
2020-01-16 22:27:38 +00:00
import Control.Monad
2020-02-24 13:56:13 +00:00
import Control.Monad.Fail ( MonadFail )
2020-02-22 18:21:10 +00:00
import Control.Monad.Logger
2020-03-01 00:05:02 +00:00
import Control.Monad.Reader
2020-02-22 18:21:10 +00:00
import Control.Monad.Trans.Class ( lift )
2020-03-01 01:21:40 +00:00
import Control.Monad.Trans.Resource
hiding ( throwM )
2020-02-18 08:40:01 +00:00
import Data.ByteString ( ByteString )
2020-02-29 23:07:39 +00:00
import Data.Foldable
2020-03-01 00:05:02 +00:00
import Data.List
import Data.Maybe
import Data.String.Interpolate
2020-02-19 19:54:23 +00:00
import Data.String.QQ
2020-01-16 22:27:38 +00:00
import Data.Versions
2020-03-01 00:05:02 +00:00
import Data.Word8
import GHC.IO.Exception
2020-01-14 21:55:34 +00:00
import HPath
import HPath.IO
2020-03-01 00:05:02 +00:00
import Haskus.Utils.Variant.Excepts
2020-01-14 21:55:34 +00:00
import Optics
import Prelude hiding ( abs
2020-01-16 22:27:38 +00:00
, readFile
2020-03-03 00:59:19 +00:00
, writeFile
2020-01-14 21:55:34 +00:00
)
2020-01-17 22:29:16 +00:00
import System.IO.Error
2020-03-03 00:59:19 +00:00
import System.Posix.Env.ByteString ( getEnvironment )
import System.Posix.FilePath ( getSearchPath )
2020-03-01 00:05:02 +00:00
import System.Posix.RawFilePath.Directory.Errors
( hideError )
2020-03-03 00:59:19 +00:00
2020-03-01 00:05:02 +00:00
import qualified Data.ByteString as B
import qualified Data.Map.Strict as Map
import qualified Data.Text.Encoding as E
2020-02-22 18:21:10 +00:00
-------------------------
--[ Tool installation ]--
-------------------------
2020-02-24 13:56:13 +00:00
-- TODO: custom logger intepreter and pretty printing
2020-02-22 18:21:10 +00:00
2020-02-28 23:33:32 +00:00
-- | Install a tool, such as GHC or cabal. This also sets
-- the ghc-x.y.z symlinks and potentially the ghc-x.y.
2020-02-24 13:56:13 +00:00
--
-- This can fail in many ways. You may want to explicitly catch
-- `AlreadyInstalled` to not make it fatal.
2020-02-22 18:21:10 +00:00
installTool :: ( MonadThrow m
, MonadReader Settings m
, MonadLogger m
, MonadCatch m
, MonadIO m
2020-02-24 14:09:38 +00:00
, MonadFail m
2020-03-01 01:21:40 +00:00
, MonadResource m
) -- tmp file
2020-03-03 00:59:19 +00:00
=> BinaryDownloads
-> ToolRequest
2020-02-24 13:56:13 +00:00
-> Maybe PlatformRequest -- ^ if Nothing, looks up current host platform
2020-02-22 18:21:10 +00:00
-> Excepts
2020-02-24 13:56:13 +00:00
'[ AlreadyInstalled
, ArchiveError
, DistroNotFound
2020-02-29 23:07:39 +00:00
, FileDoesNotExistError
, FileError
, JSONError
, NoCompatibleArch
, NoDownload
2020-02-24 14:09:38 +00:00
, NotInstalled
2020-02-29 23:07:39 +00:00
, PlatformResultError
, ProcessError
2020-02-28 23:33:32 +00:00
, URLException
2020-03-03 00:59:19 +00:00
, DigestError
2020-02-24 13:56:13 +00:00
]
2020-02-22 18:21:10 +00:00
m
()
2020-03-03 00:59:19 +00:00
installTool bDls treq mpfReq = do
2020-02-28 23:33:32 +00:00
lift $ $(logDebug) [i|Requested to install: #{treq}|]
2020-02-24 13:56:13 +00:00
alreadyInstalled <- liftIO $ toolAlreadyInstalled treq
when alreadyInstalled $ (throwE $ AlreadyInstalled treq)
Settings {..} <- lift ask
-- download (or use cached version)
2020-03-03 00:59:19 +00:00
dlinfo <- liftE $ getDownloadInfo bDls treq mpfReq
dl <- liftE $ downloadCached dlinfo Nothing
2020-02-24 13:56:13 +00:00
-- unpack
2020-03-03 00:59:19 +00:00
tmpUnpack <- lift withGHCupTmpDir
liftE $ unpackToDir tmpUnpack dl
2020-02-24 13:56:13 +00:00
-- prepare paths
2020-03-03 00:59:19 +00:00
ghcdir <- liftIO $ ghcupGHCDir (view trVersion $ treq)
bindir <- liftIO ghcupBinDir
2020-02-22 18:21:10 +00:00
-- the subdir of the archive where we do the work
2020-03-03 00:59:19 +00:00
let archiveSubdir = maybe tmpUnpack (tmpUnpack </>) (view dlSubdir dlinfo)
2020-02-22 18:21:10 +00:00
case treq of
2020-02-28 23:33:32 +00:00
(ToolRequest GHC ver) -> do
2020-02-24 14:09:38 +00:00
liftE $ installGHC archiveSubdir ghcdir
2020-03-03 00:59:19 +00:00
liftE $ postGHCInstall ver
2020-02-29 23:07:39 +00:00
(ToolRequest Cabal _) -> liftE $ installCabal archiveSubdir bindir
2020-02-22 18:21:10 +00:00
pure ()
2020-02-24 13:56:13 +00:00
toolAlreadyInstalled :: ToolRequest -> IO Bool
2020-02-29 23:07:39 +00:00
toolAlreadyInstalled ToolRequest {..} = case _trTool of
GHC -> ghcInstalled _trVersion
Cabal -> cabalInstalled _trVersion
2020-02-24 13:56:13 +00:00
2020-02-24 14:09:38 +00:00
-- | Install an unpacked GHC distribution. This only deals with the GHC build system and nothing else.
2020-02-22 18:21:10 +00:00
installGHC :: (MonadLogger m, MonadIO m)
2020-02-24 13:56:13 +00:00
=> Path Abs -- ^ Path to the unpacked GHC bindist (where the configure script resides)
2020-02-22 18:21:10 +00:00
-> Path Abs -- ^ Path to install to
-> Excepts '[ProcessError] m ()
installGHC path inst = do
2020-02-28 23:33:32 +00:00
lift $ $(logInfo) [s|Installing GHC|]
2020-02-22 18:21:10 +00:00
lEM $ liftIO $ exec [s|./configure|]
False
2020-03-03 00:59:19 +00:00
[[s|--prefix=|] <> toFilePath inst]
2020-02-22 18:21:10 +00:00
(Just path)
2020-03-03 00:59:19 +00:00
Nothing
lEM $ liftIO $ exec [s|make|] True [[s|install|]] (Just path) Nothing
2020-02-22 18:21:10 +00:00
pure ()
-- | Install an unpacked cabal distribution.
installCabal :: (MonadLogger m, MonadCatch m, MonadIO m)
2020-02-24 13:56:13 +00:00
=> Path Abs -- ^ Path to the unpacked cabal bindist (where the executable resides)
2020-02-22 18:21:10 +00:00
-> Path Abs -- ^ Path to install to
-> Excepts '[FileError] m ()
installCabal path inst = do
2020-02-28 23:33:32 +00:00
lift $ $(logInfo) [s|Installing cabal|]
2020-02-22 18:21:10 +00:00
let cabalFile = [rel|cabal|] :: Path Rel
2020-02-24 14:09:38 +00:00
liftIO $ createDirIfMissing newDirPerms inst
2020-02-24 13:56:13 +00:00
handleIO (throwE . CopyError . show) $ liftIO $ copyFile
(path </> cabalFile)
(inst </> cabalFile)
Overwrite
2020-02-22 18:21:10 +00:00
2020-02-18 08:40:01 +00:00
2020-02-28 23:33:32 +00:00
---------------
--[ Set GHC ]--
---------------
2020-02-24 13:56:13 +00:00
-- | Set GHC symlinks in ~/.ghcup/bin for the requested GHC version. The behavior depends
-- on `SetGHC`:
--
2020-02-28 23:33:32 +00:00
-- * SetGHCOnly: ~/.ghcup/bin/ghc -> ~/.ghcup/ghc/<ver>/bin/ghc
-- * SetGHCMajor: ~/.ghcup/bin/ghc-X.Y -> ~/.ghcup/ghc/<ver>/bin/ghc
-- * SetGHCMinor: ~/.ghcup/bin/ghc-<ver> -> ~/.ghcup/ghc/<ver>/bin/ghc
2020-02-24 13:56:13 +00:00
--
-- Additionally creates a ~/.ghcup/share -> ~/.ghcup/ghc/<ver>/share symlink
-- for `SetGHCOnly` constructor.
setGHC :: (MonadThrow m, MonadFail m, MonadIO m)
=> Version
-> SetGHC
-> Excepts '[NotInstalled] m ()
setGHC ver sghc = do
2020-02-28 23:33:32 +00:00
let verBS = verToBS ver
2020-03-03 00:59:19 +00:00
ghcdir <- liftIO $ ghcupGHCDir ver
2020-02-24 13:56:13 +00:00
-- symlink destination
2020-03-03 00:59:19 +00:00
bindir <- liftIO $ ghcupBinDir
liftIO $ hideError AlreadyExists $ createDirRecursive newDirPerms bindir
when (sghc == SetGHCOnly) $ liftE (delOldSymlinks bindir)
2020-02-24 13:56:13 +00:00
-- for ghc tools (ghc, ghci, haddock, ...)
2020-02-28 23:33:32 +00:00
verfiles <- ghcToolFiles ver
2020-02-29 23:07:39 +00:00
forM_ verfiles $ \file -> do
2020-03-03 00:59:19 +00:00
liftIO $ hideError doesNotExistErrorType $ deleteFile (bindir </> file)
2020-02-24 13:56:13 +00:00
targetFile <- case sghc of
SetGHCOnly -> pure file
SetGHCMajor -> do
2020-02-29 23:07:39 +00:00
major' <-
2020-02-28 23:33:32 +00:00
(\(mj, mi) -> E.encodeUtf8 $ intToText mj <> [s|.|] <> intToText mi)
<$> getGHCMajor ver
2020-02-29 23:07:39 +00:00
parseRel (toFilePath file <> B.singleton _hyphen <> major')
2020-02-24 13:56:13 +00:00
SetGHCMinor -> parseRel (toFilePath file <> B.singleton _hyphen <> verBS)
2020-02-28 23:33:32 +00:00
liftIO $ hideError doesNotExistErrorType $ deleteFile
2020-03-03 00:59:19 +00:00
(bindir </> targetFile)
liftIO $ createSymlink (bindir </> targetFile)
2020-02-28 23:33:32 +00:00
(ghcLinkDestination (toFilePath file) ver)
2020-02-24 13:56:13 +00:00
-- create symlink for share dir
2020-02-28 23:33:32 +00:00
liftIO $ symlinkShareDir ghcdir verBS
2020-02-24 13:56:13 +00:00
pure ()
where
2020-02-28 23:33:32 +00:00
symlinkShareDir :: Path Abs -> ByteString -> IO ()
symlinkShareDir ghcdir verBS = do
destdir <- ghcupBaseDir
case sghc of
SetGHCOnly -> do
let sharedir = [rel|share|] :: Path Rel
let fullsharedir = ghcdir </> sharedir
whenM (doesDirectoryExist fullsharedir) $ do
liftIO $ hideError doesNotExistErrorType $ deleteFile
(destdir </> sharedir)
createSymlink
(destdir </> sharedir)
2020-03-01 00:37:09 +00:00
([s|./ghc/|] <> verBS <> [s|/|] <> toFilePath sharedir)
2020-02-28 23:33:32 +00:00
_ -> pure ()
2020-03-03 00:59:19 +00:00
-- The old tool symlinks might be different (e.g. more) than the
-- requested version. Have to avoid "stray" symlinks.
delOldSymlinks :: forall m
. (MonadThrow m, MonadFail m, MonadIO m)
=> Path Abs
-> Excepts '[] m ()
delOldSymlinks bindir = catchLiftLeft (\NotInstalled{} -> pure ()) $ do
mv <- ghcSet
for_ mv $ \ver' -> do
verfiles <- ghcToolFiles ver'
for_ verfiles $ \f -> liftIO $ deleteFile (bindir </> f)
2020-02-28 23:33:32 +00:00
------------------
--[ List tools ]--
------------------
data ListCriteria = ListInstalled
| ListSet
deriving Show
data ListResult = ListResult
{ lTool :: Tool
, lVer :: Version
, lTag :: [Tag]
, lInstalled :: Bool
, lSet :: Bool
}
deriving Show
2020-03-03 00:59:19 +00:00
availableToolVersions :: BinaryDownloads -> Tool -> [(Version, [Tag])]
2020-02-28 23:33:32 +00:00
availableToolVersions av tool = toListOf
(ix tool % to (fmap (\(v, vi) -> (v, (_viTags vi))) . Map.toList) % folded)
av
2020-03-03 00:59:19 +00:00
listVersions :: BinaryDownloads
-> Maybe Tool
2020-02-28 23:33:32 +00:00
-> Maybe ListCriteria
2020-03-03 00:59:19 +00:00
-> IO [ListResult]
listVersions av lt criteria = case lt of
2020-02-28 23:33:32 +00:00
Just t -> do
filter' <$> forM (availableToolVersions av t) (toListResult t)
Nothing -> do
2020-03-03 00:59:19 +00:00
ghcvers <- listVersions av (Just GHC) criteria
cabalvers <- listVersions av (Just Cabal) criteria
2020-02-28 23:33:32 +00:00
pure (ghcvers <> cabalvers)
where
toListResult :: Tool -> (Version, [Tag]) -> IO ListResult
toListResult t (v, tags) = case t of
GHC -> do
lSet <- fmap (maybe False (== v)) $ ghcSet
lInstalled <- ghcInstalled v
pure ListResult { lVer = v, lTag = tags, lTool = t, .. }
Cabal -> do
lSet <- fmap (== v) $ cabalSet
lInstalled <- cabalInstalled v
pure ListResult { lVer = v, lTag = tags, lTool = t, .. }
filter' :: [ListResult] -> [ListResult]
filter' lr = case criteria of
Nothing -> lr
Just ListInstalled -> filter (\ListResult {..} -> lInstalled) lr
Just ListSet -> filter (\ListResult {..} -> lSet) lr
2020-02-29 23:07:39 +00:00
--------------
--[ GHC rm ]--
--------------
2020-02-28 23:33:32 +00:00
-- | This function may throw and crash in various ways.
rmGHCVer :: (MonadThrow m, MonadLogger m, MonadIO m, MonadFail m)
=> Version
-> Excepts '[NotInstalled] m ()
rmGHCVer ver = do
isSetGHC <- fmap (maybe False (== ver)) $ ghcSet
dir <- liftIO $ ghcupGHCDir ver
let d' = toFilePath dir
2020-02-29 23:07:39 +00:00
exists <- liftIO $ doesDirectoryExist dir
2020-02-28 23:33:32 +00:00
toolsFiles <- liftE $ ghcToolFiles ver
if exists
then do
2020-02-29 23:07:39 +00:00
-- this isn't atomic, order matters
2020-02-28 23:33:32 +00:00
lift $ $(logInfo) [i|Removing directory recursively: #{d'}|]
liftIO $ deleteDirRecursive dir
lift $ $(logInfo) [i|Removing ghc-x.y.z symlinks|]
liftIO $ rmMinorSymlinks
2020-03-01 01:21:40 +00:00
lift $ $(logInfo) [i|Removing/rewiring ghc-x.y symlinks|]
2020-02-28 23:33:32 +00:00
liftE fixMajorSymlinks
when isSetGHC $ liftE $ do
lift $ $(logInfo) [i|Removing ghc symlinks|]
2020-02-29 23:07:39 +00:00
rmPlain toolsFiles
2020-02-28 23:33:32 +00:00
liftIO
$ ghcupBaseDir
>>= hideError doesNotExistErrorType
. deleteFile
. (</> ([rel|share|] :: Path Rel))
else throwE (NotInstalled $ ToolRequest GHC ver)
where
-- e.g. ghc-8.6.5
rmMinorSymlinks :: IO ()
rmMinorSymlinks = do
bindir <- ghcupBinDir
files <- getDirsFiles' bindir
let myfiles = filter
(\x -> ([s|-|] <> verToBS ver) `B.isSuffixOf` toFilePath x)
files
forM_ myfiles $ \f -> deleteFile (bindir </> f)
-- E.g. ghc, if this version is the set one.
-- This reads `ghcupGHCDir`.
rmPlain :: (MonadThrow m, MonadFail m, MonadIO m)
2020-02-29 23:07:39 +00:00
=> [Path Rel] -- ^ tools files
2020-02-28 23:33:32 +00:00
-> Excepts '[NotInstalled] m ()
2020-02-29 23:07:39 +00:00
rmPlain files = do
2020-02-28 23:33:32 +00:00
bindir <- liftIO $ ghcupBinDir
forM_ files $ \f -> liftIO $ deleteFile (bindir </> f)
-- e.g. ghc-8.6
fixMajorSymlinks :: (MonadFail m, MonadThrow m, MonadIO m)
=> Excepts '[NotInstalled] m ()
fixMajorSymlinks = do
(mj, mi) <- getGHCMajor ver
let v' = E.encodeUtf8 $ intToText mj <> [s|.|] <> intToText mi
bindir <- liftIO $ ghcupBinDir
-- first delete them
files <- liftIO $ getDirsFiles' bindir
let myfiles =
filter (\x -> ([s|-|] <> v') `B.isSuffixOf` toFilePath x) files
forM_ myfiles $ \f -> liftIO $ deleteFile (bindir </> f)
-- then fix them (e.g. with an earlier version)
getGHCForMajor mj mi >>= mapM_ (\v -> liftE $ setGHC v SetGHCMajor)
2020-02-24 13:56:13 +00:00
2020-02-29 23:07:39 +00:00
------------------
--[ Debug info ]--
------------------
getDebugInfo :: (MonadLogger m, MonadCatch m, MonadReader Settings m, MonadIO m)
=> Excepts
'[PlatformResultError , NoCompatibleArch , DistroNotFound]
m
DebugInfo
getDebugInfo = do
diBaseDir <- liftIO $ ghcupBaseDir
diBinDir <- liftIO $ ghcupBinDir
diGHCDir <- liftIO $ ghcupGHCBaseDir
diCacheDir <- liftIO $ ghcupCacheDir
diURLSource <- lift $ getUrlSource
diArch <- lE getArchitecture
diPlatform <- liftE $ getPlatform
pure $ DebugInfo { .. }
2020-02-18 08:40:01 +00:00
2020-03-03 00:59:19 +00:00
---------------
--[ Compile ]--
---------------
2020-02-22 18:21:10 +00:00
2020-03-03 00:59:19 +00:00
-- TODO: build config
compileGHC :: ( MonadReader Settings m
, MonadThrow m
, MonadResource m
, MonadLogger m
, MonadIO m
, MonadFail m
)
=> SourceDownloads
-> Version -- ^ version to install
-> Version -- ^ version to bootstrap with
-> Maybe Int -- ^ jobs
-> Maybe (Path Abs) -- ^ build config
-> Excepts
'[ AlreadyInstalled
, NotInstalled
, GHCNotFound
, ArchiveError
, ProcessError
, URLException
, DigestError
, BuildConfigNotFound
]
m
()
compileGHC dls tver bver jobs mbuildConfig = do
let treq = ToolRequest GHC tver
lift $ $(logDebug) [i|Requested to compile: #{tver} with #{bver}|]
alreadyInstalled <- liftIO $ toolAlreadyInstalled treq
when alreadyInstalled $ (throwE $ AlreadyInstalled treq)
2020-02-24 13:56:13 +00:00
2020-03-03 00:59:19 +00:00
-- download source tarball
dlInfo <- preview (ix tver) dls ?? GHCNotFound
dl <- liftE $ downloadCached dlInfo Nothing
2020-02-24 13:56:13 +00:00
2020-03-03 00:59:19 +00:00
-- unpack
tmpUnpack <- lift mkGhcupTmpDir
liftE $ unpackToDir tmpUnpack dl
2020-02-28 23:33:32 +00:00
2020-03-03 00:59:19 +00:00
bghc <- parseRel ([s|ghc-|] <> verToBS bver)
let workdir = maybe id (flip (</>)) (view dlSubdir dlInfo) $ tmpUnpack
2020-02-28 23:33:32 +00:00
2020-03-03 00:59:19 +00:00
ghcdir <- liftIO $ ghcupGHCDir tver
if
| tver >= [vver|8.8.0|] -> do
cEnv <- liftIO $ getEnvironment
spaths <- catMaybes . fmap parseAbs <$> liftIO getSearchPath
bghcPath <- (liftIO $ searchPath spaths bghc) !? GHCNotFound
let newEnv = ([s|GHC|], toFilePath bghcPath) : cEnv
lEM $ liftIO $ exec [s|./configure|]
False
[[s|--prefix=|] <> toFilePath ghcdir]
(Just workdir)
(Just newEnv)
| otherwise -> do
lEM $ liftIO $ exec
[s|./configure|]
False
[ [s|--prefix=|] <> toFilePath ghcdir
, [s|--with-ghc=|] <> toFilePath bghc
]
(Just workdir)
Nothing
let build_mk = workdir </> ([rel|mk/build.mk|] :: Path Rel)
case mbuildConfig of
Just bc -> liftIO $ copyFile bc build_mk Overwrite
Nothing -> liftIO $ writeFile build_mk (Just newFilePerms) defaultConf
lEM $ liftIO $ exec [s|make|]
True
(maybe [] (\j -> [[s|-j|] <> fS (show j)]) jobs)
(Just workdir)
Nothing
lEM $ liftIO $ exec [s|make|] True [[s|install|]] (Just workdir) Nothing
liftE $ postGHCInstall tver
pure ()
2020-02-28 23:33:32 +00:00
where
2020-03-03 00:59:19 +00:00
defaultConf = [s|
V=0
BUILD_MAN = NO
BUILD_SPHINX_HTML = NO
BUILD_SPHINX_PDF = NO
HADDOCK_DOCS = YES
GhcWithLlvmCodeGen = YES|]
-------------
--[ Other ]--
-------------
-- | Creates ghc-x.y.z and ghc-x.y symlinks.
postGHCInstall :: (MonadThrow m, MonadFail m, MonadIO m)
=> Version
-> Excepts '[NotInstalled] m ()
postGHCInstall ver = do
liftE $ setGHC ver SetGHCMinor
2020-02-28 23:33:32 +00:00
2020-03-03 00:59:19 +00:00
-- Create ghc-x.y symlinks. This may not be the current
-- version, create it regardless.
(mj, mi) <- liftIO $ getGHCMajor ver
getGHCForMajor mj mi >>= mapM_ (\v -> liftE $ setGHC v SetGHCMajor)