{-# LANGUAGE FlexibleInstances, TypeOperators, ScopedTypeVariables, NamedFieldPuns #-}
{-# LANGUAGE GADTs, GeneralizedNewtypeDeriving, DeriveDataTypeable, RecordWildCards #-}
module Development.Shake.Command(
command, command_, cmd, cmd_, unit, CmdArgument(..), CmdArguments(..), IsCmdArgument(..), (:->),
Stdout(..), StdoutTrim(..), Stderr(..), Stdouterr(..), Exit(..), Process(..), CmdTime(..), CmdLine(..), FSATrace(..),
CmdResult, CmdString, CmdOption(..),
addPath, addEnv,
) where
import Data.Tuple.Extra
import Control.Monad.Extra
import Control.Monad.IO.Class
import Control.Exception.Extra
import Data.Char
import Data.Either.Extra
import Data.List.Extra
import Data.Maybe
import Data.Data
import Data.Semigroup
import System.Directory
import qualified System.IO.Extra as IO
import System.Environment
import System.Exit
import System.IO.Extra hiding (withTempFile, withTempDir)
import System.Process
import System.Info.Extra
import System.Time.Extra
import System.IO.Unsafe(unsafeInterleaveIO)
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as LBS
import General.Extra
import General.Process
import Prelude
import Development.Shake.Internal.CmdOption
import Development.Shake.Internal.Core.Action
import Development.Shake.Internal.Core.Types hiding (Result)
import Development.Shake.FilePath
import Development.Shake.Internal.Options
import Development.Shake.Internal.Rules.File
import Development.Shake.Internal.Derived
addPath :: MonadIO m => [String] -> [String] -> m CmdOption
addPath pre post = do
args <- liftIO getEnvironment
let (path,other) = partition ((== "PATH") . (if isWindows then upper else id) . fst) args
return $ Env $
[("PATH",intercalate [searchPathSeparator] $ pre ++ post) | null path] ++
[(a,intercalate [searchPathSeparator] $ pre ++ [b | b /= ""] ++ post) | (a,b) <- path] ++
other
addEnv :: MonadIO m => [(String, String)] -> m CmdOption
addEnv extra = do
args <- liftIO getEnvironment
return $ Env $ extra ++ filter (\(a,_) -> a `notElem` map fst extra) args
data Str = Str String | BS BS.ByteString | LBS LBS.ByteString | Unit deriving (Eq,Show)
strTrim :: Str -> Str
strTrim (Str x) = Str $ trim x
strTrim (BS x) = BS $ fst $ BS.spanEnd isSpace $ BS.dropWhile isSpace x
strTrim (LBS x) = LBS $ trimEnd $ LBS.dropWhile isSpace x
where
trimEnd x = case LBS.uncons x of
Just (c, x2) | isSpace c -> trimEnd x2
_ -> x
strTrim Unit = Unit
data Result
= ResultStdout Str
| ResultStderr Str
| ResultStdouterr Str
| ResultCode ExitCode
| ResultTime Double
| ResultLine String
| ResultProcess PID
| ResultFSATrace [FSATrace]
deriving (Eq,Show)
data PID = PID0 | PID ProcessHandle
instance Eq PID where _ == _ = True
instance Show PID where show PID0 = "PID0"; show _ = "PID"
data Params = Params
{funcName :: String
,opts :: [CmdOption]
,results :: [Result]
,prog :: String
,args :: [String]
} deriving Show
class MonadIO m => MonadTempDir m where runWithTempDir :: (FilePath -> m a) -> m a
instance MonadTempDir IO where runWithTempDir = IO.withTempDir
instance MonadTempDir Action where runWithTempDir = withTempDir
removeOptionShell
:: MonadTempDir m
=> Params
-> (Params -> m a)
-> m a
removeOptionShell params@Params{..} call
| Shell `elem` opts = do
let userCmdline = unwords $ prog : args
params <- return params{opts = UserCommand userCmdline : filter (/= Shell) opts}
prog <- liftIO $ if isFSATrace params then copyFSABinary prog else return prog
let realCmdline = unwords $ prog : args
if not isWindows then
call params{prog = "/bin/sh", args = ["-c",realCmdline]}
else
runWithTempDir $ \dir -> do
let file = dir </> "s.bat"
writeFile' file realCmdline
call params{prog = "cmd.exe", args = ["/d/q/c",file]}
| otherwise = call params
isFSATrace :: Params -> Bool
isFSATrace Params{..} = ResultFSATrace [] `elem` results || any isFSAOptions opts
copyFSABinary :: FilePath -> IO FilePath
copyFSABinary prog
| not isMac = return prog
| otherwise = do
progFull <- findExecutable prog
case progFull of
Just x | any (`isPrefixOf` x) ["/bin/","/usr/","/sbin/"] -> do
tmpdir <- getTemporaryDirectory
let fake = tmpdir </> "fsatrace-fakes" ++ x
unlessM (doesFileExist fake) $ do
createDirectoryRecursive $ takeDirectory fake
copyFile x fake
return fake
_ -> return prog
removeOptionFSATrace
:: MonadTempDir m
=> Params
-> (Params -> m [Result])
-> m [Result]
removeOptionFSATrace params@Params{..} call
| not $ isFSATrace params = call params
| ResultProcess PID0 `elem` results =
liftIO $ errorIO "Asyncronous process execution combined with FSATrace is not support"
| otherwise = runWithTempDir $ \dir -> do
let file = dir </> "fsatrace.txt"
liftIO $ writeFile file ""
params <- liftIO $ fsaParams file params
res <- call params{opts = UserCommand (showCommandForUser2 prog args) : filter (not . isFSAOptions) opts}
cwd <- liftIO getCurrentDirectory
fsaRes <- liftIO $ parseFSA <$> readFileUTF8' file
return $ replace [ResultFSATrace []] [ResultFSATrace fsaRes] res
where
fsaFlags = fromMaybe "rwmdqt" fsaOptions
fsaOptions = last $ Nothing : [Just x | FSAOptions x <- opts]
fsaParams file Params{..} = do
prog <- copyFSABinary prog
return params{prog = "fsatrace", args = fsaFlags : file : "--" : prog : args }
isFSAOptions FSAOptions{} = True
isFSAOptions _ = False
addFSAOptions :: String -> [CmdOption] -> [CmdOption]
addFSAOptions x opts | any isFSAOptions opts = map f opts
where f (FSAOptions y) = FSAOptions $ nubOrd $ y ++ x
f x = x
addFSAOptions x opts = FSAOptions x : opts
data FSATrace
=
FSAWrite FilePath
|
FSARead FilePath
|
FSADelete FilePath
|
FSAMove FilePath FilePath
|
FSAQuery FilePath
|
FSATouch FilePath
deriving (Show,Eq,Ord,Data,Typeable)
parseFSA :: String -> [FSATrace]
parseFSA = mapMaybe f . lines
where f ('w':'|':xs) = Just $ FSAWrite xs
f ('r':'|':xs) = Just $ FSARead xs
f ('d':'|':xs) = Just $ FSADelete xs
f ('m':'|':xs) | (xs,'|':ys) <- break (== '|') xs = Just $ FSAMove xs ys
f ('q':'|':xs) = Just $ FSAQuery xs
f ('t':'|':xs) = Just $ FSATouch xs
f _ = Nothing
commandExplicitAction :: Params -> Action [Result]
commandExplicitAction oparams = do
ShakeOptions{shakeCommandOptions,shakeRunCommands,shakeLint,shakeLintInside} <- getShakeOptions
params@Params{..} <- return $ oparams{opts = shakeCommandOptions ++ opts oparams}
let skipper act = if null results && not shakeRunCommands then return [] else act
let verboser act = do
let cwd = listToMaybe $ reverse [x | Cwd x <- opts]
putLoud $
maybe "" (\x -> "cd " ++ x ++ "; ") cwd ++
last (showCommandForUser2 prog args : [x | UserCommand x <- opts])
verb <- getVerbosity
(if verb >= Loud then quietly else id) act
let tracer act = do
let msg = last $ defaultTraced oparams : [x | Traced x <- opts]
if msg == "" then liftIO act else traced msg act
let async = ResultProcess PID0 `elem` results
let tracker act
| AutoDeps `elem` opts = if async then fail "Can't use AutoDeps and asyncronous execution" else autodeps act
| shakeLint == Just LintFSATrace && not async = fsalint act
| otherwise = act params
autodeps act = do
ResultFSATrace pxs : res <- act params{opts = addFSAOptions "r" opts, results = ResultFSATrace [] : results}
xs <- liftIO $ filterM doesFileExist [x | FSARead x <- pxs]
cwd <- liftIO getCurrentDirectory
unsafeAllowApply . need =<< fixPaths cwd xs
return res
fixPaths cwd xs = liftIO $ do
xs <- return $ map toStandard xs
xs <- return $ filter (\x -> any (`isPrefixOf` x) shakeLintInside) xs
mapM (\x -> fromMaybe x <$> makeRelativeEx cwd x) xs
fsalint act = do
ResultFSATrace xs : res <- act params{opts = addFSAOptions "rwm" opts, results = ResultFSATrace [] : results}
let reader (FSARead x) = Just x; reader _ = Nothing
writer (FSAWrite x) = Just x; writer (FSAMove x _) = Just x; writer _ = Nothing
existing f = liftIO . filterM doesFileExist . nubOrd . mapMaybe f
cwd <- liftIO getCurrentDirectory
trackRead =<< fixPaths cwd =<< existing reader xs
trackWrite =<< fixPaths cwd =<< existing writer xs
return res
skipper $ tracker $ \params -> verboser $ tracer $ commandExplicitIO params
defaultTraced :: Params -> String
defaultTraced Params{..} = takeBaseName $ if Shell `elem` opts then fst (word1 prog) else prog
commandExplicitIO :: Params -> IO [Result]
commandExplicitIO params = removeOptionShell params $ \params -> removeOptionFSATrace params $ \Params{..} -> do
let (grabStdout, grabStderr) = both or $ unzip $ flip map results $ \r -> case r of
ResultStdout{} -> (True, False)
ResultStderr{} -> (False, True)
ResultStdouterr{} -> (True, True)
_ -> (False, False)
optEnv <- resolveEnv opts
let optCwd = mergeCwd [x | Cwd x <- opts]
let optStdin = flip mapMaybe opts $ \x -> case x of
Stdin x -> Just $ SrcString x
StdinBS x -> Just $ SrcBytes x
FileStdin x -> Just $ SrcFile x
_ -> Nothing
let optBinary = BinaryPipes `elem` opts
let optAsync = ResultProcess PID0 `elem` results
let optTimeout = listToMaybe $ reverse [x | Timeout x <- opts]
let optWithStdout = last $ False : [x | WithStdout x <- opts]
let optWithStderr = last $ True : [x | WithStderr x <- opts]
let optFileStdout = [x | FileStdout x <- opts]
let optFileStderr = [x | FileStderr x <- opts]
let optEchoStdout = last $ (not grabStdout && null optFileStdout) : [x | EchoStdout x <- opts]
let optEchoStderr = last $ (not grabStderr && null optFileStderr) : [x | EchoStderr x <- opts]
let optRealCommand = showCommandForUser2 prog args
let optUserCommand = last $ optRealCommand : [x | UserCommand x <- opts]
let bufLBS f = do (a,b) <- buf $ LBS LBS.empty; return (a, (\(LBS x) -> f x) <$> b)
buf Str{} | optBinary = bufLBS (Str . LBS.unpack)
buf Str{} = do x <- newBuffer; return ([DestString x | not optAsync], Str . concat <$> readBuffer x)
buf LBS{} = do x <- newBuffer; return ([DestBytes x | not optAsync], LBS . LBS.fromChunks <$> readBuffer x)
buf BS {} = bufLBS (BS . BS.concat . LBS.toChunks)
buf Unit = return ([], return Unit)
(dStdout, dStderr, resultBuild) :: ([[Destination]], [[Destination]], [Double -> ProcessHandle -> ExitCode -> IO Result]) <-
fmap unzip3 $ forM results $ \r -> case r of
ResultCode _ -> return ([], [], \_ _ ex -> return $ ResultCode ex)
ResultTime _ -> return ([], [], \dur _ _ -> return $ ResultTime dur)
ResultLine _ -> return ([], [], \_ _ _ -> return $ ResultLine optUserCommand)
ResultProcess _ -> return ([], [], \_ pid _ -> return $ ResultProcess $ PID pid)
ResultStdout s -> do (a,b) <- buf s; return (a , [], \_ _ _ -> fmap ResultStdout b)
ResultStderr s -> do (a,b) <- buf s; return ([], a , \_ _ _ -> fmap ResultStderr b)
ResultStdouterr s -> do (a,b) <- buf s; return (a , a , \_ _ _ -> fmap ResultStdouterr b)
ResultFSATrace _ -> return ([], [], \_ _ _ -> return $ ResultFSATrace [])
exceptionBuffer <- newBuffer
po <- resolvePath ProcessOpts
{poCommand = RawCommand prog args
,poCwd = optCwd, poEnv = optEnv, poTimeout = optTimeout
,poStdin = [SrcBytes LBS.empty | optBinary && not (null optStdin)] ++ optStdin
,poStdout = [DestEcho | optEchoStdout] ++ map DestFile optFileStdout ++ [DestString exceptionBuffer | optWithStdout && not optAsync] ++ concat dStdout
,poStderr = [DestEcho | optEchoStderr] ++ map DestFile optFileStderr ++ [DestString exceptionBuffer | optWithStderr && not optAsync] ++ concat dStderr
,poAsync = optAsync
}
(dur,(pid,exit)) <- duration $ process po
if exit == ExitSuccess || ResultCode ExitSuccess `elem` results then
mapM (\f -> f dur pid exit) resultBuild
else do
exceptionBuffer <- readBuffer exceptionBuffer
let captured = ["Stderr" | optWithStderr] ++ ["Stdout" | optWithStdout]
cwd <- case optCwd of
Nothing -> return ""
Just v -> do
v <- canonicalizePath v `catchIO` const (return v)
return $ "Current directory: " ++ v ++ "\n"
fail $
"Development.Shake." ++ funcName ++ ", system command failed\n" ++
"Command line: " ++ optRealCommand ++ "\n" ++
(if optRealCommand /= optUserCommand then "Original command line: " ++ optUserCommand ++ "\n" else "") ++
cwd ++
"Exit code: " ++ show (case exit of ExitFailure i -> i; _ -> 0) ++ "\n" ++
if null captured then "Stderr not captured because WithStderr False was used\n"
else if null exceptionBuffer then intercalate " and " captured ++ " " ++ (if length captured == 1 then "was" else "were") ++ " empty"
else intercalate " and " captured ++ ":\n" ++ unlines (dropWhile null $ lines $ concat exceptionBuffer)
mergeCwd :: [FilePath] -> Maybe FilePath
mergeCwd [] = Nothing
mergeCwd xs = Just $ foldl1 (</>) xs
resolveEnv :: [CmdOption] -> IO (Maybe [(String, String)])
resolveEnv opts
| null env, null addEnv, null addPath, null remEnv = return Nothing
| otherwise = Just . unique . tweakPath . (++ addEnv) . filter (flip notElem remEnv . fst) <$>
if null env then getEnvironment else return (concat env)
where
env = [x | Env x <- opts]
addEnv = [(x,y) | AddEnv x y <- opts]
remEnv = [x | RemEnv x <- opts]
addPath = [(x,y) | AddPath x y <- opts]
newPath mid = intercalate [searchPathSeparator] $
concat (reverse $ map fst addPath) ++ [mid | mid /= ""] ++ concatMap snd addPath
isPath x = (if isWindows then upper else id) x == "PATH"
tweakPath xs | not $ any (isPath . fst) xs = ("PATH", newPath "") : xs
| otherwise = map (\(a,b) -> (a, if isPath a then newPath b else b)) xs
unique = reverse . nubOrdOn (if isWindows then upper . fst else fst) . reverse
resolvePath :: ProcessOpts -> IO ProcessOpts
resolvePath po
| Just e <- poEnv po
, Just (_, path) <- find ((==) "PATH" . (if isWindows then upper else id) . fst) e
, RawCommand prog args <- poCommand po
= do
let progExe = if prog == prog -<.> exe then prog else prog <.> exe
pathOld <- unsafeInterleaveIO $ fromMaybe "" <$> lookupEnv "PATH"
old <- unsafeInterleaveIO $ findExecutable prog
new <- unsafeInterleaveIO $ findExecutableWith (splitSearchPath path) progExe
old2 <- unsafeInterleaveIO $ findExecutableWith (splitSearchPath pathOld) progExe
switch <- return $ case () of
_ | path == pathOld -> False
| Nothing <- new -> False
| Nothing <- old -> True
| Just old <- old, Just new <- new, equalFilePath old new -> False
| Just old <- old, Just old2 <- old2, equalFilePath old old2 -> True
| otherwise -> False
return $ case new of
Just new | switch -> po{poCommand = RawCommand new args}
_ -> po
resolvePath po = return po
findExecutableWith :: [FilePath] -> String -> IO (Maybe FilePath)
findExecutableWith path x = flip firstJustM (map (</> x) path) $ \s ->
ifM (doesFileExist s) (return $ Just s) (return Nothing)
newtype Stdout a = Stdout {fromStdout :: a}
newtype StdoutTrim a = StdoutTrim {fromStdoutTrim :: a}
newtype Stderr a = Stderr {fromStderr :: a}
newtype Stdouterr a = Stdouterr {fromStdouterr :: a}
newtype Exit = Exit {fromExit :: ExitCode}
newtype Process = Process {fromProcess :: ProcessHandle}
newtype CmdTime = CmdTime {fromCmdTime :: Double}
newtype CmdLine = CmdLine {fromCmdLine :: String}
class CmdString a where cmdString :: (Str, Str -> a)
instance CmdString () where cmdString = (Unit, \Unit -> ())
instance CmdString String where cmdString = (Str "", \(Str x) -> x)
instance CmdString BS.ByteString where cmdString = (BS BS.empty, \(BS x) -> x)
instance CmdString LBS.ByteString where cmdString = (LBS LBS.empty, \(LBS x) -> x)
class Unit a
instance {-# OVERLAPPING #-} Unit b => Unit (a -> b)
instance {-# OVERLAPPABLE #-} a ~ () => Unit (m a)
class CmdResult a where
cmdResult :: ([Result], [Result] -> a)
instance CmdResult Exit where
cmdResult = ([ResultCode ExitSuccess], \[ResultCode x] -> Exit x)
instance CmdResult ExitCode where
cmdResult = ([ResultCode ExitSuccess], \[ResultCode x] -> x)
instance CmdResult Process where
cmdResult = ([ResultProcess PID0], \[ResultProcess (PID x)] -> Process x)
instance CmdResult ProcessHandle where
cmdResult = ([ResultProcess PID0], \[ResultProcess (PID x)] -> x)
instance CmdResult CmdLine where
cmdResult = ([ResultLine ""], \[ResultLine x] -> CmdLine x)
instance CmdResult CmdTime where
cmdResult = ([ResultTime 0], \[ResultTime x] -> CmdTime x)
instance CmdResult [FSATrace] where
cmdResult = ([ResultFSATrace []], \[ResultFSATrace x] -> x)
instance CmdString a => CmdResult (Stdout a) where
cmdResult = let (a,b) = cmdString in ([ResultStdout a], \[ResultStdout x] -> Stdout $ b x)
instance CmdString a => CmdResult (StdoutTrim a) where
cmdResult = let (a,b) = cmdString in ([ResultStdout a], \[ResultStdout x] -> StdoutTrim $ b $ strTrim x)
instance CmdString a => CmdResult (Stderr a) where
cmdResult = let (a,b) = cmdString in ([ResultStderr a], \[ResultStderr x] -> Stderr $ b x)
instance CmdString a => CmdResult (Stdouterr a) where
cmdResult = let (a,b) = cmdString in ([ResultStdouterr a], \[ResultStdouterr x] -> Stdouterr $ b x)
instance CmdResult () where
cmdResult = ([], \[] -> ())
instance (CmdResult x1, CmdResult x2) => CmdResult (x1,x2) where
cmdResult = (a1++a2, \rs -> let (r1,r2) = splitAt (length a1) rs in (b1 r1, b2 r2))
where (a1,b1) = cmdResult
(a2,b2) = cmdResult
cmdResultWith :: forall b c. CmdResult b => (b -> c) -> ([Result], [Result] -> c)
cmdResultWith f = second (f .) cmdResult
instance (CmdResult x1, CmdResult x2, CmdResult x3) => CmdResult (x1,x2,x3) where
cmdResult = cmdResultWith $ \(a,(b,c)) -> (a,b,c)
instance (CmdResult x1, CmdResult x2, CmdResult x3, CmdResult x4) => CmdResult (x1,x2,x3,x4) where
cmdResult = cmdResultWith $ \(a,(b,c,d)) -> (a,b,c,d)
instance (CmdResult x1, CmdResult x2, CmdResult x3, CmdResult x4, CmdResult x5) => CmdResult (x1,x2,x3,x4,x5) where
cmdResult = cmdResultWith $ \(a,(b,c,d,e)) -> (a,b,c,d,e)
command :: CmdResult r => [CmdOption] -> String -> [String] -> Action r
command opts x xs = b <$> commandExplicitAction (Params "command" opts a x xs)
where (a,b) = cmdResult
command_ :: [CmdOption] -> String -> [String] -> Action ()
command_ opts x xs = void $ commandExplicitAction (Params "command_" opts [] x xs)
type a :-> t = a
cmd :: CmdArguments args => args :-> Action r
cmd = cmdArguments mempty
cmd_ :: (CmdArguments args, Unit args) => args :-> Action ()
cmd_ = cmd
newtype CmdArgument = CmdArgument [Either CmdOption String]
deriving (Eq, Semigroup, Monoid, Show)
class CmdArguments t where
cmdArguments :: CmdArgument -> t
instance (IsCmdArgument a, CmdArguments r) => CmdArguments (a -> r) where
cmdArguments xs x = cmdArguments $ xs `mappend` toCmdArgument x
instance CmdResult r => CmdArguments (Action r) where
cmdArguments (CmdArgument x) = case partitionEithers x of
(opts, x:xs) -> let (a,b) = cmdResult in b <$> commandExplicitAction (Params "cmd" opts a x xs)
_ -> error "Error, no executable or arguments given to Development.Shake.cmd"
instance CmdResult r => CmdArguments (IO r) where
cmdArguments (CmdArgument x) = case partitionEithers x of
(opts, x:xs) -> let (a,b) = cmdResult in b <$> commandExplicitIO (Params "cmd" opts a x xs)
_ -> error "Error, no executable or arguments given to Development.Shake.cmd"
instance CmdArguments CmdArgument where
cmdArguments = id
class IsCmdArgument a where
toCmdArgument :: a -> CmdArgument
instance IsCmdArgument String where toCmdArgument = CmdArgument . map Right . words
instance IsCmdArgument [String] where toCmdArgument = CmdArgument . map Right
instance IsCmdArgument CmdOption where toCmdArgument = CmdArgument . return . Left
instance IsCmdArgument [CmdOption] where toCmdArgument = CmdArgument . map Left
instance IsCmdArgument a => IsCmdArgument (Maybe a) where toCmdArgument = maybe mempty toCmdArgument
showCommandForUser2 :: FilePath -> [String] -> String
showCommandForUser2 cmd args = unwords $ map (\x -> if safe x then x else showCommandForUser x []) $ cmd : args
where
safe xs = xs /= "" && not (any bad xs)
bad x = isSpace x || (x == '\\' && not isWindows) || x `elem` "\"\'"