buildbox (empty) → 1.0.0.0
raw patch · 23 files changed
+1934/−0 lines, 23 filesdep +basedep +containersdep +directorysetup-changed
Dependencies added: base, containers, directory, mtl, old-locale, pretty, process, time, unix
Files
- BuildBox.hs +25/−0
- BuildBox/Benchmark.hs +135/−0
- BuildBox/Benchmark/Base.hs +78/−0
- BuildBox/Benchmark/Compare.hs +50/−0
- BuildBox/Benchmark/Pretty.hs +52/−0
- BuildBox/Benchmark/TimeAspect.hs +86/−0
- BuildBox/Build.hs +26/−0
- BuildBox/Build/Base.hs +173/−0
- BuildBox/Build/Testable.hs +58/−0
- BuildBox/Command/Environment.hs +160/−0
- BuildBox/Command/File.hs +114/−0
- BuildBox/Command/Mail.hs +109/−0
- BuildBox/Command/Network.hs +46/−0
- BuildBox/Command/Sleep.hs +20/−0
- BuildBox/Command/System.hs +207/−0
- BuildBox/Command/System/Internals.hs +62/−0
- BuildBox/Cron.hs +42/−0
- BuildBox/Cron/Schedule.hs +211/−0
- BuildBox/Pretty.hs +108/−0
- BuildBox/Time.hs +82/−0
- LICENSE +24/−0
- Setup.hs +2/−0
- buildbox.cabal +64/−0
+ BuildBox.hs view
@@ -0,0 +1,25 @@++module BuildBox+ ( module BuildBox.Build+ , module BuildBox.Benchmark+ , module BuildBox.Pretty+ , module BuildBox.Command.Sleep+ , module BuildBox.Command.System+ , module BuildBox.Command.Network+ , module BuildBox.Command.Mail+ , module BuildBox.Command.Environment+ , module BuildBox.Command.File+ , module BuildBox.Cron+ , module BuildBox.Time)+where+import BuildBox.Build+import BuildBox.Benchmark+import BuildBox.Pretty+import BuildBox.Command.Sleep+import BuildBox.Command.System+import BuildBox.Command.Network+import BuildBox.Command.Mail+import BuildBox.Command.Environment+import BuildBox.Command.File+import BuildBox.Cron+import BuildBox.Time
+ BuildBox/Benchmark.hs view
@@ -0,0 +1,135 @@+{-# LANGUAGE PatternGuards #-}++-- | Running benchmarks and collecting timings. +--+-- These functions expect the given `Build` commands to succeed,+-- throwing an error if they don't. If you're not sure whether your command will succeed then test it first.+module BuildBox.Benchmark+ ( module BuildBox.Benchmark.TimeAspect+ , module BuildBox.Benchmark.Pretty+ , module BuildBox.Benchmark.Compare+ + -- * Types+ , Benchmark(..)+ , Timing(..)+ , BenchRunResult(..)+ , BenchResult(..)+ + -- * Benchmarking+ , runTimedCommand+ , runBenchmarkOnce+ , outRunBenchmarkOnce+ , outRunBenchmarkAgainst+ , outRunBenchmarkWith)+where+import BuildBox.Build +import BuildBox.Pretty+import BuildBox.Benchmark.Base+import BuildBox.Benchmark.TimeAspect+import BuildBox.Benchmark.Pretty+import BuildBox.Benchmark.Compare+import Data.Time+import Data.List+import Control.Monad+++-- Running Commands -------------------------------------------------------------------------------+-- | Run a command, returning its elapsed time.+runTimedCommand + :: Build a+ -> Build (NominalDiffTime, a) + +runTimedCommand cmd+ = do start <- io $ getCurrentTime+ result <- cmd+ finish <- io $ getCurrentTime+ return (diffUTCTime finish start, result)+++-- | Run a benchmark once.+runBenchmarkOnce+ :: Benchmark + -> Build BenchRunResult+ +runBenchmarkOnce bench+ = do -- Run the setup command+ benchmarkSetup bench++ (diffTime, mKernelTimings) + <- runTimedCommand + $ benchmarkCommand bench+ + benchmarkCheck bench+ + return $ BenchRunResult+ { benchRunResultElapsed = fromRational $ toRational diffTime+ , benchRunResultKernel = mKernelTimings }+++-- | Run a benchmark once, logging activity and timings to the console.+outRunBenchmarkOnce+ :: Benchmark+ -> Build BenchRunResult+ +outRunBenchmarkOnce bench+ = do out $ "Running " ++ benchmarkName bench ++ "..."+ result <- runBenchmarkOnce bench+ outLn "ok"+ outLn $ text " elapsed = " <> (pprFloatTime $ benchRunResultElapsed result)+ + maybe (return ()) (\t -> outLn $ text " kernel elapsed = " <> pprFloatTime t) + $ takeTimeAspectOfBenchRunResult TimeAspectKernelElapsed result++ maybe (return ()) (\t -> outLn $ text " kernel cpu = " <> pprFloatTime t) + $ takeTimeAspectOfBenchRunResult TimeAspectKernelCpu result++ maybe (return ()) (\t -> outLn $ text " kernel system = " <> pprFloatTime t)+ $ takeTimeAspectOfBenchRunResult TimeAspectKernelSys result+ + outBlank+ + return result+++-- | Run a benchmark several times, logging activity to the console.+-- Optionally print a comparison with a prior results.+outRunBenchmarkAgainst+ :: Int -- ^ Number of iterations.+ -> Maybe BenchResult -- ^ Optional previous result for comparison.+ -> Benchmark -- ^ Benchmark to run.+ -> Build BenchResult+ +outRunBenchmarkAgainst iterations mPrior bench + = do out $ "Running " ++ benchmarkName bench ++ " " ++ show iterations ++ " times..."+ runResults <- replicateM iterations (runBenchmarkOnce bench) + outLn "ok"++ let result = BenchResult+ { benchResultName = benchmarkName bench+ , benchResultRuns = runResults }++ outLn pprBenchResultAspectHeader+ + maybe (return ()) outLn $ pprBenchResultAspect TimeAspectElapsed mPrior result+ maybe (return ()) outLn $ pprBenchResultAspect TimeAspectKernelElapsed mPrior result+ maybe (return ()) outLn $ pprBenchResultAspect TimeAspectKernelCpu mPrior result+ maybe (return ()) outLn $ pprBenchResultAspect TimeAspectKernelSys mPrior result+ + outBlank+ return result+++-- | Run a benchmark serveral times, logging activity to the console.+-- Also lookup prior results for comparison from the given list.+-- If there is no matching entry then run the benchmark anyway, but don't print the comparison.+outRunBenchmarkWith+ :: Int -- ^ Number of times to run each benchmark to get averages.+ -> [BenchResult] -- ^ List of prior results.+ -> Benchmark -- ^ The benchmark to run.+ -> Build BenchResult++outRunBenchmarkWith iterations priors bench+ = let mPrior = find (\b -> benchResultName b == benchmarkName bench) priors+ in outRunBenchmarkAgainst iterations mPrior bench+ +
+ BuildBox/Benchmark/Base.hs view
@@ -0,0 +1,78 @@+{-# OPTIONS_HADDOCK hide #-}++module BuildBox.Benchmark.Base+ ( Benchmark(..)+ , Timing(..)+ , BenchRunResult(..)+ , BenchResult(..))+where+import BuildBox.Build+import BuildBox.Pretty+import Control.Monad++-- | Describes a benchmark that we can run.+data Benchmark+ = Benchmark+ { -- | A unique name for the benchmark.+ benchmarkName :: String++ -- | Setup command to run before the main benchmark.+ , benchmarkSetup :: Build ()++ -- | The benchmark command to run. Only the time taken to run this part is measured.+ , benchmarkCommand :: Build (Maybe Timing)++ -- | Check \/ cleanup command to run after the main benchmark.+ , benchmarkCheck :: Build ()+ }+++-- | Holds elapsed, cpu, and system timings (in seconds).+data Timing+ = Timing+ { timingElapsed :: Maybe Float+ , timingCpu :: Maybe Float+ , timingSys :: Maybe Float }+ deriving (Eq, Read, Show)+++-- | The result of running a benchmark once.+data BenchRunResult+ = BenchRunResult++ { -- | The wall-clock time taken to run the benchmark (in seconds)+ benchRunResultElapsed :: Float++ -- | Time that the benchmark itself reported was taken to run its kernel.+ , benchRunResultKernel :: Maybe Timing }++ deriving (Show, Read)+++instance Pretty BenchRunResult where+ ppr result+ = hang (ppr "BenchRunResult") 2 $ vcat+ [ ppr "elapsed: " <> (ppr $ benchRunResultElapsed result) + , maybe empty (\r -> ppr "k.elapsed: " <> ppr r) + $ join $ liftM timingElapsed $ benchRunResultKernel result++ , maybe empty (\r -> ppr "k.cpu: " <> ppr r)+ $ join $ liftM timingCpu $ benchRunResultKernel result++ , maybe empty (\r -> ppr "k.system: " <> ppr r)+ $ join $ liftM timingSys $ benchRunResultKernel result ]+ + +-- | The result of running a benchmark several times.+-- We include the name of the original benchmark to it's easy to lookup the results.+data BenchResult+ = BenchResult+ { benchResultName :: String+ , benchResultRuns :: [BenchRunResult] }+ deriving (Show, Read)++instance Pretty BenchResult where+ ppr result+ = hang (ppr "BenchResult") 2 $ vcat+ [ ppr $ benchResultName result+ , vcat $ map ppr $ benchResultRuns result ]
+ BuildBox/Benchmark/Compare.hs view
@@ -0,0 +1,50 @@++-- | Pretty printing comparisons of benchmark results.+module BuildBox.Benchmark.Compare+ ( pprComparison+ , pprComparisons)+where+import BuildBox.Pretty+import BuildBox.Benchmark.Base+import BuildBox.Benchmark.Pretty+import BuildBox.Benchmark.TimeAspect+import Data.Maybe+import Data.List++-- | Pretty print a comparison of all the aspects of these two benchmark results.+-- The first result is the ``baseline'', while the second is the ``current'' one.+-- The numbers from the current results are printed, with a percentage relative to the baseline.+pprComparison :: BenchResult -> BenchResult -> Doc+pprComparison baseline current+ = vcat+ [ pprBenchResultAspectHeader+ , fromMaybe empty $ pprBenchResultAspect TimeAspectElapsed (Just baseline) current+ , fromMaybe empty $ pprBenchResultAspect TimeAspectKernelElapsed (Just baseline) current+ , fromMaybe empty $ pprBenchResultAspect TimeAspectKernelCpu (Just baseline) current+ , fromMaybe empty $ pprBenchResultAspect TimeAspectKernelSys (Just baseline) current ]+++-- | Pretty print a comparison of all the aspects of these two benchmark results.+-- The first result is the ``baseline'' while the second is the ``current'' one.+-- All the numbers from the current results are printed, with a percentage relative to the+-- baseline if there is one. If there is no baseline for a particular result then we still+-- print the current one.+pprComparisons :: [BenchResult] -> [BenchResult] -> Doc+pprComparisons baselines currents+ = let comparison current+ = let mBaseline = find (\b -> benchResultName b == benchResultName current)+ $ baselines++ in vcat+ [ fromMaybe empty $ pprBenchResultAspect TimeAspectElapsed mBaseline current+ , fromMaybe empty $ pprBenchResultAspect TimeAspectKernelElapsed mBaseline current+ , fromMaybe empty $ pprBenchResultAspect TimeAspectKernelCpu mBaseline current+ , fromMaybe empty $ pprBenchResultAspect TimeAspectKernelSys mBaseline current ]++ in vcat+ [ nest 15 pprBenchResultAspectHeader+ , vcat $ punctuate (text "\n")+ $ map (\c -> (text (benchResultName c) $$ (nest 15 $ comparison c)))+ $ currents ]++
+ BuildBox/Benchmark/Pretty.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE PatternGuards #-}++-- | Pretty printing benchmark results.+module BuildBox.Benchmark.Pretty+ ( pprBenchResultAspectHeader+ , pprBenchResultAspect)+where+import BuildBox.Benchmark.Base+import BuildBox.Benchmark.TimeAspect+import BuildBox.Pretty++-- | Header to use when pretty printing benchmark results.+pprBenchResultAspectHeader :: Doc+pprBenchResultAspectHeader + = vcat+ [ text " aspect min ref% avg ref% max ref% spread% "+ , text " --------- ---------- ---------- ---------- ------- " ]+++-- | Pretty print an aspect of a benchmark result.+-- If the given aspect does not exist in the result then `Nothing`.+pprBenchResultAspect + :: TimeAspect -- ^ Aspect of the result to print.+ -> Maybe BenchResult -- ^ Optional prior result for comparison.+ -> BenchResult -- ^ The result to print.+ -> Maybe Doc++pprBenchResultAspect aspect prior result+ | Just (tmin, tavg, tmax) <- takeMinAvgMaxOfBenchResult aspect result+ , spread <- tmax - tmin+ , spreadPercent <- (floor $ (spread / tavg) * 100) :: Integer+ , Just result' <- prior+ , Just (tmin', tavg', tmax') <- takeMinAvgMaxOfBenchResult aspect result'+ = Just $ text " "+ <> padL 10 (ppr aspect)+ <+> (padR 12 $ pprFloatRef tmin tmin')+ <+> (padR 12 $ pprFloatRef tavg tavg')+ <+> (padR 12 $ pprFloatRef tmax tmax')+ <+> (padR 8 $ ppr spreadPercent)++ | Just (tmin, tavg, tmax) <- takeMinAvgMaxOfBenchResult aspect result+ , spread <- tmax - tmin+ , spreadPercent <- (floor $ (spread / tavg) * 100) :: Integer+ = Just $ text " "+ <> padL 10 (ppr aspect)+ <+> (padR 12 $ (pprFloatTime tmin <> text " "))+ <+> (padR 12 $ (pprFloatTime tavg <> text " "))+ <+> (padR 12 $ (pprFloatTime tmax <> text " "))+ <+> (padR 8 $ ppr spreadPercent)+ + | otherwise+ = Nothing
+ BuildBox/Benchmark/TimeAspect.hs view
@@ -0,0 +1,86 @@+{-# LANGUAGE PatternGuards #-}++-- | Dealing with aspects of timing results.+module BuildBox.Benchmark.TimeAspect+ ( TimeAspect(..)+ , takeTimeAspectOfBenchRunResult+ , takeAvgTimeOfBenchResult+ , takeMinTimeOfBenchResult+ , takeMaxTimeOfBenchResult+ , takeMinAvgMaxOfBenchResult)+where+import BuildBox.Benchmark.Base+import BuildBox.Pretty+import Control.Monad+++-- | Aspects of a benchmark runtime we can talk about.+data TimeAspect+ = TimeAspectElapsed+ | TimeAspectKernelElapsed+ | TimeAspectKernelCpu+ | TimeAspectKernelSys+ deriving (Show, Read, Enum)+++-- | Get the pretty name of a TimeAspect.+instance Pretty TimeAspect where+ ppr aspect+ = case aspect of+ TimeAspectElapsed -> ppr "elapsed"+ TimeAspectKernelElapsed -> ppr "k.elapsed"+ TimeAspectKernelCpu -> ppr "k.cpu"+ TimeAspectKernelSys -> ppr "k.system"+++-- | Get a particular aspect of a benchmark result.+takeTimeAspectOfBenchRunResult :: TimeAspect -> BenchRunResult -> Maybe Float+takeTimeAspectOfBenchRunResult aspect result+ = case aspect of+ TimeAspectElapsed -> Just $ benchRunResultElapsed result+ TimeAspectKernelElapsed -> join $ liftM timingElapsed $ benchRunResultKernel result+ TimeAspectKernelCpu -> join $ liftM timingCpu $ benchRunResultKernel result+ TimeAspectKernelSys -> join $ liftM timingSys $ benchRunResultKernel result+++-- | Get the average runtime from a benchmark result.+takeAvgTimeOfBenchResult :: TimeAspect -> BenchResult -> Maybe Float+takeAvgTimeOfBenchResult aspect result+ = let mTimes = sequence + $ map (takeTimeAspectOfBenchRunResult aspect)+ $ benchResultRuns result+ + in liftM (\ts -> sum ts / (fromIntegral $ length ts)) mTimes+ ++-- | Get the minimum runtime from a benchmark result.+takeMinTimeOfBenchResult :: TimeAspect -> BenchResult -> Maybe Float+takeMinTimeOfBenchResult aspect result+ = let mTimes = sequence+ $ map (takeTimeAspectOfBenchRunResult aspect)+ $ benchResultRuns result++ in liftM (\ts -> minimum ts) mTimes+++-- | Get the maximum runtime from a benchmark result.+takeMaxTimeOfBenchResult :: TimeAspect -> BenchResult -> Maybe Float+takeMaxTimeOfBenchResult aspect result+ = let mTimes = sequence+ $ map (takeTimeAspectOfBenchRunResult aspect)+ $ benchResultRuns result++ in liftM (\ts -> maximum ts) mTimes+++-- | Get the min, avg, and max runtimes from this benchmark result.+takeMinAvgMaxOfBenchResult :: TimeAspect -> BenchResult -> Maybe (Float, Float, Float)+takeMinAvgMaxOfBenchResult aspect result+ | Just tmin <- takeMinTimeOfBenchResult aspect result+ , Just tavg <- takeAvgTimeOfBenchResult aspect result+ , Just tmax <- takeMaxTimeOfBenchResult aspect result+ = Just (tmin, tavg, tmax)+ + | otherwise+ = Nothing+
+ BuildBox/Build.hs view
@@ -0,0 +1,26 @@++-- | Defines the main `Build` monad and common utils.+module BuildBox.Build + ( module BuildBox.Build.Testable+ , Build+ , BuildError(..)+ , BuildConfig(..)+ , buildConfigDefault+ , runBuild+ , runBuildPrint+ , runBuildPrintWithConfig+ , throw+ , io+ , logSystem+ , out+ , outLn+ , outBlank+ , outLine+ , outLINE+ , whenM)+where+import BuildBox.Build.Base+import BuildBox.Build.Testable++ +
+ BuildBox/Build/Base.hs view
@@ -0,0 +1,173 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE ExistentialQuantification #-}++module BuildBox.Build.Base where+import BuildBox.Pretty+import Control.Monad.Error+import Control.Monad.Reader+import System.IO+import System.IO.Error+import System.Exit++-- | The builder monad encapsulates and IO action that can fail with an error, +-- and also read some global configuration info.+type Build a = ErrorT BuildError (ReaderT BuildConfig IO) a++-- BuildError -------------------------------------------------------------------------------------+-- | The errors we recognise.+data BuildError+ -- | Some generic error+ = ErrorOther String++ -- | Some system command fell over, and it barfed out the given stdout and stderr.+ | ErrorSystemCmdFailed+ { buildErrorCmd :: String+ , buildErrorCode :: ExitCode+ , buildErrorStdout :: String+ , buildErrorStderr :: String }+ + -- | Some other IO action failed.+ | ErrorIOError IOError++ -- | Some property `check` was supposed to return the given boolean value, but it didn't.+ | forall prop. Show prop => ErrorCheckFailed Bool prop ++instance Error BuildError where+ strMsg s = ErrorOther s++instance Pretty BuildError where+ ppr err+ = case err of+ ErrorOther str+ -> text "Other error: " <> text str++ ErrorSystemCmdFailed{}+ -> vcat + [ text "System command failure."+ , text " command: " <> (text $ buildErrorCmd err)+ , text " exit code: " <> (text $ show $ buildErrorCode err)+ , blank+ , if (not $ null $ buildErrorStdout err)+ then vcat [ text "-- stdout (last 10 lines) ------------------------------------------------------"+ , vcat $ map text $ reverse $ take 10 $ reverse $ lines $ buildErrorStdout err]+ else text ""+ , blank+ , if (not $ null $ buildErrorStderr err)+ then vcat [ text "-- stderr (last 10 lines) ------------------------------------------------------"+ , vcat $ map text $ reverse $ take 10 $ reverse $ lines $ buildErrorStderr err]+ else text ""+ + , text "--------------------------------------------------------------------------------" ]+ + ErrorIOError ioerr+ -> text "IO error: " <> (text $ show ioerr)++ ErrorCheckFailed expected prop+ -> text "Check failure: " <> (text $ show prop) <> (text " expected ") <> (text $ show expected)+++-- BuildConfig ------------------------------------------------------------------------------------+-- | Global builder configuration.+data BuildConfig+ = BuildConfig+ { -- | Log all system commands executed to this file handle.+ buildConfigLogSystem :: Maybe Handle }++-- | The default build config.+buildConfigDefault :: BuildConfig+buildConfigDefault+ = BuildConfig+ { buildConfigLogSystem = Nothing }++-- | Log a system command to the handle in our `BuildConfig`, if any.+logSystem :: String -> Build ()+logSystem cmd+ = do mHandle <- asks buildConfigLogSystem+ case mHandle of+ Nothing -> return ()+ Just handle + -> do io $ hPutStr handle "buildbox system: "+ io $ hPutStrLn handle cmd+ return ()++-- Build ------------------------------------------------------------------------------------------+-- | Throw an error in the build monad.+throw :: BuildError -> Build a+throw = throwError++-- | Run a build command.+runBuild :: Build a -> IO (Either BuildError a)+runBuild build+ = runReaderT (runErrorT build) buildConfigDefault+++-- | Run a build command, reporting whether it succeeded to the console.+-- If it succeeded then return Just the result, else Nothing.+runBuildPrint :: Build a -> IO (Maybe a)+runBuildPrint + = runBuildPrintWithConfig buildConfigDefault+++-- | Like `runBuildPrintWithConfig` but also takes a `BuildConfig`.+runBuildPrintWithConfig :: BuildConfig -> Build a -> IO (Maybe a)+runBuildPrintWithConfig config build+ = do result <- runReaderT (runErrorT build) config+ case result of+ Left err+ -> do putStrLn "\nBuild failed"+ putStr " due to "+ putStrLn $ render $ ppr err+ return $ Nothing+ + Right x+ -> do putStrLn "Build succeeded."+ return $ Just x+++-- Utils ------------------------------------------------------------------------------------------+-- | Lift an IO action into the build monad.+-- If the action throws any exceptions they get caught and turned into+-- `ErrorIOError` exceptions in our `Build` monad.+io :: IO a -> Build a+io x+ = do -- catch IOError exceptions+ result <- liftIO $ try x+ + case result of+ Left err -> throw $ ErrorIOError err+ Right res -> return res+++-- | Print some text to stdout.+out :: Pretty a => a -> Build ()+out str + = io + $ do putStr $ render $ ppr str+ hFlush stdout++-- | Print some text to stdout followed by a newline.+outLn :: Pretty a => a -> Build ()+outLn str = io $ putStrLn $ render $ ppr str+++-- | Print a blank line to stdout+outBlank :: Build ()+outBlank = out $ text "\n"+++-- | Print a @-----@ line to stdout +outLine :: Build ()+outLine = io $ putStr (replicate 80 '-' ++ "\n")+++-- | Print a @=====@ line to stdout+outLINE :: Build ()+outLINE = io $ putStr (replicate 80 '=' ++ "\n")+++-- | Like `when`, but with teh monadz.+whenM :: Monad m => m Bool -> m () -> m ()+whenM cb cx+ = do b <- cb+ if b then cx else return ()+
+ BuildBox/Build/Testable.hs view
@@ -0,0 +1,58 @@++-- | Some property of the system we can test for.+--+-- They have `Show` instances so we can make nice error messages if a `check` fails.+module BuildBox.Build.Testable+ ( Testable(..)+ , check+ , checkFalse+ , outCheckOk+ , outCheckFalseOk)+where+import BuildBox.Build.Base +import Control.Monad.Error+++-- | Some testable property.+class Testable prop where+ test :: prop -> Build Bool++-- | Testable properties are checkable.+-- If the check returns false then throw an error.+check :: (Show prop, Testable prop) => prop -> Build ()+check prop+ = do result <- test prop+ if result+ then return ()+ else throwError $ ErrorCheckFailed True prop+++-- | Testable properties are checkable.+-- If the check returns true then throw an error.+checkFalse :: (Show prop, Testable prop) => prop -> Build ()+checkFalse prop+ = do result <- test prop+ if result+ then throwError $ ErrorCheckFailed False prop+ else return ()+ ++-- | Check some property while printing what we're doing.+outCheckOk + :: (Show prop, Testable prop) + => String -> prop -> Build ()++outCheckOk str prop+ = do outLn str+ check prop+++-- | Check some property while printing what we're doing.+outCheckFalseOk + :: (Show prop, Testable prop) + => String -> prop -> Build ()++outCheckFalseOk str prop+ = do outLn str+ checkFalse prop+
+ BuildBox/Command/Environment.hs view
@@ -0,0 +1,160 @@++-- | Gathering information about the build environment.+module BuildBox.Command.Environment+ ( -- * Build Environment+ Environment(..)+ , getEnvironmentWith+ + -- * Build platform+ , Platform(..)+ , getHostPlatform+ , getHostName+ , getHostArch+ , getHostProcessor+ , getHostOS+ , getHostRelease+ + -- * Software versions+ , getVersionGHC+ , getVersionGCC)+where+import BuildBox.Build+import BuildBox.Command.System+import BuildBox.Command.File+import BuildBox.Pretty+++-- Environment ------------------------------------------------------------------------------------+-- | The environment consists of the `Platform`, and some tool versions.+data Environment + = Environment+ { environmentPlatform :: Platform+ , environmentVersions :: [(String, String)] }+ deriving (Show, Read)+++instance Pretty Environment where+ ppr env+ = hang (ppr "Environment") 2 $ vcat+ [ ppr $ environmentPlatform env+ , hang (ppr "Versions") 2 + $ vcat + $ map (\(name, ver) -> ppr name <+> ppr ver) + $ environmentVersions env ]++++-- | Get the current environment, including versions of these tools.+getEnvironmentWith + :: [(String, Build String)] -- ^ List of tool names and commands to get their versions.+ -> Build Environment+ +getEnvironmentWith nameGets + = do platform <- getHostPlatform++ versions <- mapM (\(name, get) -> do+ ver <- get+ return (name, ver))+ $ nameGets+ + return $ Environment+ { environmentPlatform = platform + , environmentVersions = versions }+ +++-- Platform ---------------------------------------------------------------------------------------+-- | Generic information about the platform we're running on.+data Platform+ = Platform+ { platformHostName :: String+ , platformHostArch :: String+ , platformHostProcessor :: String+ , platformHostOS :: String+ , platformHostRelease :: String }+ deriving (Show, Read)+ + +instance Pretty Platform where+ ppr plat+ = hang (ppr "Platform") 2 $ vcat+ [ ppr "host: " <> (ppr $ platformHostName plat)+ , ppr "arch: " <> (ppr $ platformHostArch plat)+ , ppr "processor: " <> (ppr $ platformHostProcessor plat)+ , ppr "system: " <> (ppr $ platformHostOS plat) <+> (ppr $ platformHostRelease plat) ]+++-- | Get information about the host platform.+getHostPlatform :: Build Platform+getHostPlatform + = do name <- getHostName+ arch <- getHostArch+ processor <- getHostProcessor+ os <- getHostOS+ release <- getHostRelease+ + return $ Platform+ { platformHostName = name+ , platformHostArch = arch+ , platformHostProcessor = processor+ , platformHostOS = os+ , platformHostRelease = release }+ ++-- Platform Tests ---------------------------------------------------------------------------------+-- | Get the name of this host, using @uname@.+getHostName :: Build String+getHostName+ = do check $ HasExecutable "uname"+ name <- qssystemOut "uname -n"+ return $ init name+++-- | Get the host architecture, using @uname@.+getHostArch :: Build String+getHostArch+ = do check $ HasExecutable "arch"+ name <- qssystemOut "arch"+ return $ init name+++-- | Get the host processor name, using @uname@.+getHostProcessor :: Build String+getHostProcessor+ = do check $ HasExecutable "uname"+ name <- qssystemOut "uname -p"+ return $ init name+++-- | Get the host operating system, using @uname@.+getHostOS :: Build String+getHostOS+ = do check $ HasExecutable "uname"+ os <- qssystemOut "uname -s"+ return $ init os+++-- | Get the host operating system release, using @uname@.+getHostRelease :: Build String+getHostRelease+ = do check $ HasExecutable "uname"+ str <- qssystemOut "uname -r"+ return $ init str+++-- Software version tests -------------------------------------------------------------------------+-- | Get the version of this GHC, or throw an error if it can't be found.+getVersionGHC :: FilePath -> Build String+getVersionGHC path+ = do check $ HasExecutable path+ str <- qssystemOut $ path ++ " --version"+ return $ init str+ +-- | Get the version of this GCC, or throw an error if it can't be found. +getVersionGCC :: FilePath -> Build String+getVersionGCC path+ = do check $ HasExecutable path+ str <- qssystemOut $ path ++ " --version"+ return $ head $ lines str++
+ BuildBox/Command/File.hs view
@@ -0,0 +1,114 @@++-- | Working with the file system.+module BuildBox.Command.File+ ( PropFile(..)+ , inDir+ , inScratchDir+ , clobberDir+ , ensureDir+ , withTempFile)+where+import BuildBox.Build.Base+import BuildBox.Build.Testable+import BuildBox.Command.System+import System.Posix.Temp+import System.Directory+import System.IO++-- | Properties of the file system we can test for.+data PropFile++ -- | Some executable is in the current path.+ = HasExecutable String++ -- | Some file exists.+ | HasFile FilePath++ -- | Some directory exists.+ | HasDir FilePath++ -- | Some file is empty.+ | FileEmpty FilePath+ deriving Show+++instance Testable PropFile where+ test prop + = case prop of+ HasExecutable name+ -> do code <- qsystem $ "which " ++ name+ return $ code == ExitSuccess++ HasFile path + -> io $ doesFileExist path++ HasDir path+ -> io $ doesDirectoryExist path++ FileEmpty path + -> do contents <- io $ readFile path+ return (null contents)+++-- | Run a command in a different working directory. Throws an error if the directory doesn't exist.+inDir :: FilePath -> Build a -> Build a+inDir name build+ = do check $ HasDir name+ oldDir <- io $ getCurrentDirectory++ io $ setCurrentDirectory name+ x <- build+ io $ setCurrentDirectory oldDir++ return x++-- | Create a new directory with the given name, run a command within it,+-- then change out and recursively delete the directory. Throws an error if a directory+-- with the given name already exists.+inScratchDir :: FilePath -> Build a -> Build a+inScratchDir name build+ = do + -- Make sure a dir with this name doesn't already exist.+ checkFalse $ HasDir name++ ssystem $ "mkdir -p " ++ name+ x <- inDir name build+ + ssystem $ "rm -Rf " ++ name + return x+++-- | Delete a dir recursively if it's there, otherwise do nothing.+-- Unlike `removeDirectoryRecursive`, this function does+-- not follow symlinks, it just deletes them.+clobberDir :: FilePath -> Build ()+clobberDir path+ = ssystem $ "rm -Rf " ++ path+++-- | Create a new directory if it isn't already there, or return successfully if it is.+ensureDir :: FilePath -> Build ()+ensureDir path+ = do already <- io $ doesDirectoryExist path+ if already+ then return ()+ else ssystem $ "mkdir -p " ++ path+++-- | Create a temp file, pass it to some command, then delete the file after the command finishes.+withTempFile :: (FilePath -> Build a) -> Build a+withTempFile build+ = do (fileName, handle) <- io $ mkstemp "/tmp/buildbox-XXXXXX"++ -- We just want the file name here, so close the handle to let the real+ -- build command write to it however it wants.+ io $ hClose handle+ + -- run the real command+ result <- build fileName+ + -- cleanup + io $ removeFile fileName+ + return result+
+ BuildBox/Command/Mail.hs view
@@ -0,0 +1,109 @@++-- | Sending email. We've got baked in support for @msmtp@, which is easy to set up. Adding support for other mailers +-- should be easy. Get @msmtp@ here: <http://msmtp.sourceforge.net>+module BuildBox.Command.Mail+ ( Mail(..)+ , Mailer(..)+ , createMailWithCurrentTime+ , renderMail+ , sendMailWithMailer)+where+import BuildBox.Build+import BuildBox.Pretty+import BuildBox.Command.Environment+import BuildBox.Command.System+import System.Locale (defaultTimeLocale)+import Data.Time.Clock+import Data.Time.LocalTime+import Data.Time.Format+import Data.Time.Calendar+++-- | An email message that we can send.+data Mail+ = Mail+ { mailFrom :: String+ , mailTo :: String+ , mailSubject :: String+ , mailTime :: UTCTime+ , mailTimeZone :: TimeZone+ , mailMessageId :: String+ , mailBody :: String }+ deriving Show+++-- | An external mailer that can send messages.+-- Also contains mail server info if needed.+-- We only support msmtp at the moment.+data Mailer+ = MailerMSMTP+ { mailerPath :: FilePath+ , mailerPort :: Maybe Int }+ deriving Show+++-- | Create a mail with a given from, to, subject and body.+-- Fill in the date and message id based on the current time.+-- Valid dates and message ids are needed to prevent the mail+-- being bounced by spambots.+createMailWithCurrentTime + :: String -- ^ ''from'' field. Should be an email address.+ -> String -- ^ ''to'' field. Should be an email address.+ -> String -- ^ Subject line.+ -> String -- ^ Message body.+ -> Build Mail++createMailWithCurrentTime from to subject body+ = do+ -- We need to add the date otherwise our messages will get marked as spam.+ -- Use RFC 2822 format timestamp.+ utime <- io $ getCurrentTime+ zone <- io $ getCurrentTimeZone++ -- Generate a messageid based on the clock time.+ hostName <- getHostName+ let dayNum = toModifiedJulianDay $ utctDay utime+ let secTime = utctDayTime utime+ let messageId = "<" ++ show dayNum ++ "." ++ show secTime+ ++ "@" ++ hostName ++ ">"+ + return $ Mail+ { mailFrom = from+ , mailTo = to+ , mailSubject = subject+ , mailTime = utime+ , mailTimeZone = zone+ , mailMessageId = messageId+ , mailBody = body }+++-- | Render an email message as a string.+renderMail :: Mail -> Doc+renderMail mail+ = vcat+ [ ppr "From: " <> ppr (mailFrom mail)+ , ppr "To: " <> ppr (mailTo mail)+ , ppr "Subject: " <> ppr (mailSubject mail)+ , ppr "Date: " <> (ppr $ formatTime defaultTimeLocale "%a, %e %b %Y %H:%M:%S %z"+ $ utcToZonedTime (mailTimeZone mail) (mailTime mail))++ , ppr "Message-Id: " <> ppr (mailMessageId mail)+ , ppr ""+ , ppr (mailBody mail) ]+++-- | Send a mail message.+sendMailWithMailer :: Mail -> Mailer -> Build ()+sendMailWithMailer mail mailer+ = case mailer of+ MailerMSMTP{} -> sendMailWithMSMTP mail mailer++sendMailWithMSMTP :: Mail -> Mailer -> Build ()+sendMailWithMSMTP mail mailer@MailerMSMTP{}+ = ssystemTee False+ (mailerPath mailer + ++ " -t " -- read recipients from the mail+ ++ (maybe "" (\port -> " --port=" ++ show port) $ mailerPort mailer))+ (render $ renderMail mail)++
+ BuildBox/Command/Network.hs view
@@ -0,0 +1,46 @@++-- | Working with the network.+module BuildBox.Command.Network+ (PropNetwork(..))+where+import BuildBox.Build+import BuildBox.Command.File+import BuildBox.Command.System++type HostName = String+type URL = String++data PropNetwork++ -- | The given host is reachable with @ping@.+ = HostReachable HostName++ -- | Use @wget@ to check if a web-page is gettable.+ -- The page is deleted after downloading.+ | UrlGettable URL++ deriving Show+ +instance Testable PropNetwork where+ test prop+ = case prop of++ -- Works on OSX 10.6.2+ -- -o exit successfully after recieving one reply packet.+ HostReachable hostName+ -> do check $ HasExecutable "ping"+ code <- qsystem $ "ping -o " ++ hostName+ return $ code == ExitSuccess+ + -- Works on OSX 10.6.2, wget 1.12+ -- -q quiet+ -- --delete-after delete page after downloading.+ UrlGettable url+ -> do check $ HasExecutable "wget"+ code <- qsystem $ "wget -q --delete-after " ++ url+ return $ code == ExitSuccess+ +++ +
+ BuildBox/Command/Sleep.hs view
@@ -0,0 +1,20 @@++-- | What do build 'bots dream about?+module BuildBox.Command.Sleep+ ( sleep+ , msleep)+where+import BuildBox.Build+import Control.Concurrent+++-- | Sleep for a given number of seconds.+sleep :: Int -> Build ()+sleep secs + = io $ threadDelay $ secs * 1000000++ +-- | Sleep for a given number of milliseconds.+msleep :: Int -> Build ()+msleep msecs+ = io $ threadDelay $ msecs * 1000
+ BuildBox/Command/System.hs view
@@ -0,0 +1,207 @@+{-# LANGUAGE PatternGuards, BangPatterns #-}++-- | Running system commands. On some platforms this may cause the command to be executed directly, so +-- shell tricks won't work. The `Build` monad can be made to log commands executed with all versions+-- of `system` by setting `buildConfigLogSystem` in the `BuildConfig` passed to `runBuildPrintWithConfig`.+--+-- We define a lot of wrappers because executing system commands is the bread-and-butter of +-- buildbots, and we usually need all the versions...++module BuildBox.Command.System + ( module System.Exit++ -- * Wrappers+ , system+ , ssystem+ , qsystem+ , qssystem+ , ssystemOut+ , qssystemOut+ , systemTee+ , ssystemTee++ -- * The real function+ , systemTeeIO)+where+import BuildBox.Command.System.Internals+import BuildBox.Build+import Control.Concurrent+import System.Process hiding (system)+import System.Exit+import System.IO+import Control.Monad++debug :: Bool+debug = False++trace :: String -> IO ()+trace s = when debug $ putStrLn s+++-- Wrappers ---------------------------------------------------------------------------------------+-- | Run a system command, returning its exit code.+system :: String -> Build ExitCode+system cmd+ = do (code, _, _) <- systemTee True cmd ""+ return code+++-- | Run a successful system command.+-- If the exit code is `ExitFailure` then throw an error in the `Build` monad.+ssystem :: String -> Build ()+ssystem cmd+ = do (code, strOut, strErr) <- systemTee True cmd ""++ when (code /= ExitSuccess)+ $ throw $ ErrorSystemCmdFailed cmd code strOut strErr+++-- | Quietly run a system command, returning its exit code.+qsystem :: String -> Build ExitCode+qsystem cmd+ = do (code, _, _) <- systemTee False cmd ""+ return code+++-- | Quietly run a successful system command.+-- If the exit code is `ExitFailure` then throw an error in the `Build` monad.+qssystem :: String -> Build ()+qssystem cmd+ = do (code, strOut, strErr) <- systemTee False cmd ""++ when (code /= ExitSuccess)+ $ throw $ ErrorSystemCmdFailed cmd code strOut strErr+ ++-- | Run a successful system command, returning what it wrote to its @stdout@.+-- If anything was written to @stderr@ then treat that as failure. +-- If it fails due to writing to @stderr@ or returning `ExitFailure`+-- then throw an error in the `Build` monad.+ssystemOut :: String -> Build String+ssystemOut cmd+ = do (code, strOut, strErr) <- systemTee True cmd ""+ when (code /= ExitSuccess || (not $ null strErr))+ $ throw $ ErrorSystemCmdFailed cmd code strOut strErr++ return strOut++-- | Quietly run a successful system command, returning what it wrote to its @stdout@.+-- If anything was written to @stderr@ then treat that as failure. +-- If it fails due to writing to @stderr@ or returning `ExitFailure`+-- then throw an error in the `Build` monad.+qssystemOut :: String -> Build String+qssystemOut cmd+ = do (code, strOut, strErr) <- systemTee False cmd ""+ when (code /= ExitSuccess || (not $ null strErr))+ $ throw $ ErrorSystemCmdFailed cmd code strOut strErr++ return strOut++++-- Tee versions -----------------------------------------------------------------------------------++-- | Like `systemTeeIO`, but in the `Build` monad.+systemTee + :: Bool + -> String + -> String + -> Build (ExitCode, String, String)++systemTee tee cmd strIn+ = do logSystem cmd+ io $ systemTeeIO tee cmd strIn+++-- | Like `systemTeeIO`, but in the `Build` monad and throw an error if it returns `ExitFailure`.+ssystemTee :: Bool -> String -> String -> Build ()+ssystemTee tee cmd strIn+ = do logSystem cmd+ (code, strOut, strErr) <- systemTee tee cmd strIn+ when (code /= ExitSuccess)+ $ throw $ ErrorSystemCmdFailed cmd code strOut strErr++ ++-- | Run a system command, returning its `ExitCode` and what was written to @stdout@ and @stderr@.+systemTeeIO + :: Bool -- ^ Whether @stdout@ and @stderr@ should be forwarded to the parent process.+ -> String -- ^ Command to run.+ -> String -- ^ What to pass to the command's @stdin@.+ -> IO (ExitCode, String, String)++systemTeeIO tee cmd strIn+ = do trace $ "systemTeeIO " ++ show tee ++ ": " ++ cmd++ -- Create some new pipes for the process to write its stdout and stderr to.+ trace $ "systemTeeIO: Creating process"+ (Just hInWrite, Just hOutRead, Just hErrRead, phProc)+ <- createProcess + $ CreateProcess+ { cmdspec = ShellCommand cmd+ , cwd = Nothing+ , env = Nothing+ , std_in = CreatePipe+ , std_out = CreatePipe+ , std_err = CreatePipe+ , close_fds = False }++ -- Push input into in handle+ hPutStr hInWrite strIn+ + -- To implement the tee-like behavior we'll fork some threads that read lines from the+ -- processes stdout and stderr and write them to these channels. + -- When they hit EOF they signal this via the semaphores.+ chanOut <- newChan+ chanErr <- newChan+ semOut <- newQSem 0+ semErr <- newQSem 0++ -- Make duplicates of the above, which will store everything+ -- written to them. This gives us the copy to return from the fn.+ chanOutAcc <- dupChan chanOut+ chanErrAcc <- dupChan chanErr++ -- Fork threads to read from the process handles and write to our channels.+ _tidOut <- forkIO $ streamIn hOutRead chanOut+ _tidErr <- forkIO $ streamIn hErrRead chanErr++ -- If tee-like behavior is turned on, we forward what the process writes to+ -- its stdout and stderr to the parent.+ _tidStream <- forkIO $ streamOuts+ [ (chanOut, if tee then Just stdout else Nothing, semOut)+ , (chanErr, if tee then Just stderr else Nothing, semErr) ]++ -- Wait for the main process to complete.+ code <- waitForProcess phProc+ trace $ "systemTeeIO: Process done, code = " ++ show code++ trace $ "systemTeeIO: Waiting for sems"+ -- Wait for the tee processes to finish.+ -- We need to do this to avoid corrupted output on the console due to our forwarding+ -- threads writing at the same time as successing Build commands.+ mapM_ waitQSem [semOut, semErr]++ trace $ "systemTeeIO: Getting output"+ -- Get what was written to its stdout and stderr.+ -- getChanContents is a lazy read, so don't pull from the channel after+ -- seeing a Nothing else we'll block forever.+ strOut <- liftM (concat . slurpUntilNothing) $ getChanContents chanOutAcc+ strErr <- liftM (concat . slurpUntilNothing) $ getChanContents chanErrAcc++ trace $ "systemTeeIO stdout: " ++ strOut+ trace $ "systemTeeIO stderr: " ++ strErr++ trace $ "systemTeeIO: All done"+ code `seq` strOut `seq` strErr `seq` + return (code, strOut, strErr)++++slurpUntilNothing :: [Maybe a] -> [a]+slurpUntilNothing xx+ = case xx of+ [] -> []+ Nothing : _ -> []+ Just x : xs -> x : slurpUntilNothing xs+
+ BuildBox/Command/System/Internals.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE PatternGuards #-}+module BuildBox.Command.System.Internals+ ( streamIn+ , streamOuts)+where+import System.IO+import Control.Concurrent ++-- | Continually read lines from a handle and write them to this channel.+-- When the handle hits EOF then write `Nothing` to the channel.+streamIn :: Handle -> Chan (Maybe String) -> IO ()+streamIn hRead chan+ = do eof <- hIsEOF hRead+ if eof+ then do+ writeChan chan Nothing+ return ()+ + else do+ str <- hGetLine hRead+ writeChan chan (Just (str ++ "\n"))+ streamIn hRead chan+++-- | Continually read lines from some channels and write them to handles.+-- When all the channels return `Nothing` then we're done.+-- When we're done, signal this fact on the semaphore.+streamOuts :: [(Chan (Maybe String), (Maybe Handle), QSem)] -> IO ()+streamOuts chans + = streamOuts' False [] chans++ where -- we're done.+ streamOuts' _ [] [] + = return ()++ -- play it again, sam.+ streamOuts' True prev [] + = streamOuts' False [] prev++ streamOuts' False prev []+ = do yield+ streamOuts' False [] prev++ -- try to read from the current chan.+ streamOuts' active prev (x@(chan, mHandle, qsem) : rest)+ = isEmptyChan chan >>= \empty -> + if empty + then streamOuts' active (prev ++ [x]) rest+ else do+ mStr <- readChan chan+ case mStr of+ Nothing + -> do signalQSem qsem+ streamOuts' active prev rest++ Just str + | Just h <- mHandle+ -> do hPutStr h str+ streamOuts' True (prev ++ [x]) rest++ | otherwise+ -> streamOuts' True (prev ++ [x]) rest
+ BuildBox/Cron.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE ScopedTypeVariables, PatternGuards #-}+{-# OPTIONS -fno-warn-orphans #-}++-- | A simple ''cron'' loop. Used for running commands according to a given schedule.+module BuildBox.Cron+ ( module BuildBox.Cron.Schedule+ , cronLoop )+where+import BuildBox.Build+import BuildBox.Cron.Schedule+import BuildBox.Command.Sleep+import Data.Time++-- | Given a schedule of commands, run them when their time is due.+-- Only one command is run at a time. If several commands could be started at a specific+-- moment, then we take the one with the earliest potential start time. If any command throws+-- an error in the `Build` monad then the whole loop does.+--+cronLoop :: Schedule (Build ())-> Build ()+cronLoop schedule+ = do startTime <- io $ getCurrentTime++ case earliestEventToStartAt startTime $ eventsOfSchedule schedule of+ Nothing + -> do sleep 1+ cronLoop schedule+ + Just event + -> do let Just build = lookupCommandOfSchedule (eventName event) schedule+ build+ endTime <- io $ getCurrentTime++ let event' = event+ { eventLastStarted = Just startTime+ , eventLastEnded = Just endTime }++ let schedule' = adjustEventOfSchedule event' schedule+ cronLoop schedule'+ + + +
+ BuildBox/Cron/Schedule.hs view
@@ -0,0 +1,211 @@+{-# LANGUAGE ScopedTypeVariables, PatternGuards #-}+{-# OPTIONS -fno-warn-orphans #-}++-- | A schedule of commands that should be run at a certain time.+module BuildBox.Cron.Schedule+ ( + -- * Time Periods+ second, minute, hour, day++ -- * When+ , When (..)+ , WhenModifier (..)++ -- * Events+ , EventName+ , Event (..)+ , earliestEventToStartAt+ , eventCouldStartAt++ -- * Schedules+ , Schedule (..)+ , makeSchedule+ , lookupEventOfSchedule+ , lookupCommandOfSchedule+ , adjustEventOfSchedule+ , eventsOfSchedule)+where+import Data.Time+import Data.List+import Data.Function+import Data.Maybe+import Control.Monad+import Data.Map (Map)+import qualified Data.Map as Map+++instance Read NominalDiffTime where+ readsPrec n str+ = let [(secs :: Double, rest)] = readsPrec n str+ in case rest of+ 's' : rest' -> [(fromRational $ toRational secs, rest')]+ _ -> []++second, minute, hour, day :: NominalDiffTime+second = 1+minute = 60+hour = 60 * minute+day = 24 * hour+++-- When -------------------------------------------------------------------------------------------+-- | When to invoke some event.+data When+ -- | Just keep doing it.+ = Always++ -- | Don't do it, ever.+ | Never++ -- | Do it some time after we last started it.+ | Every NominalDiffTime+ + -- | Do it some time after it last finished.+ | After NominalDiffTime+ + -- | Do it each day at this time. The ''days'' are UTC days, not local ones.+ | Daily TimeOfDay+ deriving (Read, Show, Eq)+++-- | Modifier to when.+data WhenModifier+ -- | If the event hasn't been invoked before then do it immediately+ -- when we start the cron process.+ = Immediate++ -- | Wait until after this time before doing it first.+ | WaitUntil UTCTime+ deriving (Read, Show, Eq)+++-- Event ------------------------------------------------------------------------------------------+type EventName = String++-- | Records when an event should start, and when it last ran.+data Event+ = Event+ { -- | A unique name for this event.+ -- Used when writing the schedule to a file.+ eventName :: EventName++ -- | When to run the command.+ , eventWhen :: When++ -- | Modifier to the previous.+ , eventWhenModifier :: Maybe WhenModifier++ -- | When the event was last started, if any.+ , eventLastStarted :: Maybe UTCTime+ + -- | When the event last finished, if any.+ , eventLastEnded :: Maybe UTCTime }+ deriving (Read, Show, Eq)+ ++-- | Given the current time and a list of events, determine which one should be started now.+-- If several events are avaliable then take the one with the earliest start time.+earliestEventToStartAt :: UTCTime -> [Event] -> Maybe Event+earliestEventToStartAt curTime events+ = let eventsStartable = filter (eventCouldStartAt curTime) events+ eventsSorted = sortBy (compare `on` eventLastStarted) eventsStartable+ in listToMaybe eventsSorted+++-- | Given the current time, decide whether an event could be started.+-- If the `WhenModifier` is `Immediate` this always returns true.+-- The `SkipFirst` modifier is ignored, as this is handled separately.+eventCouldStartAt :: UTCTime -> Event -> Bool+eventCouldStartAt curTime event+ + -- If the current end time is before the start time, then the most+ -- recent iteration is still running, so don't start it again.+ | Just lastStarted <- eventLastStarted event+ , Just lastEnded <- eventLastEnded event+ = lastEnded < lastStarted+ + -- If the event has never started or ended, and is marked as immediate,+ -- then start it right away.+ | Nothing <- eventLastStarted event+ , Nothing <- eventLastEnded event+ , Just Immediate <- eventWhenModifier event+ = True++ -- Keep waiting if there's a seconday wait modifier.+ | Just (WaitUntil waitTime) <- eventWhenModifier event+ , curTime < waitTime+ = False++ -- Otherwise we have to look at the real schedule.+ | otherwise+ = case eventWhen event of+ Always -> True+ Never -> False++ Every diffTime + -> maybe True+ (\lastTime -> (curTime `diffUTCTime` lastTime ) > diffTime)+ (eventLastStarted event)++ After diffTime + -> maybe True+ (\lastTime -> (curTime `diffUTCTime` lastTime ) > diffTime)+ (eventLastEnded event)+ + Daily timeOfDay+ -- If it's been more than a day since we last started it, then do it now.+ | Just lastStarted <- eventLastStarted event+ , (curTime `diffUTCTime` lastStarted) > day+ -> True+ + | otherwise+ -> let -- If we were going to run it today, this is when it would be.+ startTimeToday+ = curTime+ { utctDayTime = timeOfDayToTime timeOfDay }+ + -- If it's after that time then quit fooling around..+ in curTime > startTimeToday+++-- Schedule ---------------------------------------------------------------------------------------+-- | Map of event names to their details and build commands. +data Schedule cmd+ = Schedule (Map EventName (Event, cmd))+++-- | Get the list of events in a schedule, ignoring the build commands.+eventsOfSchedule :: Schedule cmd -> [Event]+eventsOfSchedule (Schedule sched)+ = map fst $ Map.elems sched+++-- | A nice way to produce a schedule.+makeSchedule :: [(EventName, When, Maybe WhenModifier, cmd)] -> Schedule cmd+makeSchedule tuples+ = let makeSched (name, whn, mMod, cmd)+ = (name, (Event name whn mMod Nothing Nothing, cmd))+ in Schedule $ Map.fromList $ map makeSched tuples+++-- | Given an event name, lookup the associated event from a schedule.+lookupEventOfSchedule :: EventName -> Schedule cmd -> Maybe Event+lookupEventOfSchedule name (Schedule sched)+ = liftM fst $ Map.lookup name sched+ + +-- | Given an event name, lookup the associated build command from a schedule.+lookupCommandOfSchedule :: EventName -> Schedule cmd -> Maybe cmd+lookupCommandOfSchedule name (Schedule sched)+ = liftM snd $ Map.lookup name sched+++-- | Given a new version of an event, update any matching event in the schedule.+-- If the event not already there then return the original schedule.+adjustEventOfSchedule :: Event -> Schedule cmd -> Schedule cmd+adjustEventOfSchedule event (Schedule sched)+ = Schedule + $ Map.adjust + (\(_, build) -> (event, build))+ (eventName event) + sched
+ BuildBox/Pretty.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE TypeSynonymInstances #-}++-- | Pretty printing utils.+module BuildBox.Pretty+ ( module Text.PrettyPrint+ , Pretty(..)+ , pprPSecTime+ , pprFloatTime+ , pprFloatSF+ , pprFloatRef+ , padRc, padR+ , padLc, padL+ , blank)+where+import Text.PrettyPrint+import Data.Time++-- Things that can be pretty printed+class Pretty a where+ ppr :: a -> Doc++-- Basic instances+instance Pretty Doc where+ ppr = id++instance Pretty String where+ ppr = text+ +instance Pretty Float where+ ppr = text . show++instance Pretty Int where+ ppr = int+ +instance Pretty Integer where+ ppr = text . show++instance Pretty UTCTime where+ ppr = text . show+ ++-- To handle type defaulting+ten12 :: Float+ten12 = 10^(12 :: Integer)++ten12i :: Integer+ten12i = 10^(12 :: Integer)+++-- | Print a number of picoseconds as a time.+pprPSecTime :: Integer -> Doc+pprPSecTime psecs+ = text (show (psecs `div` ten12i))+ <> text "." + <> (text $ (take 3 $ render $ padRc 12 '0' $ text $ show $ psecs `mod` ten12i))+++-- | Print a float number of seconds as a time.+pprFloatTime :: Float -> Doc+pprFloatTime stime+ = let psecs = truncate (stime * ten12)+ in pprPSecTime psecs+++-- | Pretty print a signed float, with a percentage change relative to a reference figure.+-- Comes out like @0.235( +5)@ for a +5 percent swing.+pprFloatRef :: Float -> Float -> Doc+pprFloatRef stime stimeRef + = let psecs = truncate (stime * ten12)+ diff = (1 - (stimeRef / stime))*100+ in pprPSecTime psecs+ <> parens (padR 3 $ pprFloatSF diff)+++-- | Print a float number of seconds, flooring it and, prefixing with @+@ or @-@ appropriately.+pprFloatSF :: Float -> Doc+pprFloatSF p+ | p > 0+ = text "+" <> (ppr $ (floor p :: Integer))+ + | otherwise+ = text "-" <> (ppr $ (floor (negate p) :: Integer))+++-- | Right justify a doc, padding with a given character.+padRc :: Int -> Char -> Doc -> Doc+padRc n c str+ = (text $ replicate (n - length (render str)) c) <> str+ ++-- | Right justify a string with spaces.+padR :: Int -> Doc -> Doc+padR n str = padRc n ' ' str+++-- | Left justify a string, padding with a given character.+padLc :: Int -> Char -> Doc -> Doc+padLc n c str+ = str <> (text $ replicate (n - length (render str)) c)+++-- | Left justify a string with spaces.+padL :: Int -> Doc -> Doc+padL n str = padLc n ' ' str++-- | Blank text. This is different different from `empty` because it comes out a a newline when used in a `vcat`.+blank :: Doc+blank = ppr ""
+ BuildBox/Time.hs view
@@ -0,0 +1,82 @@++-- | Time utils useful for writing buildbots.+module BuildBox.Time+ ( module Data.Time+ , readLocalTimeOfDayAsUTC+ , getStampyTime+ , getMidnightTodayLocal+ , getMidnightTodayUTC+ , getMidnightTomorrowLocal+ , getMidnightTomorrowUTC)+where+import Data.Time+import System.Locale+++-- | Read a time of day string like @17:34:05@ in the local time zone+-- and convert that to a UTC time of day. Good for parsing command line args to buildbots.+readLocalTimeOfDayAsUTC :: String -> IO TimeOfDay+readLocalTimeOfDayAsUTC str+ = do + -- Get the current timeZone.+ curTime <- getZonedTime++ -- Convert the time of day to what it would be as UTC.+ let (_, timeOfDayUTC)+ = localToUTCTimeOfDay + (zonedTimeZone curTime)+ (read str) ++ return timeOfDayUTC+++-- | Get a local time stamp with format YYYYMMDD_HHMMSS. Good for naming files with.+getStampyTime :: IO String+getStampyTime+ = do time <- getZonedTime+ return $ formatTime defaultTimeLocale "%Y%m%d_%k%M%S" time+++-- | Get the local midnight we've just had as a `LocalTime`.+getMidnightTodayLocal :: IO LocalTime+getMidnightTodayLocal+ = do curTime <- getZonedTime+ return $ LocalTime+ { localDay = localDay $ zonedTimeToLocalTime curTime+ , localTimeOfDay = midnight }+++-- | Get the local midnight that we've just had, as a `UTCTime`.+getMidnightTodayUTC :: IO UTCTime +getMidnightTodayUTC+ = do curTime <- getZonedTime+ return $ zonedTimeToUTC+ $ ZonedTime+ (LocalTime + { localDay = localDay $ zonedTimeToLocalTime curTime+ , localTimeOfDay = midnight })+ (zonedTimeZone curTime)+++-- | Get the local midnight we're about to have as a `LocalTime`.+getMidnightTomorrowLocal :: IO LocalTime+getMidnightTomorrowLocal+ = do curTime <- getZonedTime+ return $ LocalTime+ { localDay = addDays 1 (localDay (zonedTimeToLocalTime curTime)) + , localTimeOfDay = midnight }++-- | Get the local midnight we're about to have as a `UTCTime`.+getMidnightTomorrowUTC :: IO UTCTime+getMidnightTomorrowUTC+ = do curTime <- getZonedTime+ return $ zonedTimeToUTC+ $ ZonedTime+ (LocalTime+ { localDay = addDays 1 (localDay (zonedTimeToLocalTime curTime)) + , localTimeOfDay = midnight })+ (zonedTimeZone curTime)+++ +
+ LICENSE view
@@ -0,0 +1,24 @@+Copyright (c) 2010, University of New South Wales.+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:+ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.+ * Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the distribution.+ * Neither the name of the University of New South Wales nor the+ names of its contributors may be used to endorse or promote products+ derived from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS BE LIABLE FOR ANY+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ buildbox.cabal view
@@ -0,0 +1,64 @@+Name: buildbox+Version: 1.0.0.0+License: BSD3+License-file: LICENSE+Author: Ben Lippmeier+Maintainer: Ben Lippmeier <benl@ouroborus.net>+Build-Type: Simple+Cabal-Version: >=1.6+Stability: experimental+Category: Development, Testing+Homepage: http://code.haskell.org/~benl/code/buildbox-head+Description:+ Rehackable components for writing buildbots and test harnesses. Includes functions+ for checking the host platform, running tests, capturing output, handling errors,+ comparing runtimes against a baseline, sending mail, running events to a schedule etc.++ Some of these functions are just wrappers around shell commands, so be careful about passing+ them weirdly formed arguments.+ +Synopsis:+ Rehackable components for writing buildbots and test harnesses.++Tested-with:+ GHC == 6.12.1++Library+ Build-Depends: + base == 4.*,+ time == 1.1.*,+ mtl == 1.1.*,+ directory == 1.0.*,+ process == 1.0.*,+ unix == 2.4.*,+ pretty == 1.0.*,+ old-locale == 1.0.*,+ containers == 0.3.*++ ghc-options:+ -Wall++ Exposed-modules:+ BuildBox.Benchmark.Base+ BuildBox.Benchmark.Compare+ BuildBox.Benchmark.Pretty+ BuildBox.Benchmark.TimeAspect+ BuildBox.Benchmark+ BuildBox.Build.Base+ BuildBox.Build.Testable+ BuildBox.Build+ BuildBox.Command.Environment+ BuildBox.Command.File+ BuildBox.Command.Mail+ BuildBox.Command.Network+ BuildBox.Command.System+ BuildBox.Command.Sleep+ BuildBox.Cron.Schedule+ BuildBox.Cron+ BuildBox.Pretty+ BuildBox.Time+ BuildBox++ Other-modules:+ BuildBox.Command.System.Internals+