{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} -- | Utilities for running stack commands. -- -- Instead of using Has-style classes below, the type signatures use -- concrete environments to try and avoid accidentally rerunning -- configuration parsing. For example, we want @withConfig $ -- withConfig $ ...@ to fail. module Stack.Runners ( withBuildConfig , withEnvConfig , withDefaultEnvConfig , withConfig , withGlobalProject , withRunnerGlobal , ShouldReexec (..) ) where import RIO.Process ( mkDefaultProcessContext ) import RIO.Time ( addUTCTime, getCurrentTime ) import Stack.Build.Target ( NeedTargets (..) ) import Stack.Config import Stack.Constants import Stack.DefaultColorWhen ( defaultColorWhen ) import qualified Stack.Docker as Docker import qualified Stack.Nix as Nix import Stack.Prelude import Stack.Setup import Stack.Storage.User ( upgradeChecksSince, logUpgradeCheck ) import Stack.Types.Config import Stack.Types.Docker ( dockerEnable ) import Stack.Types.Nix ( nixEnable ) import Stack.Types.Version ( stackMinorVersion, minorVersion ) import System.Console.ANSI ( hSupportsANSIWithoutEmulation ) import System.Terminal ( getTerminalWidth ) -- | Type representing exceptions thrown by functions exported by the -- "Stack.Runners" module. data RunnersException = CommandInvalid | DockerAndNixInvalid | NixWithinDockerInvalid | DockerWithinNixInvalid deriving (Show, Typeable) instance Exception RunnersException where displayException CommandInvalid = "Error: [S-7144]\n" ++ "Cannot use this command with options which override the stack.yaml \ \location." displayException DockerAndNixInvalid = "Error: [S-8314]\n" ++ "Cannot use both Docker and Nix at the same time." displayException NixWithinDockerInvalid = "Error: [S-8641]\n" ++ "Cannot use Nix from within a Docker container." displayException DockerWithinNixInvalid = "Error: [S-5107]\n" ++ "Cannot use Docker from within a Nix shell." -- | Ensure that no project settings are used when running 'withConfig'. withGlobalProject :: RIO Runner a -> RIO Runner a withGlobalProject inner = do oldSYL <- view stackYamlLocL case oldSYL of SYLDefault -> local (set stackYamlLocL SYLGlobalProject) inner _ -> throwIO CommandInvalid -- | Helper for 'withEnvConfig' which passes in some default arguments: -- -- * No targets are requested -- -- * Default command line build options are assumed withDefaultEnvConfig :: RIO EnvConfig a -> RIO Config a withDefaultEnvConfig = withEnvConfig AllowNoTargets defaultBuildOptsCLI -- | Upgrade a 'Config' environment to an 'EnvConfig' environment by -- performing further parsing of project-specific configuration (like -- 'withBuildConfig') and then setting up a build environment -- toolchain. This is intended to be run inside a call to -- 'withConfig'. withEnvConfig :: NeedTargets -> BuildOptsCLI -> RIO EnvConfig a -- ^ Action that uses the build config. If Docker is enabled for builds, -- this will be run in a Docker container. -> RIO Config a withEnvConfig needTargets boptsCLI inner = withBuildConfig $ do envConfig <- setupEnv needTargets boptsCLI Nothing logDebug "Starting to execute command inside EnvConfig" runRIO envConfig inner -- | If the settings justify it, should we reexec inside Docker or Nix? data ShouldReexec = YesReexec | NoReexec -- | Load the configuration. Convenience function used -- throughout this module. withConfig :: ShouldReexec -> RIO Config a -> RIO Runner a withConfig shouldReexec inner = loadConfig $ \config -> do -- If we have been relaunched in a Docker container, perform in-container initialization -- (switch UID, etc.). We do this after first loading the configuration since it must -- happen ASAP but needs a configuration. view (globalOptsL.to globalDockerEntrypoint) >>= traverse_ (Docker.entrypoint config) runRIO config $ do -- Catching all exceptions here, since we don't want this -- check to ever cause Stack to stop working shouldUpgradeCheck `catchAny` \e -> logError $ "Error: [S-7353]\n" <> "Error when running shouldUpgradeCheck: " <> displayShow e case shouldReexec of YesReexec -> reexec inner NoReexec -> inner -- | Perform a Docker or Nix reexec, if warranted. Otherwise run the -- inner action. reexec :: RIO Config a -> RIO Config a reexec inner = do nixEnable' <- asks $ nixEnable . configNix dockerEnable' <- asks $ dockerEnable . configDocker case (nixEnable', dockerEnable') of (True, True) -> throwIO DockerAndNixInvalid (False, False) -> inner -- Want to use Nix (True, False) -> do whenM getInContainer $ throwIO NixWithinDockerInvalid isReexec <- view reExecL if isReexec then inner else Nix.runShellAndExit -- Want to use Docker (False, True) -> do whenM getInNixShell $ throwIO DockerWithinNixInvalid inContainer <- getInContainer if inContainer then do isReexec <- view reExecL if isReexec then inner else throwIO Docker.OnlyOnHostException else Docker.runContainerAndExit -- | Use the 'GlobalOpts' to create a 'Runner' and run the provided -- action. withRunnerGlobal :: GlobalOpts -> RIO Runner a -> IO a withRunnerGlobal go inner = do colorWhen <- maybe defaultColorWhen pure $ getFirst $ configMonoidColorWhen $ globalConfigMonoid go useColor <- case colorWhen of ColorNever -> pure False ColorAlways -> pure True ColorAuto -> fromMaybe True <$> hSupportsANSIWithoutEmulation stderr termWidth <- clipWidth <$> maybe (fromMaybe defaultTerminalWidth <$> getTerminalWidth) pure (globalTermWidth go) menv <- mkDefaultProcessContext let update = globalStylesUpdate go withNewLogFunc go useColor update $ \logFunc -> runRIO Runner { runnerGlobalOpts = go , runnerUseColor = useColor , runnerLogFunc = logFunc , runnerTermWidth = termWidth , runnerProcessContext = menv } inner where clipWidth w | w < minTerminalWidth = minTerminalWidth | w > maxTerminalWidth = maxTerminalWidth | otherwise = w -- | Check if we should recommend upgrading Stack and, if so, recommend it. shouldUpgradeCheck :: RIO Config () shouldUpgradeCheck = do config <- ask when (configRecommendUpgrade config) $ do now <- getCurrentTime let yesterday = addUTCTime (-24 * 60 * 60) now checks <- upgradeChecksSince yesterday when (checks == 0) $ do mversion <- getLatestHackageVersion NoRequireHackageIndex "stack" UsePreferredVersions case mversion of -- Compare the minor version so we avoid patch-level, Hackage-only releases. -- See: https://github.com/commercialhaskell/stack/pull/4729#pullrequestreview-227176315 Just (PackageIdentifierRevision _ version _) | minorVersion version > stackMinorVersion -> do logWarn "<<<<<<<<<<<<<<<<<<" logWarn $ "You are currently using Stack version " <> fromString (versionString stackVersion) <> ", but version " <> fromString (versionString version) <> " is available" logWarn "You can try to upgrade by running 'stack upgrade'" logWarn $ "Tired of seeing this? Add 'recommend-stack-upgrade: false' to " <> fromString (toFilePath (configUserConfigPath config)) logWarn ">>>>>>>>>>>>>>>>>>" logWarn "" logWarn "" _ -> pure () logUpgradeCheck now