123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- {-# LANGUAGE OverloadedStrings #-}
- {-# LANGUAGE CPP #-}
- {-# LANGUAGE ScopedTypeVariables #-}
- {-# LANGUAGE FlexibleContexts #-}
- module Build
- ( getDeps
- , touchDeps
- , touch
- , recompDeps
- , isNewerThan
- , safeReadFile
- ) where
- import Control.Applicative ((<|>), many, (<$>))
- import qualified Data.Attoparsec.Text as A
- import Data.Char (isSpace, isUpper)
- import qualified Data.Text as T
- import Data.Text.Encoding (decodeUtf8With)
- import Data.Text.Encoding.Error (lenientDecode)
- import Data.ByteString (ByteString)
- import qualified Data.ByteString as S
- import Control.Exception (SomeException, try, IOException)
- import Control.Exception.Lifted (handle)
- import Control.Monad (when, filterM, forM, forM_, (>=>))
- import Control.Monad.Trans.State (StateT, get, put, execStateT)
- import Control.Monad.Trans.Writer (WriterT, tell, execWriterT)
- import Control.Monad.IO.Class (MonadIO, liftIO)
- import Control.Monad.Trans.Class (lift)
- import Data.Monoid (Monoid (mappend, mempty))
- import qualified Data.Map as Map
- import qualified Data.Set as Set
- import qualified System.Posix.Types
- import System.Directory
- import System.FilePath (takeExtension, replaceExtension, (</>), takeDirectory,
- splitPath, joinPath)
- import System.PosixCompat.Files (getFileStatus, setFileTimes,
- accessTime, modificationTime)
- import Text.Shakespeare (Deref)
- import Text.Julius (juliusUsedIdentifiers)
- import Text.Cassius (cassiusUsedIdentifiers)
- import Text.Lucius (luciusUsedIdentifiers)
- safeReadFile :: MonadIO m => FilePath -> m (Either IOException ByteString)
- safeReadFile = liftIO . try . S.readFile
- touch :: IO ()
- touch = do
- m <- handle (\(_ :: SomeException) -> return Map.empty) $ readFile touchCache >>= readIO
- x <- fmap snd (getDeps [])
- m' <- execStateT (execWriterT $ touchDeps id updateFileTime x) m
- createDirectoryIfMissing True $ takeDirectory touchCache
- writeFile touchCache $ show m'
- where
- touchCache = "dist/touchCache.txt"
- -- | Returns True if any files were touched, otherwise False
- recompDeps :: [FilePath] -> StateT (Map.Map FilePath (Set.Set Deref)) IO Bool
- recompDeps =
- fmap toBool . execWriterT . (liftIO . getDeps >=> touchDeps hiFile removeHi . snd)
- where
- toBool NoFilesTouched = False
- toBool SomeFilesTouched = True
- type Deps = Map.Map FilePath ([FilePath], ComparisonType)
- getDeps :: [FilePath] -> IO ([FilePath], Deps)
- getDeps hsSourceDirs = do
- let defSrcDirs = case hsSourceDirs of
- [] -> ["."]
- ds -> ds
- hss <- fmap concat $ mapM findHaskellFiles defSrcDirs
- deps' <- mapM determineDeps hss
- return $ (hss, fixDeps $ zip hss deps')
- data AnyFilesTouched = NoFilesTouched | SomeFilesTouched
- instance Monoid AnyFilesTouched where
- mempty = NoFilesTouched
- mappend NoFilesTouched NoFilesTouched = mempty
- mappend _ _ = SomeFilesTouched
- touchDeps :: (FilePath -> FilePath) ->
- (FilePath -> FilePath -> IO ()) ->
- Deps -> WriterT AnyFilesTouched (StateT (Map.Map FilePath (Set.Set Deref)) IO) ()
- touchDeps f action deps = (mapM_ go . Map.toList) deps
- where
- go (x, (ys, ct)) = do
- isChanged <- handle (\(_ :: SomeException) -> return True) $ lift $
- case ct of
- AlwaysOutdated -> return True
- CompareUsedIdentifiers getDerefs -> do
- derefMap <- get
- ebs <- safeReadFile x
- let newDerefs =
- case ebs of
- Left _ -> Set.empty
- Right bs -> Set.fromList $ getDerefs $ T.unpack $ decodeUtf8With lenientDecode bs
- put $ Map.insert x newDerefs derefMap
- case Map.lookup x derefMap of
- Just oldDerefs | oldDerefs == newDerefs -> return False
- _ -> return True
- when isChanged $ forM_ ys $ \y -> do
- n <- liftIO $ x `isNewerThan` f y
- when n $ do
- liftIO $ putStrLn ("Forcing recompile for " ++ y ++ " because of " ++ x)
- liftIO $ action x y
- tell SomeFilesTouched
- -- | remove the .hi files for a .hs file, thereby forcing a recompile
- removeHi :: FilePath -> FilePath -> IO ()
- removeHi _ hs = mapM_ removeFile' hiFiles
- where
- removeFile' file = try' (removeFile file) >> return ()
- hiFiles = map (\e -> "dist/build" </> removeSrc (replaceExtension hs e))
- ["hi", "p_hi"]
- -- | change file mtime of .hs file to that of the dependency
- updateFileTime :: FilePath -> FilePath -> IO ()
- updateFileTime x hs = do
- (_ , modx) <- getFileStatus' x
- (access, _ ) <- getFileStatus' hs
- _ <- try' (setFileTimes hs access modx)
- return ()
- hiFile :: FilePath -> FilePath
- hiFile hs = "dist/build" </> removeSrc (replaceExtension hs "hi")
- removeSrc :: FilePath -> FilePath
- removeSrc f = case splitPath f of
- ("src/" : xs) -> joinPath xs
- _ -> f
- try' :: IO x -> IO (Either SomeException x)
- try' = try
- isNewerThan :: FilePath -> FilePath -> IO Bool
- isNewerThan f1 f2 = do
- (_, mod1) <- getFileStatus' f1
- (_, mod2) <- getFileStatus' f2
- return (mod1 > mod2)
- getFileStatus' :: FilePath ->
- IO (System.Posix.Types.EpochTime, System.Posix.Types.EpochTime)
- getFileStatus' fp = do
- efs <- try' $ getFileStatus fp
- case efs of
- Left _ -> return (0, 0)
- Right fs -> return (accessTime fs, modificationTime fs)
- fixDeps :: [(FilePath, [(ComparisonType, FilePath)])] -> Deps
- fixDeps =
- Map.unionsWith combine . map go
- where
- go :: (FilePath, [(ComparisonType, FilePath)]) -> Deps
- go (x, ys) = Map.fromList $ map (\(ct, y) -> (y, ([x], ct))) ys
- combine (ys1, ct) (ys2, _) = (ys1 `mappend` ys2, ct)
- findHaskellFiles :: FilePath -> IO [FilePath]
- findHaskellFiles path = do
- contents <- getDirectoryContents path
- fmap concat $ mapM go contents
- where
- go ('.':_) = return []
- go filename = do
- d <- doesDirectoryExist full
- if not d
- then if isHaskellFile
- then return [full]
- else return []
- else if isHaskellDir
- then findHaskellFiles full
- else return []
- where
- -- this could fail on unicode
- isHaskellDir = isUpper (head filename)
- isHaskellFile = takeExtension filename `elem` watch_files
- full = path </> filename
- watch_files = [".hs", ".lhs"]
- data TempType = StaticFiles FilePath
- | Verbatim | Messages FilePath | Hamlet | Widget | Julius | Cassius | Lucius
- deriving Show
- -- | How to tell if a file is outdated.
- data ComparisonType = AlwaysOutdated
- | CompareUsedIdentifiers (String -> [Deref])
- determineDeps :: FilePath -> IO [(ComparisonType, FilePath)]
- determineDeps x = do
- y <- safeReadFile x
- case y of
- Left _ -> return []
- Right bs -> do
- let z = A.parseOnly (many $ (parser <|> (A.anyChar >> return Nothing)))
- $ decodeUtf8With lenientDecode bs
- case z of
- Left _ -> return []
- Right r -> mapM go r >>= filterM (doesFileExist . snd) . concat
- where
- go (Just (StaticFiles fp, _)) = map ((,) AlwaysOutdated) <$> getFolderContents fp
- go (Just (Hamlet, f)) = return [(AlwaysOutdated, f)]
- go (Just (Widget, f)) = return
- [ (AlwaysOutdated, "templates/" ++ f ++ ".hamlet")
- , (CompareUsedIdentifiers $ map fst . juliusUsedIdentifiers, "templates/" ++ f ++ ".julius")
- , (CompareUsedIdentifiers $ map fst . luciusUsedIdentifiers, "templates/" ++ f ++ ".lucius")
- , (CompareUsedIdentifiers $ map fst . cassiusUsedIdentifiers, "templates/" ++ f ++ ".cassius")
- ]
- go (Just (Julius, f)) = return [(CompareUsedIdentifiers $ map fst . juliusUsedIdentifiers, f)]
- go (Just (Cassius, f)) = return [(CompareUsedIdentifiers $ map fst . cassiusUsedIdentifiers, f)]
- go (Just (Lucius, f)) = return [(CompareUsedIdentifiers $ map fst . luciusUsedIdentifiers, f)]
- go (Just (Verbatim, f)) = return [(AlwaysOutdated, f)]
- go (Just (Messages f, _)) = map ((,) AlwaysOutdated) <$> getFolderContents f
- go Nothing = return []
- parser = do
- ty <- (do _ <- A.string "\nstaticFiles \""
- x' <- A.many1 $ A.satisfy (/= '"')
- return $ StaticFiles x')
- <|> (A.string "$(parseRoutesFile " >> return Verbatim)
- <|> (A.string "$(hamletFile " >> return Hamlet)
- <|> (A.string "$(ihamletFile " >> return Hamlet)
- <|> (A.string "$(whamletFile " >> return Hamlet)
- <|> (A.string "$(html " >> return Hamlet)
- <|> (A.string "$(widgetFile " >> return Widget)
- <|> (A.string "$(Settings.hamletFile " >> return Hamlet)
- <|> (A.string "$(Settings.widgetFile " >> return Widget)
- <|> (A.string "$(juliusFile " >> return Julius)
- <|> (A.string "$(cassiusFile " >> return Cassius)
- <|> (A.string "$(luciusFile " >> return Lucius)
- <|> (A.string "$(persistFile " >> return Verbatim)
- <|> (
- A.string "$(persistFileWith " >>
- A.many1 (A.satisfy (/= '"')) >>
- return Verbatim)
- <|> (do
- _ <- A.string "\nmkMessage \""
- A.skipWhile (/= '"')
- _ <- A.string "\" \""
- x' <- A.many1 $ A.satisfy (/= '"')
- _ <- A.string "\" \""
- _y <- A.many1 $ A.satisfy (/= '"')
- _ <- A.string "\""
- return $ Messages x')
- case ty of
- Messages{} -> return $ Just (ty, "")
- StaticFiles{} -> return $ Just (ty, "")
- _ -> do
- A.skipWhile isSpace
- _ <- A.char '"'
- y <- A.many1 $ A.satisfy (/= '"')
- _ <- A.char '"'
- A.skipWhile isSpace
- _ <- A.char ')'
- return $ Just (ty, y)
- getFolderContents :: FilePath -> IO [FilePath]
- getFolderContents fp = do
- cs <- getDirectoryContents fp
- let notHidden ('.':_) = False
- notHidden ('t':"mp") = False
- notHidden ('f':"ay") = False
- notHidden _ = True
- fmap concat $ forM (filter notHidden cs) $ \c -> do
- let f = fp ++ '/' : c
- isFile <- doesFileExist f
- if isFile then return [f] else getFolderContents f
|