1{-# LANGUAGE LambdaCase #-}23module Mach.Exec4 ( mkConfig,5 maybeBuild,6 targetOrFile,7 ExecConfig,8 )9where1011import Control.Exception (catch, throwIO)12import Control.Monad (filterM, unless, when)13import Data.List (find)14import Data.Maybe (catMaybes)15import Mach.Error (MakeErr (..))16import Mach.Eval17import qualified Mach.Types as T18import System.Directory (doesPathExist, getModificationTime)19import System.Exit (ExitCode (ExitFailure, ExitSuccess))20import System.IO (Handle, hFlush, hPutStrLn, stderr, stdout)21import System.Process (ProcessHandle, StdStream (UseHandle), createProcess_, shell, std_out, waitForProcess)2223-- | Configuration regarding the execution of Makefiles.24data ExecConfig = ExecConfig25 { -- | Handle used for all output26 output :: Handle,27 -- | Continue execution of indepentent targets on error.28 contExec :: Bool,29 -- | Command line flags.30 flags :: [T.Flag],31 -- | Print commands instead of executing them32 dryRun :: Bool,33 -- | Silent targets (.SILENT special target)34 silenced :: Maybe [String],35 -- | Ignored targets (.IGNORE special target)36 ignored :: Maybe [String],37 -- | .PHONY targets.38 phonies :: [String]39 }4041mkConfig :: MkDef -> Handle -> [T.Flag] -> ExecConfig42mkConfig mkDef handle cflags =43 let cnExec = not $ execTerminate cflags44 ignAll = not $ null [() | T.IgnoreAll <- cflags]45 slnAll = not $ null [() | T.SilentAll <- cflags]46 noExec = not $ null [() | T.DryRun <- cflags]47 in ExecConfig48 { output = handle,49 contExec = cnExec,50 flags = cflags,51 dryRun = noExec,52 silenced = if slnAll then Just [] else silent mkDef,53 ignored = if ignAll then Just [] else ignore mkDef,54 phonies = phony mkDef55 }56 where57 isTerminate :: T.Flag -> Bool58 isTerminate T.TermOnErr = True59 isTerminate _ = False6061 -- Implements check for precedence of -k and -S.62 execTerminate :: [T.Flag] -> Bool63 execTerminate =64 maybe True isTerminate65 . find66 ( \case67 T.IgnoreAll -> True68 T.ExecCont -> True69 _ -> False70 )7172isSilent :: ExecConfig -> Target -> Bool73isSilent ExecConfig {silenced = s} tgt =74 case s of75 Nothing -> False76 Just [] -> True77 Just xs -> getName tgt `elem` xs7879isIgnored :: ExecConfig -> Target -> Bool80isIgnored ExecConfig {ignored = s} tgt =81 case s of82 Nothing -> False83 Just [] -> True84 Just xs -> getName tgt `elem` xs8586isPhony :: ExecConfig -> Target -> Bool87isPhony ExecConfig {phonies = lst} tgt = getName tgt `elem` lst8889makeProc :: ExecConfig -> String -> IO ProcessHandle90makeProc ExecConfig {output = handle} cmd = do91 (_, _, _, p) <-92 if handle == stdout93 then createProcess_ [] (shell cmd)94 else createProcess_ [] (shell cmd) {std_out = UseHandle handle}95 pure p9697printCmd :: ExecConfig -> Cmd -> IO ()98printCmd ExecConfig {output = handle} cmd =99 hPutStrLn handle (show cmd) >> hFlush handle100101callCmd :: ExecConfig -> Target -> Cmd -> IO ()102callCmd conf tgt cmd = do103 p <- makeProc conf $ cmdShell cmd104 exitCode <- waitForProcess p105 case exitCode of106 ExitSuccess -> pure ()107 ExitFailure _ ->108 unless (cmdIgnore cmd || isIgnored conf tgt) $109 throwIO $110 ExecErr ("non-zero exit: " ++ show cmd)111112runCmd :: ExecConfig -> Target -> Cmd -> IO ()113runCmd conf = if dryRun conf then dryRunCmd else execCmd114 where115 execCmd :: Target -> Cmd -> IO ()116 execCmd tgt cmd = do117 unless (cmdSilent cmd || isSilent conf tgt) $118 printCmd conf cmd119120 callCmd conf tgt cmd121122 dryRunCmd :: Target -> Cmd -> IO ()123 dryRunCmd tgt cmd = do124 printCmd conf cmd125 when (cmdExec cmd) $126 callCmd conf tgt cmd127128runTarget :: ExecConfig -> MkDef -> Target -> IO ()129runTarget conf mk tgt = mapM_ (runCmd conf tgt) (getCmds mk tgt)130131------------------------------------------------------------------------132133-- Lookup the given target. If neither a target nor a file with134-- the given name exists, then return the default target or throw135-- an error. If no target but a file exists, return Nothing.136targetOrFile :: MkDef -> String -> IO (Maybe Target)137targetOrFile mk name = do138 t <- lookupRule mk name139 case t of140 Just x -> pure $ Just x141 Nothing -> do142 exists <- doesPathExist name143 if exists144 then pure Nothing145 else either (throwIO . TargetErr) (pure . Just) $ defaultTarget name mk146147-- Return all prerequisites which represent targets. If a prerequisite148-- is neither a target nor an existing file, then an error is thrown.149getTargetPreqs :: MkDef -> Target -> IO [Target]150getTargetPreqs mk target =151 catMaybes <$> mapM (targetOrFile mk) (getPreqs $ getDef target)152153-- Return the names of all prerequisites that a newer than the given target.154newerPreqs :: Target -> IO [FilePath]155newerPreqs target = do156 targetTime <- getModificationTime (getName target)157 filterM (fmap (targetTime <) . getModificationTime) (getPreqs $ getDef target)158159-- Check whether the given target is up-to-date, a target shall be160-- considered up-to-date if it exists and is newer than all of its161-- dependencies.162isUp2Date :: Target -> IO Bool163isUp2Date target = do164 exists <- doesPathExist (getName target)165 if not exists166 then pure False167 else null <$> newerPreqs target168169-- Build a target if it isn't up-to-date.170--171-- Returns 'False' for execution errors when executing with `-k`.172-- Otherwise, an exception is raised on execution errors.173--174-- TODO: Consider using Either error handling instead of exceptions.175maybeBuild :: ExecConfig -> MkDef -> Target -> IO Bool176maybeBuild conf@ExecConfig {contExec = cont} mk target = do177 -- Recursively ensure that all prerequisites are up-to-date.178 res <- getTargetPreqs mk target >>= mapM (maybeBuild conf mk)179180 catch181 ( do182 up2Date <- isUp2Date target183 when (not up2Date || isPhony conf target) $184 runTarget conf mk target185186 pure (not $ any (== False) res)187 )188 ( \case189 exception@(ExecErr _) ->190 if cont191 then do192 hPutStrLn stderr $ "mach: Failed to build '" ++ getName target ++ "'"193 pure False194 else throwIO exception195 exception -> throwIO exception196 )