gdo-0.1.2: Lib.hs
module Lib
( putStrErr
, putStrLnErr
-- * directory names
, getMetaDir
, getMissingDepsFile
, getDefaultDirs
-- * Actions
, executeRecipe
, getRecipeFile
, upToDate
, recordDependency
, recordMissingDependency
, purgeRecordedDependencies
, getHashesAndDepsForTarget
)
where
import Control.Exception (finally,tryJust)
import Control.Monad (when,filterM,guard)
import Control.Monad.IO.Class (liftIO, MonadIO)
import Crypto.Hash (hash,Digest,MD5)
import qualified Data.ByteString as BS
import qualified Data.Map as Map (toList,fromList,insert)
import Data.Maybe (listToMaybe)
import System.Directory (getCurrentDirectory, makeAbsolute, doesFileExist, renameFile, removeFile, createDirectoryIfMissing, getDirectoryContents, removeDirectoryRecursive, withCurrentDirectory)
import System.Environment (getEnvironment,lookupEnv)
import System.Exit (ExitCode(ExitSuccess))
import System.FilePath ((</>), takeBaseName, dropFileName, replaceBaseName, addExtension,splitSearchPath)
import System.IO (hPutStrLn,stderr,hPutStr,withFile,IOMode(ReadMode,WriteMode,AppendMode),hFileSize)
import System.IO.Error (isDoesNotExistError)
import qualified System.Process as Proc (env, waitForProcess, createProcess, proc)
import DepInfo
-- | Print a string to standard error output with a newline character
-- in the end.
putStrLnErr :: (MonadIO m) => String -> m ()
putStrLnErr msg =
liftIO $ hPutStrLn stderr msg
-- | Print a string to standard error output.
putStrErr :: (MonadIO m) => String -> m ()
putStrErr = liftIO . hPutStr stderr
-- | Get the path for the directory where the targets meta information
-- is stored. This will only work, if you are in the same directory
-- as the target.
getMetaDir :: (MonadIO m) =>
String -- ^ target name
-> m FilePath
getMetaDir target = do
current <- liftIO $ makeAbsolute =<< getCurrentDirectory
return $ current </> metadir </> target
metadir :: FilePath
metadir = ".gdo"
getMissingDepsFile :: (MonadIO m) => String -> m FilePath
getMissingDepsFile target = (</> gdoMissingFile) <$> getMetaDir target
gdoMissingFile :: FilePath
gdoMissingFile = "gdo---watch-dir"
metaIgnoreFiles :: [FilePath]
metaIgnoreFiles = [".","..",gdoMissingFile]
-- | Execute a build recipe for a given target.
executeRecipe :: FilePath -- ^ build target
-> FilePath -- ^ recipe file
-> IO ExitCode
executeRecipe target recipe = do
currentDir <- getCurrentDirectory
let tempFilePath = currentDir </> tempFileName
createFile tempFilePath
finally
(do
process <- modEnv $ Proc.proc recipe
[targetDir,takeBaseName target, tempFilePath]
(_,_,_,ph) <- liftIO $ Proc.createProcess process
code <- liftIO $ Proc.waitForProcess ph
if code /= ExitSuccess
then putStrLnErr $ "*** redo failed for target "++show target
else do filesize <- getFileSize tempFilePath
when (filesize /= 0) $ renameFile tempFilePath target
return code
)
(do exists <- doesFileExist tempFilePath
when exists $ removeFile tempFilePath
)
where
modEnv process = do
oldEnv <- liftIO getEnvironment
return process {Proc.env =
Just (Map.toList .
Map.insert "REDO_TARGET" target .
Map.fromList $ oldEnv)
}
targetDir = dropFileName target
tempFileName = target ++ "---gdo"
getFileSize :: FilePath -> IO Integer
getFileSize path = withFile path ReadMode hFileSize
createFile :: FilePath -> IO ()
createFile path = writeFile path ""
-- | Find out which recipe file to use to build a target.
getRecipeFile :: (MonadIO m) =>
[FilePath] -- ^ List of extra search paths with
-- potential default do files
-> FilePath -- ^ the target name
-> m (Maybe FilePath)
getRecipeFile defaultDirs target =
liftIO $ mapM makeAbsolute =<< listToMaybe <$>
filterM doesFileExist
( [ addExtension target ".do"
, replaceBaseName target "default" ++ ".do"
] ++ defaultsDirFile
)
where
defaultDoFile = replaceBaseName target "default" ++ ".do"
defaultsDirFile = fmap (</> defaultDoFile) defaultDirs
-- Get the default dirs from the environment.
getDefaultDirs :: (MonadIO m) => m [FilePath]
getDefaultDirs =
maybe [] splitSearchPath <$> liftIO (lookupEnv "GDO_DEFAULTS")
hashFile :: (MonadIO m) => FilePath -> m (Digest MD5)
hashFile target = do
bs <- liftIO $ BS.readFile target
return $ hash bs
upToDate :: (MonadIO m) =>
[FilePath]
-> FilePath
-> m Bool
upToDate mDefaultsDir buildTarget = do
metaDepDir <- getMetaDir buildTarget
liftIO $
maybe
-- When no recipe exists, we assume the target is up to date.
(return True)
( const $ do
r <- tryJust
(guard . isDoesNotExistError)
( do md5s <- Prelude.filter (not . (`elem` metaIgnoreFiles)) <$>
getDirectoryContents metaDepDir
depsNotChanged <- and <$> mapM upToDate' md5s
noNewFiles <- not <$> newFilesCreated
return $ depsNotChanged && noNewFiles
)
return $ fromRight (const False) r
) =<< getRecipeFile mDefaultsDir buildTarget
where
fromRight :: (a -> b) -> Either a b -> b
fromRight f = either f id
upToDate' :: String -> IO Bool
upToDate' oldHash = do
metaDepDir <- getMetaDir buildTarget
r <- tryJust (guard . isDoesNotExistError) $ do
dep <- readFile $ metaDepDir </> oldHash
newHash <- show <$> hashFile dep
depUpToDate <- upToDate mDefaultsDir dep
let allUpToDate = oldHash == newHash && depUpToDate
return allUpToDate
case r of
Right b -> return b
Left _ -> return False
newFilesCreated :: IO Bool
newFilesCreated = do
missingDepsFile <- getMissingDepsFile buildTarget
r <- tryJust (guard . isDoesNotExistError) $
Prelude.filter (\e -> (e `notElem` metaIgnoreFiles) ||
Prelude.null e) .
lines <$>
readFile missingDepsFile
case r of
Left _ -> return False
Right watchlist -> or <$> mapM
doesFileExist watchlist
getHashesAndDepsForTarget :: FilePath -> IO [DepInfo]
getHashesAndDepsForTarget target = do
metaDir <- getMetaDir target
(filenames, oldMd5s) <- withCurrentDirectory metaDir $ do
hashes <- filter (not . (`elem` metaIgnoreFiles)) <$>
getDirectoryContents metaDir
(,) <$> mapM readFile hashes <*> pure hashes
md5s <- mapM hashFromPath filenames
return (zipWith3 DepInfo
filenames
(map Just oldMd5s)
(map (Just . show) md5s))
where
hashFromPath :: FilePath -> IO (Digest MD5)
hashFromPath f = hash <$> BS.readFile f
recordDependency :: FilePath -- ^ target name
-> FilePath -- ^ dependency name
-> IO ()
recordDependency target dep = do
newHash <- show <$> hashFile dep
metaDepDir <- getMetaDir target
let md5file = metaDepDir </> newHash
liftIO $ createDirectoryIfMissing True metaDepDir
liftIO $ withFile md5file WriteMode
(`hPutStr` dep)
recordMissingDependency :: FilePath -> FilePath -> IO ()
recordMissingDependency target dep = do
metaDepsDir <- getMetaDir target
missingDepsFile <- getMissingDepsFile target
liftIO $ createDirectoryIfMissing True metaDepsDir
liftIO $ withFile
missingDepsFile
AppendMode
(`hPutStrLn` dep)
purgeRecordedDependencies :: FilePath -> IO ()
purgeRecordedDependencies target =
removeDirectoryRecursive =<< getMetaDir target