{-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE DuplicateRecordFields #-} module Main where import GHCup import GHCup.Download import GHCup.Errors import GHCup.Types import GHCup.Utils import GHCup.Utils.File import GHCup.Utils.Logger import GHCup.Utils.Prelude import GHCup.Utils.String.QQ import Control.Monad.Logger import Control.Monad.Reader import Control.Monad.Trans.Resource import Data.Bifunctor import Data.Char import Data.List ( intercalate ) import Data.Semigroup ( (<>) ) import Data.String.Interpolate import Data.Versions import Haskus.Utils.Variant.Excepts import HPath import HPath.IO import Options.Applicative hiding ( style ) import Prelude hiding ( appendFile ) import System.Console.Pretty import System.Environment import System.Exit import System.IO hiding ( appendFile ) import Text.Read import Text.Layout.Table import URI.ByteString import qualified Data.ByteString as B import qualified Data.ByteString.UTF8 as UTF8 import qualified Data.Text as T import qualified Data.Text.Encoding as E data Options = Options { -- global options optVerbose :: Bool , optCache :: Bool , optUrlSource :: Maybe URI , optNoVerify :: Bool -- commands , optCommand :: Command } data Command = Install InstallCommand | SetGHC SetGHCOptions | List ListOptions | Rm RmOptions | DInfo | Compile CompileOptions | Upgrade UpgradeOpts data ToolVersion = ToolVersion Version | ToolTag Tag data InstallCommand = InstallGHC InstallOptions | InstallCabal InstallOptions data InstallOptions = InstallOptions { instVer :: Maybe ToolVersion } data SetGHCOptions = SetGHCOptions { ghcVer :: Maybe ToolVersion } data ListOptions = ListOptions { lTool :: Maybe Tool , lCriteria :: Maybe ListCriteria } data RmOptions = RmOptions { ghcVer :: Version } data CompileOptions = CompileOptions { ghcVer :: Version , bootstrapVer :: Version , jobs :: Maybe Int , buildConfig :: Maybe (Path Abs) } data UpgradeOpts = UpgradeInplace | UpgradeAt (Path Abs) | UpgradeGHCupDir deriving Show opts :: Parser Options opts = Options <$> switch (short 'v' <> long "verbose" <> help "Whether to enable verbosity (default: False)" ) <*> switch (short 'c' <> long "cache" <> help "Whether to cache downloads (default: False)" ) <*> (optional (option (eitherReader parseUri) (short 's' <> long "url-source" <> metavar "URL" <> help "Alternative ghcup download info url" ) ) ) <*> switch ( short 'n' <> long "no-verify" <> help "Skip tarball checksum checks (default: False)" ) <*> com where parseUri s' = bimap show id $ parseURI strictURIParserOptions (UTF8.fromString s') com :: Parser Command com = subparser ( command "install" ( Install <$> (info (installP <**> helper) (progDesc "Install or update GHC/cabal") ) ) <> command "list" ( List <$> (info (listOpts <**> helper) (progDesc "Show available GHCs and other tools") ) ) <> command "upgrade" ( Upgrade <$> (info (upgradeOptsP <**> helper) (progDesc "Upgrade ghcup (per default in ~/.ghcup/bin/)") ) ) <> commandGroup "Main commands:" ) <|> subparser ( command "set" ( SetGHC <$> (info (setGHCOpts <**> helper) (progDesc "Set the currently active GHC version") ) ) <> command "rm" ( Rm <$> (info (rmOpts <**> helper) (progDesc "Remove a GHC version installed by ghcup") ) ) <> command "compile" ( Compile <$> (info (compileOpts <**> helper) (progDesc "Compile GHC from source") ) ) <> commandGroup "GHC commands:" <> hidden ) <|> subparser ( command "debug-info" ((\_ -> DInfo) <$> (info (helper) (progDesc "Show debug info"))) <> commandGroup "Other commands:" <> hidden ) installP :: Parser InstallCommand installP = subparser ( command "ghc" ( InstallGHC <$> (info (installOpts <**> helper) (progDesc "Install a GHC version")) ) <> command "cabal" ( InstallCabal <$> (info (installOpts <**> helper) (progDesc "Install or update a Cabal version") ) ) ) installOpts :: Parser InstallOptions installOpts = InstallOptions <$> optional toolVersionParser setGHCOpts :: Parser SetGHCOptions setGHCOpts = SetGHCOptions <$> optional toolVersionParser listOpts :: Parser ListOptions listOpts = ListOptions <$> optional (option (eitherReader toolParser) (short 't' <> long "tool" <> metavar "" <> help "Tool to list versions for. Default is all" ) ) <*> (optional (option (eitherReader criteriaParser) ( short 'c' <> long "show-criteria" <> metavar "" <> help "Show only installed or set tool versions" ) ) ) rmOpts :: Parser RmOptions rmOpts = RmOptions <$> versionParser compileOpts :: Parser CompileOptions compileOpts = CompileOptions <$> (option (eitherReader (bimap (const "Not a valid version") id . version . T.pack) ) (short 'v' <> long "version" <> metavar "VERSION" <> help "The GHC version to compile" ) ) <*> (option (eitherReader (bimap (const "Not a valid version") id . version . T.pack) ) ( short 'b' <> long "bootstrap-version" <> metavar "BOOTSTRAP_VERSION" <> help "The GHC version to bootstrap with (must be installed)" ) ) <*> optional (option (eitherReader (readEither @Int)) (short 'j' <> long "jobs" <> metavar "JOBS" <> help "How many jobs to use for make" ) ) <*> optional (option (eitherReader (\x -> bimap show id . parseAbs . E.encodeUtf8 . T.pack $ x :: Either String (Path Abs) ) ) (short 'c' <> long "config" <> metavar "CONFIG" <> help "Absolute path to build config file" ) ) versionParser :: Parser Version versionParser = option (eitherReader (bimap (const "Not a valid version") id . version . T.pack)) (short 'v' <> long "version" <> metavar "VERSION" <> help "The target version" ) toolVersionParser :: Parser ToolVersion toolVersionParser = verP <|> toolP where verP = ToolVersion <$> versionParser toolP = ToolTag <$> (option (eitherReader (\s' -> case fmap toLower s' of "recommended" -> Right Recommended "latest" -> Right Latest other -> Left ([i|Unknown tag #{other}|]) ) ) (short 't' <> long "tag" <> metavar "TAG" <> help "The target tag") ) toolParser :: String -> Either String Tool toolParser s' | t == T.pack "ghc" = Right GHC | t == T.pack "cabal" = Right Cabal | otherwise = Left ("Unknown tool: " <> s') where t = T.toLower (T.pack s') criteriaParser :: String -> Either String ListCriteria criteriaParser s' | t == T.pack "installed" = Right ListInstalled | t == T.pack "set" = Right ListSet | otherwise = Left ("Unknown criteria: " <> s') where t = T.toLower (T.pack s') toSettings :: Options -> Settings toSettings Options {..} = let cache = optCache urlSource = maybe GHCupURL OwnSource optUrlSource noVerify = optNoVerify in Settings { .. } upgradeOptsP :: Parser UpgradeOpts upgradeOptsP = flag' UpgradeInplace (short 'i' <> long "inplace" <> help "Upgrade ghcup in-place (wherever it's at)" ) <|> ( UpgradeAt <$> (option (eitherReader (\x -> bimap show id . parseAbs . E.encodeUtf8 . T.pack $ x :: Either String (Path Abs) ) ) (short 't' <> long "target" <> metavar "TARGET_DIR" <> help "Absolute filepath to write ghcup into" ) ) ) <|> (pure UpgradeGHCupDir) -- TODO: something better than Show instance for errors main :: IO () main = do customExecParser (prefs showHelpOnError) (info (opts <**> helper) idm) >>= \opt@Options {..} -> do let settings = toSettings opt -- logger interpreter logfile <- initGHCupFileLogging ([rel|ghcup.log|] :: Path Rel) let runLogger = myLoggerT LoggerConfig { lcPrintDebug = optVerbose , colorOutter = B.hPut stderr , rawOutter = appendFile logfile } -- wrapper to run effects with settings let runInstTool = runLogger . flip runReaderT settings . runResourceT . runE @'[ AlreadyInstalled , ArchiveError , DistroNotFound , FileDoesNotExistError , FileError , JSONError , NoCompatibleArch , NoDownload , NotInstalled , PlatformResultError , ProcessError , TagNotFound , URLException , DigestError ] let runSetGHC = runLogger . flip runReaderT settings . runE @'[ FileDoesNotExistError , NotInstalled , TagNotFound , URLException , JSONError , TagNotFound ] let runListGHC = runLogger . flip runReaderT settings . runE @'[FileDoesNotExistError , URLException , JSONError] let runRmGHC = runLogger . flip runReaderT settings . runE @'[NotInstalled] let runDebugInfo = runLogger . flip runReaderT settings . runE @'[PlatformResultError , NoCompatibleArch , DistroNotFound] let runCompileGHC = runLogger . flip runReaderT settings . runResourceT . runE @'[ AlreadyInstalled , NotInstalled , GHCNotFound , ArchiveError , ProcessError , URLException , DigestError , BuildConfigNotFound , FileDoesNotExistError , URLException , JSONError ] let runUpgrade = runLogger . flip runReaderT settings . runResourceT . runE @'[ DigestError , URLException , DistroNotFound , PlatformResultError , NoCompatibleArch , NoDownload , FileDoesNotExistError , JSONError ] case optCommand of Install (InstallGHC InstallOptions {..}) -> void $ (runInstTool $ do dls <- _binaryDownloads <$> liftE getDownloads v <- liftE $ fromVersion dls instVer GHC liftE $ installTool dls (ToolRequest GHC v) Nothing ) >>= \case VRight _ -> runLogger $ $(logInfo) ([s|GHC installation successful|]) VLeft (V (AlreadyInstalled treq)) -> runLogger $ $(logWarn) (T.pack (show treq) <> [s| already installed|]) VLeft e -> do runLogger $ do $(logError) [i|#{e}|] $(logError) [i|Also check the logs in ~/.ghcup/logs|] exitFailure Install (InstallCabal InstallOptions {..}) -> void $ (runInstTool $ do dls <- _binaryDownloads <$> liftE getDownloads v <- liftE $ fromVersion dls instVer Cabal liftE $ installTool dls (ToolRequest Cabal v) Nothing ) >>= \case VRight _ -> runLogger $ $(logInfo) ([s|Cabal installation successful|]) VLeft (V (AlreadyInstalled treq)) -> runLogger $ $(logWarn) (T.pack (show treq) <> [s| already installed|]) VLeft e -> do runLogger $ do $(logError) [i|#{e}|] $(logError) [i|Also check the logs in ~/.ghcup/logs|] exitFailure SetGHC (SetGHCOptions {..}) -> void $ (runSetGHC $ do dls <- _binaryDownloads <$> liftE getDownloads v <- liftE $ fromVersion dls ghcVer GHC liftE $ setGHC v SetGHCOnly ) >>= \case VRight _ -> runLogger $ $(logInfo) ([s|GHC successfully set|]) VLeft e -> runLogger ($(logError) [i|#{e}|]) >> exitFailure List (ListOptions {..}) -> void $ (runListGHC $ do dls <- _binaryDownloads <$> liftE getDownloads liftIO $ listVersions dls lTool lCriteria ) >>= \case VRight r -> liftIO $ printListResult r VLeft e -> runLogger ($(logError) [i|#{e}|]) >> exitFailure Rm (RmOptions {..}) -> void $ (runRmGHC $ do liftE $ rmGHCVer ghcVer ) >>= \case VRight _ -> pure () VLeft e -> runLogger ($(logError) [i|#{e}|]) >> exitFailure DInfo -> do void $ (runDebugInfo $ do liftE $ getDebugInfo ) >>= \case VRight dinfo -> putStrLn $ show dinfo VLeft e -> runLogger ($(logError) [i|#{e}|]) >> exitFailure Compile (CompileOptions {..}) -> void $ (runCompileGHC $ do dls <- _sourceDownloads <$> liftE getDownloads liftE $ compileGHC dls ghcVer bootstrapVer jobs buildConfig ) >>= \case VRight _ -> runLogger $ $(logInfo) ([s|GHC successfully compiled and installed|]) VLeft (V (AlreadyInstalled treq)) -> runLogger $ $(logWarn) (T.pack (show treq) <> [s| already installed|]) VLeft e -> runLogger ($(logError) [i|#{e}|]) >> exitFailure Upgrade (uOpts) -> do liftIO $ putStrLn $ show uOpts target <- case uOpts of UpgradeInplace -> do efp <- liftIO $ getExecutablePath p <- parseAbs . E.encodeUtf8 . T.pack $ efp pure $ Just p (UpgradeAt p) -> pure $ Just p UpgradeGHCupDir -> do bdir <- liftIO $ ghcupBinDir pure (Just (bdir ([rel|ghcup|] :: Path Rel))) void $ (runUpgrade $ do dls <- _binaryDownloads <$> liftE getDownloads liftE $ upgradeGHCup dls target ) >>= \case VRight v' -> do let pretty_v = prettyVer v' runLogger $ $(logInfo) [i|Successfully upgraded GHCup to version #{pretty_v}|] VLeft e -> runLogger ($(logError) [i|#{e}|]) >> exitFailure pure () fromVersion :: Monad m => BinaryDownloads -> Maybe ToolVersion -> Tool -> Excepts '[TagNotFound] m Version fromVersion av Nothing tool = getRecommended av tool ?? TagNotFound Recommended tool fromVersion _ (Just (ToolVersion v)) _ = pure v fromVersion av (Just (ToolTag Latest)) tool = getLatest av tool ?? TagNotFound Latest tool fromVersion av (Just (ToolTag Recommended)) tool = getRecommended av tool ?? TagNotFound Recommended tool printListResult :: [ListResult] -> IO () printListResult lr = do let formatted = gridString [ column expand left def def , column expand left def def , column expand left def def , column expand left def def ] $ fmap (\ListResult {..} -> [ if | lSet -> (color Green "✔✔") | lInstalled -> (color Green "✓") | otherwise -> (color Red "✗") , fmap toLower . show $ lTool , T.unpack . prettyVer $ lVer , intercalate "," $ ((fmap . fmap) toLower . fmap show $ lTag) ] ) lr putStrLn $ formatted