diff --git a/BuildBox.hs b/BuildBox.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox.hs
@@ -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
diff --git a/BuildBox/Benchmark.hs b/BuildBox/Benchmark.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Benchmark.hs
@@ -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
+	
+	
diff --git a/BuildBox/Benchmark/Base.hs b/BuildBox/Benchmark/Base.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Benchmark/Base.hs
@@ -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 ]
diff --git a/BuildBox/Benchmark/Compare.hs b/BuildBox/Benchmark/Compare.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Benchmark/Compare.hs
@@ -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 ]
+
+
diff --git a/BuildBox/Benchmark/Pretty.hs b/BuildBox/Benchmark/Pretty.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Benchmark/Pretty.hs
@@ -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	
diff --git a/BuildBox/Benchmark/TimeAspect.hs b/BuildBox/Benchmark/TimeAspect.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Benchmark/TimeAspect.hs
@@ -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
+
diff --git a/BuildBox/Build.hs b/BuildBox/Build.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Build.hs
@@ -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
+
+	
+
diff --git a/BuildBox/Build/Base.hs b/BuildBox/Build/Base.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Build/Base.hs
@@ -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 ()
+	
diff --git a/BuildBox/Build/Testable.hs b/BuildBox/Build/Testable.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Build/Testable.hs
@@ -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
+
diff --git a/BuildBox/Command/Environment.hs b/BuildBox/Command/Environment.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Command/Environment.hs
@@ -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
+
+
diff --git a/BuildBox/Command/File.hs b/BuildBox/Command/File.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Command/File.hs
@@ -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
+	
diff --git a/BuildBox/Command/Mail.hs b/BuildBox/Command/Mail.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Command/Mail.hs
@@ -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)
+
+		
diff --git a/BuildBox/Command/Network.hs b/BuildBox/Command/Network.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Command/Network.hs
@@ -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
+		
+
+
+	
+
diff --git a/BuildBox/Command/Sleep.hs b/BuildBox/Command/Sleep.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Command/Sleep.hs
@@ -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
diff --git a/BuildBox/Command/System.hs b/BuildBox/Command/System.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Command/System.hs
@@ -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
+
diff --git a/BuildBox/Command/System/Internals.hs b/BuildBox/Command/System/Internals.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Command/System/Internals.hs
@@ -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
diff --git a/BuildBox/Cron.hs b/BuildBox/Cron.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Cron.hs
@@ -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'
+				
+		
+		
+	
diff --git a/BuildBox/Cron/Schedule.hs b/BuildBox/Cron/Schedule.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Cron/Schedule.hs
@@ -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
diff --git a/BuildBox/Pretty.hs b/BuildBox/Pretty.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Pretty.hs
@@ -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 ""
diff --git a/BuildBox/Time.hs b/BuildBox/Time.hs
new file mode 100644
--- /dev/null
+++ b/BuildBox/Time.hs
@@ -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)
+
+
+	
+	
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/buildbox.cabal b/buildbox.cabal
new file mode 100644
--- /dev/null
+++ b/buildbox.cabal
@@ -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
+       
