{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedRecordDot #-} {-# 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 qualified Data.ByteString.Lazy.Char8 as L8 import RIO.Process ( findExecutable, mkDefaultProcessContext, proc , readProcess ) import RIO.Time ( addUTCTime, getCurrentTime ) import Stack.Build.Target ( NeedTargets (..) ) import Stack.Config ( getInContainer, getInNixShell, loadConfig, withBuildConfig , withNewLogFunc ) import Stack.Constants ( defaultTerminalWidth, maxTerminalWidth, minTerminalWidth , nixProgName ) import Stack.DefaultColorWhen ( defaultColorWhen ) import qualified Stack.Docker as Docker import qualified Stack.Nix as Nix import Stack.Prelude import Stack.Setup ( setupEnv ) import Stack.Storage.User ( logUpgradeCheck, upgradeChecksSince ) import Stack.Types.BuildOptsCLI ( BuildOptsCLI, defaultBuildOptsCLI ) import Stack.Types.ColorWhen ( ColorWhen (..) ) import Stack.Types.Config ( Config (..) ) import Stack.Types.ConfigMonoid ( ConfigMonoid (..) ) import Stack.Types.Docker ( DockerOpts (..) ) import Stack.Types.EnvConfig ( EnvConfig ) import Stack.Types.GlobalOpts ( GlobalOpts (..) ) import Stack.Types.Nix ( NixOpts (..) ) import Stack.Types.Runner ( Runner (..), globalOptsL, reExecL, stackYamlLocL ) import Stack.Types.StackYamlLoc ( StackYamlLoc (..) ) import Stack.Types.Version ( minorVersion, stackMinorVersion, stackVersion ) import System.Console.ANSI ( hNowSupportsANSI ) 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 (.dockerEntrypoint)) >>= 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 $ (.nix.enable) notifyIfNixOnPath <- asks (.notifyIfNixOnPath) when (not nixEnable' && notifyIfNixOnPath) $ do eNix <- findExecutable nixProgName case eNix of Left _ -> pure () Right nix -> proc nix ["--version"] $ \pc -> do let nixProgName' = style Shell (fromString nixProgName) muteMsg = fillSep [ flow "To mute this message in future, set" , style Shell (flow "notify-if-nix-on-path: false") , flow "in Stack's configuration." ] reportErr errMsg = prettyWarn $ fillSep [ nixProgName' , flow "is on the PATH" , parens (fillSep ["at", style File (fromString nix)]) , flow "but Stack encountered the following error with" , nixProgName' , style Shell "--version" <> ":" ] <> blankLine <> errMsg <> blankLine <> muteMsg <> line res <- tryAny (readProcess pc) case res of Left e -> reportErr (ppException e) Right (ec, out, err) -> case ec of ExitFailure _ -> reportErr $ string (L8.unpack err) ExitSuccess -> do let trimFinalNewline str = case reverse str of '\n' : rest -> reverse rest _ -> str prettyWarn $ fillSep [ fromString (trimFinalNewline $ L8.unpack out) , flow "is on the PATH" , parens (fillSep ["at", style File (fromString nix)]) , flow "but Stack's Nix integration is disabled." , muteMsg ] <> line dockerEnable' <- asks (.docker.enable) 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 go.configMonoid.colorWhen useColor <- case colorWhen of ColorNever -> pure False ColorAlways -> pure True ColorAuto -> hNowSupportsANSI stderr termWidth <- clipWidth <$> maybe (fromMaybe defaultTerminalWidth <$> getTerminalWidth) pure go.termWidthOpt menv <- mkDefaultProcessContext -- MVar used to ensure the Docker entrypoint is performed exactly once. dockerEntrypointMVar <- newMVar False let update = go.stylesUpdate withNewLogFunc go useColor update $ \logFunc -> do runRIO Runner { globalOpts = go , useColor = useColor , logFunc = logFunc , termWidth = termWidth , processContext = menv , dockerEntrypointMVar = dockerEntrypointMVar } 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 config.recommendUpgrade $ 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 prettyWarn $ fillSep [ flow "You are currently using Stack version" , fromString (versionString stackVersion) , flow "but version" , fromString (versionString version) , flow "is available." ] <> blankLine <> fillSep [ "You can try to upgrade by running" , style Shell (flow "stack upgrade") ] <> blankLine <> fillSep [ flow "Tired of seeing this? Add" , style Shell (flow "recommend-stack-upgrade: false") , "to" , pretty config.userConfigPath <> "." ] <> blankLine _ -> pure () logUpgradeCheck now