build-env-1.0.0.0: src/BuildEnv/Utils.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
-- |
-- Module : BuildEnv.Utils
-- Description : Utilities for @build-env@
--
-- Various utilities:
--
-- - Spawning of processes in particular environments; see 'callProcessInIO'.
-- - Semaphores.
--
module BuildEnv.Utils
( -- * Call a process in a given environment
ProgPath(..), CallProcess(..), callProcessInIO
-- * Create temporary directories
, TempDirPermanence(..), withTempDir
-- * Abstract semaphores
, AbstractSem(..)
, newAbstractSem, noSem, abstractQSem
-- * Other utilities
, splitOn
) where
-- base
import Control.Concurrent.QSem
( QSem, newQSem, signalQSem, waitQSem )
import Control.Exception
( bracket_ )
import Data.List
( intercalate )
import Data.Maybe
( fromMaybe )
import Data.IORef
( readIORef )
import System.Environment
( getEnvironment )
import System.Exit
( ExitCode(..), exitWith )
import System.IO
( IOMode(..), hPutStrLn, withFile )
import qualified System.IO as System.Handle
( stderr )
import GHC.IO.Handle
( hDuplicateTo )
import GHC.Stack
( HasCallStack )
-- containers
import Data.Map.Strict
( Map )
import qualified Data.Map.Strict as Map
( alter, fromList, toList )
-- directory
import System.Directory
( createDirectoryIfMissing, makeAbsolute )
-- filepath
import System.FilePath
( (</>), (<.>), takeDirectory )
-- process
import qualified System.Process as Proc
-- temporary
import System.IO.Temp
( createTempDirectory
, getCanonicalTemporaryDirectory
, withSystemTempDirectory
)
-- build-env
import BuildEnv.Config
( AsyncSem(..), Args, TempDirPermanence(..), Counter(..)
, pATHSeparator, hostStyle
)
--------------------------------------------------------------------------------
-- | The path of a program to run.
data ProgPath
-- | An absolute path, or an executable in @PATH@.
= AbsPath { progPath :: !FilePath }
-- | A relative path. What it is relative to depends on context.
| RelPath { progPath :: !FilePath }
-- | Arguments to 'callProcess'.
data CallProcess
= CP
{ cwd :: !FilePath
-- ^ Working directory.
, extraPATH :: ![FilePath]
-- ^ Absolute filepaths to add to PATH.
, extraEnvVars :: ![(String, String)]
-- ^ Extra envi!ronment variables to add before running the command.
, prog :: !ProgPath
-- ^ The program to run.
--
-- If it's a relative path, it should be relative to the @cwd@ field.
, args :: !Args
-- ^ Arguments to the program.
, logBasePath :: !( Maybe FilePath )
-- ^ Log @stdout@ to @basePath.stdout@ and @stderr@ to @basePath.stderr@.
, sem :: !AbstractSem
-- ^ Lock to take when calling the process
-- and waiting for it to return, to avoid
-- contention in concurrent situations.
}
-- | Run a command and wait for it to complete.
--
-- Crashes if the process returns with non-zero exit code.
--
-- See 'CallProcess' for a description of the options.
callProcessInIO :: HasCallStack
=> Maybe Counter
-- ^ Optional counter. Used when the command fails,
-- to report the progress that has been made so far.
-> CallProcess
-> IO ()
callProcessInIO mbCounter ( CP { cwd, extraPATH, extraEnvVars, prog, args, logBasePath, sem } ) = do
absProg <-
case prog of
AbsPath p -> return p
RelPath p -> makeAbsolute $ cwd </> p
-- Needs to be an absolute path, as per the @process@ documentation:
--
-- If cwd is provided, it is implementation-dependent whether
-- relative paths are resolved with respect to cwd or the current
-- working directory, so absolute paths should be used
-- to ensure portability.
--
-- We always want the program to be interpreted relative to the cwd
-- argument, so we prepend @cwd@ and then make it absolute.
let argsStr
| null args = ""
| otherwise = " " ++ unwords args
command =
[ " > " ++ absProg ++ argsStr
, " CWD = " ++ cwd ]
env <-
if null extraPATH && null extraEnvVars
then return Nothing
else do env0 <- getEnvironment
let env1 = Map.fromList $ env0 ++ extraEnvVars
env2 = Map.toList $ augmentSearchPath "PATH" extraPATH env1
return $ Just env2
let withHandles :: ( ( Proc.StdStream, Proc.StdStream ) -> IO () ) -> IO ()
withHandles action = case logBasePath of
Nothing -> action ( Proc.Inherit, Proc.Inherit )
Just logPath -> do
let stdoutFile = logPath <.> "stdout"
stderrFile = logPath <.> "stderr"
createDirectoryIfMissing True $ takeDirectory logPath
withFile stdoutFile AppendMode \ stdoutFileHandle ->
withFile stderrFile AppendMode \ stderrFileHandle -> do
hDuplicateTo System.Handle.stderr stderrFileHandle
-- Write stderr to the log file and to the terminal.
hPutStrLn stdoutFileHandle ( unlines command )
action ( Proc.UseHandle stdoutFileHandle, Proc.UseHandle stderrFileHandle )
withHandles \ ( stdoutStream, stderrStream ) -> do
let processArgs =
( Proc.proc absProg args )
{ Proc.cwd = if cwd == "." then Nothing else Just cwd
, Proc.env = env
, Proc.std_out = stdoutStream
, Proc.std_err = stderrStream }
res <- withAbstractSem sem do
(_, _, _, ph) <- Proc.createProcess_ "createProcess" processArgs
-- Use 'createProcess_' to avoid closing handles prematurely.
Proc.waitForProcess ph
case res of
ExitSuccess -> return ()
ExitFailure i -> do
progressReport <-
case mbCounter of
Nothing -> return []
Just ( Counter { counterRef, counterMax } ) -> do
progress <- readIORef counterRef
return $ [ "After " <> show progress <> " of " <> show counterMax ]
let msg = [ "callProcess failed with non-zero exit code " ++ show i ++ ". Command:" ]
++ command ++ progressReport
case stderrStream of
Proc.UseHandle errHandle ->
hPutStrLn errHandle
( unlines $ msg ++ [ "Logs are available at: " <> fromMaybe "" logBasePath <> ".{stdout, stderr}" ] )
_ -> putStrLn (unlines msg)
exitWith res
-- | Add filepaths to the given key in a key/value environment.
augmentSearchPath :: Ord k => k -> [FilePath] -> Map k String -> Map k String
augmentSearchPath _ [] = id
augmentSearchPath var paths = Map.alter f var
where
pathsVal = intercalate (pATHSeparator hostStyle) paths
f Nothing = Just pathsVal
f (Just p) = Just (p <> (pATHSeparator hostStyle) <> pathsVal)
-- | Perform an action with a fresh temporary directory.
withTempDir :: TempDirPermanence -- ^ whether to delete the temporary directory
-- after the action completes
-> String -- ^ directory name template
-> (FilePath -> IO a) -- ^ action to perform
-> IO a
withTempDir del name k =
case del of
DeleteTempDirs
-> withSystemTempDirectory name k
Don'tDeleteTempDirs
-> do root <- getCanonicalTemporaryDirectory
createTempDirectory root name >>= k
-- | Utility list 'splitOn' function.
splitOn :: Char -> String -> [String]
splitOn c = go
where
go "" = []
go s
| (a,as) <- break (== c) s
= a : go (drop 1 as)
--------------------------------------------------------------------------------
-- Semaphores.
-- | Abstract acquire/release mechanism.
newtype AbstractSem =
AbstractSem { withAbstractSem :: forall r. IO r -> IO r }
-- | Create a semaphore-based acquire/release mechanism.
newAbstractSem :: AsyncSem -> IO AbstractSem
newAbstractSem whatSem =
case whatSem of
NoSem -> return noSem
NewQSem n -> do
qsem <- newQSem ( fromIntegral n )
return $ abstractQSem qsem
-- | Abstract acquire/release mechanism controlled by the given 'QSem'.
abstractQSem :: QSem -> AbstractSem
abstractQSem sem =
AbstractSem $ bracket_ (waitQSem sem) (signalQSem sem)
-- | No acquire/release mechanism required.
noSem :: AbstractSem
noSem = AbstractSem { withAbstractSem = id }