packages feed

gdo-0.1.0: Lib.hs

module Lib
       ( putStrErr
       , putStrLnErr
         -- * directory names
       , getMetaDir
       , getMissingDepsFile
       , metaIgnoreFiles
       , getDefaultDirs
         -- * Actions
       , executeRecipe
       , getRecipeFile
       , upToDate
       , recordDependency
       )
       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)
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),hFileSize)
import           System.IO.Error (isDoesNotExistError)
import qualified System.Process as Proc

putStrLnErr :: (MonadIO m) => String -> m ()
putStrLnErr msg =
  liftIO $ hPutStrLn stderr msg

putStrErr :: (MonadIO m) => String -> m ()
putStrErr = liftIO . hPutStr stderr

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
  createFile tempFileName
  finally
    (do
        process <- modEnv $ Proc.proc recipe
                   [targetDir,takeBaseName target, tempFileName]
        (_,_,_,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 tempFileName
                  when (filesize /= 0) $ renameFile tempFileName target
        return code
    )
    (do exists <- doesFileExist tempFileName
        when exists $ removeFile tempFileName
    )
  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")

upToDate :: (MonadIO m) =>
            [FilePath]
         -> FilePath
         -> m Bool
upToDate mDefaultsDir buildTarget = do
  metaDepDir <- getMetaDir buildTarget
  liftIO $
    not . Prelude.null <$> getRecipeFile mDefaultsDir buildTarget >>= \canBuild ->
    if canBuild
    then 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 $ either (const False) id r
    else return True
  where
    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

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)

hashFile :: (MonadIO m) => FilePath -> m (Digest MD5)
hashFile target = do
  bs <- liftIO $ BS.readFile target
  return $ hash bs