{-# LANGUAGE CPP #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} module GHCup.OptParse.Run where import GHCup import GHCup.Utils import GHCup.OptParse.Common import GHCup.Errors import GHCup.Types import GHCup.Types.Optics import GHCup.Prelude import GHCup.Prelude.File #ifdef IS_WINDOWS import GHCup.Prelude.Process import GHCup.Prelude.Process.Windows ( execNoMinGW ) #endif import GHCup.Prelude.Logger import GHCup.Prelude.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.FilePath import System.Environment import System.Exit import qualified Data.Map.Strict as Map import qualified Data.Text as T #ifndef IS_WINDOWS import qualified System.Posix.Process as SPP #endif import Data.Versions ( prettyVer, Version ) --------------- --[ Options ]-- --------------- data RunOptions = RunOptions { runAppendPATH :: Bool , runInstTool' :: Bool , runMinGWPath :: Bool , runGHCVer :: Maybe ToolVersion , runCabalVer :: Maybe ToolVersion , runHLSVer :: Maybe ToolVersion , runStackVer :: Maybe ToolVersion , runBinDir :: Maybe FilePath , runQuick :: Bool , runCOMMAND :: [String] } --------------- --[ Parsers ]-- --------------- 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") <*> switch (short 'm' <> long "mingw-path" <> help "On windows, add mingw64 PATHs to environment (does nothing on unix)") <*> optional (option (eitherReader ghcVersionTagEither) (metavar "GHC_VERSION" <> long "ghc" <> help "The ghc version" <> completer (tagCompleter GHC []) <> (completer $ versionCompleter Nothing GHC) ) ) <*> optional (option (eitherReader toolVersionTagEither) (metavar "CABAL_VERSION" <> long "cabal" <> help "The cabal version" <> completer (tagCompleter Cabal []) <> (completer $ versionCompleter Nothing Cabal) ) ) <*> optional (option (eitherReader toolVersionTagEither) (metavar "HLS_VERSION" <> long "hls" <> help "The HLS version" <> completer (tagCompleter HLS []) <> (completer $ versionCompleter Nothing HLS) ) ) <*> optional (option (eitherReader toolVersionTagEither) (metavar "STACK_VERSION" <> long "stack" <> help "The stack version" <> completer (tagCompleter Stack []) <> (completer $ versionCompleter Nothing Stack) ) ) <*> optional (option (eitherReader isolateParser) ( short 'b' <> long "bindir" <> metavar "DIR" <> help "directory where to create the tool symlinks (default: newly created system temp dir)" <> completer (bashCompleter "directory") ) ) <*> switch (short 'q' <> long "quick" <> help "Avoid any expensive work (such as downloads, version/tag resolution etc.). Disables --install.") <*> 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 = '[ AlreadyInstalled , UnknownArchive , ArchiveResult , FileDoesNotExistError , CopyError , NotInstalled , DirNotEmpty , NoDownload , NotInstalled , BuildFailed , TagNotFound , DigestError , ContentLengthError , GPGError , DownloadFailed , TarDirDoesNotExist , NextVerNotFound , NoToolVersionSet , FileAlreadyExistsError , ProcessError , UninstallFailed , MergeFileTreeError ] 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 runRUN :: MonadUnliftIO m => IO AppState -> Excepts RunEffects (ResourceT (ReaderT AppState m)) a -> m (VEither RunEffects a) runRUN appState action' = do s' <- liftIO appState flip runReaderT s' . runResourceT . runE @RunEffects $ action' ------------------ --[ Entrypoint ]-- ------------------ run :: forall m . ( MonadFail m , MonadMask m , MonadCatch m , MonadIO m , MonadUnliftIO m ) => RunOptions -> IO AppState -> LeanAppState -> (ReaderT LeanAppState m () -> m ()) -> m ExitCode run RunOptions{..} runAppState leanAppstate runLogger = do r <- if not runQuick then runRUN runAppState $ do toolchain <- liftE resolveToolchainFull -- oh dear r <- lift ask tmp <- lift . lift . lift . flip runReaderT (fromAppState r) $ createTmpDir toolchain liftE $ installToolChainFull toolchain tmp pure tmp else runLeanRUN leanAppstate $ do toolchain <- resolveToolchain tmp <- lift $ createTmpDir toolchain liftE $ installToolChain toolchain tmp pure tmp case r of VRight tmp -> do case runCOMMAND of [] -> do liftIO $ putStr tmp pure ExitSuccess (cmd:args) -> do newEnv <- liftIO $ addToPath tmp runAppendPATH #ifndef IS_WINDOWS void $ liftIO $ SPP.executeFile cmd True args (Just newEnv) pure ExitSuccess #else r' <- if runMinGWPath then runLeanRUN leanAppstate $ liftE $ lEM @_ @'[ProcessError] $ exec cmd args Nothing (Just newEnv) else runLeanRUN leanAppstate $ liftE $ lEM @_ @'[ProcessError] $ execNoMinGW cmd args Nothing (Just newEnv) case r' of VRight _ -> pure ExitSuccess VLeft e -> do runLogger $ logError $ T.pack $ prettyHFError e pure $ ExitFailure 28 #endif VLeft e -> do runLogger $ logError $ T.pack $ prettyHFError e pure $ ExitFailure 27 where -- TODO: doesn't work for cross resolveToolchainFull :: ( MonadFail m , MonadThrow m , MonadIO m , MonadCatch m ) => Excepts '[ TagNotFound , NextVerNotFound , NoToolVersionSet ] (ResourceT (ReaderT AppState m)) Toolchain resolveToolchainFull = do ghcVer <- forM runGHCVer $ \ver -> do (v, _) <- liftE $ fromVersion (Just ver) GHC pure v cabalVer <- forM runCabalVer $ \ver -> do (v, _) <- liftE $ fromVersion (Just ver) Cabal pure (_tvVersion v) hlsVer <- forM runHLSVer $ \ver -> do (v, _) <- liftE $ fromVersion (Just ver) HLS pure (_tvVersion v) stackVer <- forM runStackVer $ \ver -> do (v, _) <- liftE $ fromVersion (Just ver) Stack pure (_tvVersion v) pure Toolchain{..} resolveToolchain = do ghcVer <- case runGHCVer of Just (GHCVersion v) -> pure $ Just v Just (ToolVersion v) -> pure $ Just (mkTVer v) Nothing -> pure Nothing _ -> fail "Internal error" cabalVer <- case runCabalVer of Just (GHCVersion v) -> pure $ Just (_tvVersion v) Just (ToolVersion v) -> pure $ Just v Nothing -> pure Nothing _ -> fail "Internal error" hlsVer <- case runHLSVer of Just (GHCVersion v) -> pure $ Just (_tvVersion v) Just (ToolVersion v) -> pure $ Just v Nothing -> pure Nothing _ -> fail "Internal error" stackVer <- case runStackVer of Just (GHCVersion v) -> pure $ Just (_tvVersion v) Just (ToolVersion v) -> pure $ Just v Nothing -> pure Nothing _ -> fail "Internal error" pure Toolchain{..} installToolChainFull :: ( MonadFail m , MonadThrow m , MonadIO m , MonadCatch m ) => Toolchain -> FilePath -> Excepts '[ TagNotFound , NextVerNotFound , NoToolVersionSet , UnknownArchive , TarDirDoesNotExist , ProcessError , NotInstalled , NoDownload , GPGError , DownloadFailed , DirNotEmpty , DigestError , ContentLengthError , BuildFailed , ArchiveResult , AlreadyInstalled , FileAlreadyExistsError , CopyError , UninstallFailed , MergeFileTreeError ] (ResourceT (ReaderT AppState m)) () installToolChainFull Toolchain{..} tmp = do case ghcVer of Just v -> do isInstalled <- lift $ checkIfToolInstalled' GHC v unless isInstalled $ when (runInstTool' && isNothing (_tvTarget v)) $ void $ liftE $ installGHCBin (_tvVersion v) GHCupInternal False [] setGHC' v tmp _ -> pure () case cabalVer of Just v -> do isInstalled <- lift $ checkIfToolInstalled' Cabal (mkTVer v) unless isInstalled $ when runInstTool' $ void $ liftE $ installCabalBin v GHCupInternal False setCabal' v tmp _ -> pure () case stackVer of Just v -> do isInstalled <- lift $ checkIfToolInstalled' Stack (mkTVer v) unless isInstalled $ when runInstTool' $ void $ liftE $ installStackBin v GHCupInternal False setStack' v tmp _ -> pure () case hlsVer of Just v -> do isInstalled <- lift $ checkIfToolInstalled' HLS (mkTVer v) unless isInstalled $ when runInstTool' $ void $ liftE $ installHLSBin v GHCupInternal False setHLS' v tmp _ -> pure () installToolChain :: ( MonadFail m , MonadThrow m , MonadIO m , MonadCatch m ) => Toolchain -> FilePath -> Excepts '[NotInstalled] (ReaderT LeanAppState m) () installToolChain Toolchain{..} tmp = do case ghcVer of Just v -> setGHC' v tmp _ -> pure () case cabalVer of Just v -> setCabal' v tmp _ -> pure () case stackVer of Just v -> setStack' v tmp _ -> pure () case hlsVer of Just v -> setHLS' v tmp _ -> pure () setGHC' v tmp = do void $ liftE $ setGHC v SetGHC_XYZ (Just tmp) void $ liftE $ setGHC v SetGHCOnly (Just tmp) setCabal' v tmp = do bin <- liftE $ whereIsTool Cabal (mkTVer v) cbin <- liftIO $ canonicalizePath bin lift $ createLink (relativeSymlink tmp cbin) (tmp ("cabal" <.> exeExt)) setStack' v tmp = do bin <- liftE $ whereIsTool Stack (mkTVer v) cbin <- liftIO $ canonicalizePath bin lift $ createLink (relativeSymlink tmp cbin) (tmp ("stack" <.> exeExt)) setHLS' v tmp = do Dirs {..} <- getDirs legacy <- isLegacyHLS v if legacy then do -- TODO: factor this out hlsWrapper <- liftE @_ @'[NotInstalled] $ hlsWrapperBinary v !? (NotInstalled HLS (mkTVer v)) 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 v SetHLSOnly (Just tmp) else do liftE $ setHLS v SetHLS_XYZ (Just tmp) liftE $ setHLS v SetHLSOnly (Just tmp) createTmpDir :: ( MonadUnliftIO m , MonadCatch m , MonadThrow m , MonadMask m , MonadIO m ) => Toolchain -> ReaderT LeanAppState m FilePath createTmpDir toolchain = case runBinDir of Just bindir -> do liftIO $ createDirRecursive' bindir liftIO $ canonicalizePath bindir Nothing -> do d <- predictableTmpDir toolchain liftIO $ createDirRecursive' d liftIO $ canonicalizePath d predictableTmpDir :: Monad m => Toolchain -> ReaderT LeanAppState m FilePath predictableTmpDir (Toolchain Nothing Nothing Nothing Nothing) = do Dirs { tmpDir } <- getDirs pure (fromGHCupPath tmpDir "ghcup-none") predictableTmpDir Toolchain{..} = do Dirs { tmpDir } <- getDirs pure $ fromGHCupPath tmpDir ("ghcup-" <> intercalate "_" ( maybe [] ( (:[]) . ("ghc-" <>) . T.unpack . tVerToText) ghcVer <> maybe [] ( (:[]) . ("cabal-" <>) . T.unpack . prettyVer) cabalVer <> maybe [] ( (:[]) . ("hls-" <>) . T.unpack . prettyVer) hlsVer <> maybe [] ( (:[]) . ("stack-" <>) . T.unpack . prettyVer) stackVer ) ) ------------------------- --[ Other local types ]-- ------------------------- data Toolchain = Toolchain { ghcVer :: Maybe GHCTargetVersion , cabalVer :: Maybe Version , hlsVer :: Maybe Version , stackVer :: Maybe Version } deriving Show