2022-02-09 17:57:59 +00:00
{- # LANGUAGE CPP # -}
{- # LANGUAGE QuasiQuotes # -}
{- # LANGUAGE TypeApplications # -}
{- # LANGUAGE DataKinds # -}
{- # LANGUAGE RankNTypes # -}
{- # LANGUAGE FlexibleContexts # -}
{- # LANGUAGE TypeFamilies # -}
module GHCup.OptParse.Run where
import GHCup
import GHCup.Utils
import GHCup.Utils.Prelude
import GHCup.Utils.File
import GHCup.OptParse.Common
import GHCup.Errors
import GHCup.Types
2022-02-10 19:35:09 +00:00
import GHCup.Types.Optics ( getDirs )
2022-02-09 17:57:59 +00:00
import GHCup.Utils.Logger
import GHCup.Utils.String.QQ
import Control.Exception.Safe ( MonadMask , MonadCatch )
# if ! MIN_VERSION_base ( 4 , 13 , 0 )
import Control.Monad.Fail ( MonadFail )
# endif
import Codec.Archive
import Control.Monad.Reader
import Control.Monad.Trans.Resource
import Data.Functor
import Data.Maybe ( isNothing )
import Data.List ( intercalate )
import Haskus.Utils.Variant.Excepts
import Options.Applicative hiding ( style )
import Prelude hiding ( appendFile )
import System.Directory
import System.FilePath
import System.Environment
import System.Exit
import Text.PrettyPrint.HughesPJClass ( prettyShow )
import qualified Data.Map.Strict as Map
import qualified Data.Text as T
# ifndef IS_WINDOWS
import qualified System.Posix.Process as SPP
# endif
---------------
--[ Options ]--
---------------
data RunOptions = RunOptions
{ runAppendPATH :: Bool
, runInstTool' :: Bool
, runGHCVer :: Maybe ToolVersion
, runCabalVer :: Maybe ToolVersion
, runHLSVer :: Maybe ToolVersion
, runStackVer :: Maybe ToolVersion
, runBinDir :: Maybe FilePath
, runCOMMAND :: [ String ]
}
---------------
--[ Parsers ]--
---------------
2022-03-04 23:46:37 +00:00
2022-02-09 17:57:59 +00:00
runOpts :: Parser RunOptions
runOpts =
RunOptions
<$> switch
( short 'a' <> long " append " <> help " Append bin/ dir to PATH instead of prepending (this means that e.g. a system installation may take precedence) " )
<*> switch
( short 'i' <> long " install " <> help " Install the tool, if missing " )
<*> optional
( option
( eitherReader toolVersionEither )
2022-03-04 23:46:37 +00:00
( metavar " GHC_VERSION " <> long " ghc " <> help " The ghc version "
<> completer ( tagCompleter GHC [] )
<> ( completer $ versionCompleter Nothing GHC )
)
2022-02-09 17:57:59 +00:00
)
<*> optional
( option
( eitherReader toolVersionEither )
2022-03-04 23:46:37 +00:00
( metavar " CABAL_VERSION " <> long " cabal " <> help " The cabal version "
<> completer ( tagCompleter Cabal [] )
<> ( completer $ versionCompleter Nothing Cabal )
)
2022-02-09 17:57:59 +00:00
)
<*> optional
( option
( eitherReader toolVersionEither )
2022-03-04 23:46:37 +00:00
( metavar " HLS_VERSION " <> long " hls " <> help " The HLS version "
<> completer ( tagCompleter HLS [] )
<> ( completer $ versionCompleter Nothing HLS )
)
2022-02-09 17:57:59 +00:00
)
<*> optional
( option
( eitherReader toolVersionEither )
2022-03-04 23:46:37 +00:00
( metavar " STACK_VERSION " <> long " stack " <> help " The stack version "
<> completer ( tagCompleter Stack [] )
<> ( completer $ versionCompleter Nothing Stack )
)
2022-02-09 17:57:59 +00:00
)
<*> optional
( option
( eitherReader isolateParser )
( short 'b'
<> long " bindir "
<> metavar " DIR "
<> help " directory where to create the tool symlinks (default: newly created system temp dir) "
2022-03-04 23:46:37 +00:00
<> completer ( bashCompleter " directory " )
2022-02-09 17:57:59 +00:00
)
)
<*> many ( argument str ( metavar " COMMAND " <> help " The command to run, with arguments (use longopts --). If omitted, just prints the created bin/ dir to stdout and exits. " ) )
--------------
--[ Footer ]--
--------------
runFooter :: String
runFooter = [ s | Discussion :
Adds the given tools to a dedicated bin / directory and adds them to PATH , exposing
the relevant binaries , then executes a command .
Examples :
# run VSCode with all latest toolchain exposed , installing missing versions if necessary
ghcup run --ghc latest --cabal latest --hls latest --stack latest --install -- code Setup.hs
# create a custom toolchain bin / dir with GHC and cabal that can be manually added to PATH
ghcup run --ghc 8.10.7 --cabal 3.2.0.0 --bindir $HOME/toolchain/bin
# run a specific ghc version
ghcup run --ghc 8.10.7 -- ghc --version|]
---------------------------
--[ Effect interpreters ]--
---------------------------
type RunEffects = ' [ A l r e a d y I n s t a l l e d
, UnknownArchive
, ArchiveResult
, FileDoesNotExistError
, CopyError
, NotInstalled
, DirNotEmpty
, NoDownload
, NotInstalled
, BuildFailed
, TagNotFound
, DigestError
, GPGError
, DownloadFailed
, TarDirDoesNotExist
, NextVerNotFound
, NoToolVersionSet
, FileAlreadyExistsError
, ProcessError
]
2022-02-10 18:29:32 +00:00
runLeanRUN :: ( MonadUnliftIO m , MonadIO m )
=> LeanAppState
-> Excepts RunEffects ( ReaderT LeanAppState m ) a
-> m ( VEither RunEffects a )
runLeanRUN leanAppstate =
-- Don't use runLeanAppState here, which is disabled on windows.
-- This is the only command on all platforms that doesn't need full appstate.
flip runReaderT leanAppstate
. runE
@ RunEffects
2022-02-09 17:57:59 +00:00
runRUN :: MonadUnliftIO m
=> ( ReaderT AppState m ( VEither RunEffects a ) -> m ( VEither RunEffects a ) )
-> Excepts RunEffects ( ResourceT ( ReaderT AppState m ) ) a
-> m ( VEither RunEffects a )
runRUN runAppState =
runAppState
. runResourceT
. runE
@ RunEffects
------------------
--[ Entrypoint ]--
------------------
run :: forall m .
( MonadFail m
, MonadMask m
, MonadCatch m
, MonadIO m
, MonadUnliftIO m
)
=> RunOptions
-> ( forall a . ReaderT AppState m ( VEither RunEffects a ) -> m ( VEither RunEffects a ) )
2022-02-10 18:29:32 +00:00
-> LeanAppState
2022-02-09 17:57:59 +00:00
-> ( ReaderT LeanAppState m () -> m () )
-> m ExitCode
2022-03-13 22:49:53 +00:00
run RunOptions { .. } runAppState leanAppstate runLogger = runE @ RunEffects ( do
toolchain <- Excepts resolveToolchain
2022-02-10 18:29:32 +00:00
tmp <- case runBinDir of
2022-03-13 22:49:53 +00:00
Just bindir -> do
liftIO $ createDirRecursive' bindir
liftIO $ canonicalizePath bindir
Nothing -> do
d <- liftIO $ predictableTmpDir toolchain
liftIO $ createDirRecursive' d
liftIO $ canonicalizePath d
Excepts $ installToolChain toolchain tmp
pure tmp
) >>= \ case
VRight tmp -> do
2022-02-10 18:29:32 +00:00
case runCOMMAND of
2022-02-10 20:49:19 +00:00
[] -> do
liftIO $ putStr tmp
pure ExitSuccess
2022-02-10 18:29:32 +00:00
( cmd : args ) -> do
newEnv <- liftIO $ addToPath tmp
2022-02-10 17:35:25 +00:00
# ifndef IS_WINDOWS
2022-02-10 20:49:19 +00:00
void $ liftIO $ SPP . executeFile cmd True args ( Just newEnv )
pure ExitSuccess
2022-02-10 17:35:25 +00:00
# else
2022-02-10 20:49:19 +00:00
r' <- runLeanRUN leanAppstate $ liftE $ lEM @ _ @ '[ProcessError] $ exec cmd args Nothing ( Just newEnv )
case r' of
VRight _ -> pure ExitSuccess
VLeft e -> do
runLogger $ logError $ T . pack $ prettyShow e
pure $ ExitFailure 28
2022-02-10 17:35:25 +00:00
# endif
2022-02-10 18:29:32 +00:00
VLeft e -> do
runLogger $ logError $ T . pack $ prettyShow e
pure $ ExitFailure 27
2022-02-09 17:57:59 +00:00
where
2022-02-10 18:29:32 +00:00
isToolTag :: ToolVersion -> Bool
isToolTag ( ToolTag _ ) = True
isToolTag _ = False
2022-02-09 17:57:59 +00:00
-- TODO: doesn't work for cross
2022-03-13 22:49:53 +00:00
resolveToolchain
2022-02-10 18:29:32 +00:00
| or ( fmap ( maybe False isToolTag ) [ runGHCVer , runCabalVer , runHLSVer , runStackVer ] ) || runInstTool' = runRUN runAppState $ do
2022-03-13 22:49:53 +00:00
ghcVer <- forM runGHCVer $ \ ver -> do
2022-02-10 18:29:32 +00:00
( v , _ ) <- liftE $ fromVersion ( Just ver ) GHC
2022-03-13 22:49:53 +00:00
pure v
cabalVer <- forM runCabalVer $ \ ver -> do
2022-02-10 18:29:32 +00:00
( v , _ ) <- liftE $ fromVersion ( Just ver ) Cabal
2022-03-13 22:49:53 +00:00
pure v
hlsVer <- forM runHLSVer $ \ ver -> do
2022-02-10 18:29:32 +00:00
( v , _ ) <- liftE $ fromVersion ( Just ver ) HLS
2022-03-13 22:49:53 +00:00
pure v
stackVer <- forM runStackVer $ \ ver -> do
2022-02-10 18:29:32 +00:00
( v , _ ) <- liftE $ fromVersion ( Just ver ) Stack
2022-03-13 22:49:53 +00:00
pure v
pure Toolchain { .. }
2022-02-10 18:29:32 +00:00
| otherwise = runLeanRUN leanAppstate $ do
2022-03-13 22:49:53 +00:00
ghcVer <- case runGHCVer of
Just ( ToolVersion v ) -> pure $ Just v
Nothing -> pure Nothing
2022-02-10 18:29:32 +00:00
_ -> fail " Internal error "
2022-03-13 22:49:53 +00:00
cabalVer <- case runCabalVer of
Just ( ToolVersion v ) -> pure $ Just v
Nothing -> pure Nothing
2022-02-10 18:29:32 +00:00
_ -> fail " Internal error "
2022-03-13 22:49:53 +00:00
hlsVer <- case runHLSVer of
Just ( ToolVersion v ) -> pure $ Just v
Nothing -> pure Nothing
2022-02-10 18:29:32 +00:00
_ -> fail " Internal error "
2022-03-13 22:49:53 +00:00
stackVer <- case runStackVer of
Just ( ToolVersion v ) -> pure $ Just v
Nothing -> pure Nothing
2022-02-10 18:29:32 +00:00
_ -> fail " Internal error "
2022-03-13 22:49:53 +00:00
pure Toolchain { .. }
2022-02-10 18:29:32 +00:00
2022-03-13 22:49:53 +00:00
installToolChain Toolchain { .. } tmp
| or ( fmap ( maybe False isToolTag ) [ runGHCVer , runCabalVer , runHLSVer , runStackVer ] ) || runInstTool' = runRUN runAppState $ do
forM_ [ ( GHC , ) <$> ghcVer , ( Cabal , ) <$> cabalVer , ( HLS , ) <$> hlsVer , ( Stack , ) <$> stackVer ] $ \ mt -> do
isInstalled <- maybe ( pure False ) ( \ ( tool , v ) -> lift $ checkIfToolInstalled' tool v ) mt
case mt of
Just ( GHC , v ) -> do
unless isInstalled $ when ( runInstTool' && isNothing ( _tvTarget v ) ) $ void $ liftE $ installGHCBin
( _tvVersion v )
Nothing
False
setTool GHC v tmp
Just ( Cabal , v ) -> do
unless isInstalled $ when runInstTool' $ void $ liftE $ installCabalBin
( _tvVersion v )
Nothing
False
setTool Cabal v tmp
Just ( Stack , v ) -> do
unless isInstalled $ when runInstTool' $ void $ liftE $ installStackBin
( _tvVersion v )
Nothing
False
setTool Stack v tmp
Just ( HLS , v ) -> do
unless isInstalled $ when runInstTool' $ void $ liftE $ installHLSBin
( _tvVersion v )
Nothing
False
setTool HLS v tmp
_ -> pure ()
| otherwise = runLeanRUN leanAppstate $ do
forM_ [ ( GHC , ) <$> ghcVer , ( Cabal , ) <$> cabalVer , ( HLS , ) <$> hlsVer , ( Stack , ) <$> stackVer ] $ \ mt -> do
case mt of
Just ( GHC , v ) -> setTool GHC v tmp
Just ( Cabal , v ) -> setTool Cabal v tmp
Just ( Stack , v ) -> setTool Stack v tmp
Just ( HLS , v ) -> setTool HLS v tmp
_ -> pure ()
2022-02-10 18:29:32 +00:00
setTool tool v tmp =
case tool of
GHC -> do
void $ liftE $ setGHC v SetGHC_XYZ ( Just tmp )
void $ liftE $ setGHC v SetGHCOnly ( Just tmp )
Cabal -> do
bin <- liftE $ whereIsTool Cabal v
cbin <- liftIO $ canonicalizePath bin
2022-02-10 20:49:19 +00:00
lift $ createLink ( relativeSymlink tmp cbin ) ( tmp </> ( " cabal " <.> exeExt ) )
2022-02-10 18:29:32 +00:00
Stack -> do
bin <- liftE $ whereIsTool Stack v
cbin <- liftIO $ canonicalizePath bin
2022-02-10 20:49:19 +00:00
lift $ createLink ( relativeSymlink tmp cbin ) ( tmp </> ( " stack " <.> exeExt ) )
2022-02-10 18:29:32 +00:00
HLS -> do
2022-02-10 19:35:09 +00:00
Dirs { .. } <- getDirs
let v' = _tvVersion v
legacy <- isLegacyHLS v'
if legacy
then do
-- TODO: factor this out
2022-03-05 19:19:54 +00:00
hlsWrapper <- liftE @ _ @ '[NotInstalled] $ hlsWrapperBinary v' !? ( NotInstalled HLS ( mkTVer v' ) )
2022-02-10 19:35:09 +00:00
cw <- liftIO $ canonicalizePath ( binDir </> hlsWrapper )
lift $ createLink ( relativeSymlink tmp cw ) ( tmp </> takeFileName cw )
hlsBins <- hlsServerBinaries v' Nothing >>= liftIO . traverse ( canonicalizePath . ( binDir </> ) )
forM_ hlsBins $ \ bin ->
lift $ createLink ( relativeSymlink tmp bin ) ( tmp </> takeFileName bin )
liftE $ setHLS ( _tvVersion v ) SetHLSOnly ( Just tmp )
else do
liftE $ setHLS ( _tvVersion v ) SetHLS_XYZ ( Just tmp )
liftE $ setHLS ( _tvVersion v ) SetHLSOnly ( Just tmp )
2022-02-10 18:29:32 +00:00
GHCup -> pure ()
2022-02-09 17:57:59 +00:00
addToPath path = do
cEnv <- Map . fromList <$> getEnvironment
let paths = [ " PATH " , " Path " ]
curPaths = ( \ x -> maybe [] splitSearchPath ( Map . lookup x cEnv ) ) =<< paths
newPath = intercalate [ searchPathSeparator ] ( if runAppendPATH then ( curPaths ++ [ path ] ) else ( path : curPaths ) )
envWithoutPath = foldr ( \ x y -> Map . delete x y ) cEnv paths
pathVar = if isWindows then " Path " else " PATH "
envWithNewPath = Map . toList $ Map . insert pathVar newPath envWithoutPath
liftIO $ setEnv pathVar newPath
return envWithNewPath
2022-03-13 22:49:53 +00:00
predictableTmpDir ( Toolchain Nothing Nothing Nothing Nothing ) =
liftIO ( getTemporaryDirectory >>= \ tmp -> pure ( tmp </> " ghcup-none " ) )
predictableTmpDir Toolchain { .. } = do
tmp <- getTemporaryDirectory
pure $ tmp
2022-03-16 22:15:09 +00:00
</> ( " ghcup- " <> intercalate " _ "
( maybe [] ( ( : [] ) . ( " ghc- " <> ) . T . unpack . tVerToText ) ghcVer
<> maybe [] ( ( : [] ) . ( " cabal- " <> ) . T . unpack . tVerToText ) cabalVer
<> maybe [] ( ( : [] ) . ( " hls- " <> ) . T . unpack . tVerToText ) hlsVer
<> maybe [] ( ( : [] ) . ( " stack- " <> ) . T . unpack . tVerToText ) stackVer
)
2022-03-13 22:49:53 +00:00
)
-------------------------
--[ Other local types ]--
-------------------------
data Toolchain = Toolchain
{ ghcVer :: Maybe GHCTargetVersion
, cabalVer :: Maybe GHCTargetVersion
, hlsVer :: Maybe GHCTargetVersion
, stackVer :: Maybe GHCTargetVersion
}