Build.hs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. {-# LANGUAGE OverloadedStrings #-}
  2. {-# LANGUAGE CPP #-}
  3. {-# LANGUAGE ScopedTypeVariables #-}
  4. {-# LANGUAGE FlexibleContexts #-}
  5. module Build
  6. ( getDeps
  7. , touchDeps
  8. , touch
  9. , recompDeps
  10. , isNewerThan
  11. , safeReadFile
  12. ) where
  13. import Control.Applicative ((<|>), many, (<$>))
  14. import qualified Data.Attoparsec.Text as A
  15. import Data.Char (isSpace, isUpper)
  16. import qualified Data.Text as T
  17. import Data.Text.Encoding (decodeUtf8With)
  18. import Data.Text.Encoding.Error (lenientDecode)
  19. import Data.ByteString (ByteString)
  20. import qualified Data.ByteString as S
  21. import Control.Exception (SomeException, try, IOException)
  22. import Control.Exception.Lifted (handle)
  23. import Control.Monad (when, filterM, forM, forM_, (>=>))
  24. import Control.Monad.Trans.State (StateT, get, put, execStateT)
  25. import Control.Monad.Trans.Writer (WriterT, tell, execWriterT)
  26. import Control.Monad.IO.Class (MonadIO, liftIO)
  27. import Control.Monad.Trans.Class (lift)
  28. import Data.Monoid (Monoid (mappend, mempty))
  29. import qualified Data.Map as Map
  30. import qualified Data.Set as Set
  31. import qualified System.Posix.Types
  32. import System.Directory
  33. import System.FilePath (takeExtension, replaceExtension, (</>), takeDirectory,
  34. splitPath, joinPath)
  35. import System.PosixCompat.Files (getFileStatus, setFileTimes,
  36. accessTime, modificationTime)
  37. import Text.Shakespeare (Deref)
  38. import Text.Julius (juliusUsedIdentifiers)
  39. import Text.Cassius (cassiusUsedIdentifiers)
  40. import Text.Lucius (luciusUsedIdentifiers)
  41. safeReadFile :: MonadIO m => FilePath -> m (Either IOException ByteString)
  42. safeReadFile = liftIO . try . S.readFile
  43. touch :: IO ()
  44. touch = do
  45. m <- handle (\(_ :: SomeException) -> return Map.empty) $ readFile touchCache >>= readIO
  46. x <- fmap snd (getDeps [])
  47. m' <- execStateT (execWriterT $ touchDeps id updateFileTime x) m
  48. createDirectoryIfMissing True $ takeDirectory touchCache
  49. writeFile touchCache $ show m'
  50. where
  51. touchCache = "dist/touchCache.txt"
  52. -- | Returns True if any files were touched, otherwise False
  53. recompDeps :: [FilePath] -> StateT (Map.Map FilePath (Set.Set Deref)) IO Bool
  54. recompDeps =
  55. fmap toBool . execWriterT . (liftIO . getDeps >=> touchDeps hiFile removeHi . snd)
  56. where
  57. toBool NoFilesTouched = False
  58. toBool SomeFilesTouched = True
  59. type Deps = Map.Map FilePath ([FilePath], ComparisonType)
  60. getDeps :: [FilePath] -> IO ([FilePath], Deps)
  61. getDeps hsSourceDirs = do
  62. let defSrcDirs = case hsSourceDirs of
  63. [] -> ["."]
  64. ds -> ds
  65. hss <- fmap concat $ mapM findHaskellFiles defSrcDirs
  66. deps' <- mapM determineDeps hss
  67. return $ (hss, fixDeps $ zip hss deps')
  68. data AnyFilesTouched = NoFilesTouched | SomeFilesTouched
  69. instance Monoid AnyFilesTouched where
  70. mempty = NoFilesTouched
  71. mappend NoFilesTouched NoFilesTouched = mempty
  72. mappend _ _ = SomeFilesTouched
  73. touchDeps :: (FilePath -> FilePath) ->
  74. (FilePath -> FilePath -> IO ()) ->
  75. Deps -> WriterT AnyFilesTouched (StateT (Map.Map FilePath (Set.Set Deref)) IO) ()
  76. touchDeps f action deps = (mapM_ go . Map.toList) deps
  77. where
  78. go (x, (ys, ct)) = do
  79. isChanged <- handle (\(_ :: SomeException) -> return True) $ lift $
  80. case ct of
  81. AlwaysOutdated -> return True
  82. CompareUsedIdentifiers getDerefs -> do
  83. derefMap <- get
  84. ebs <- safeReadFile x
  85. let newDerefs =
  86. case ebs of
  87. Left _ -> Set.empty
  88. Right bs -> Set.fromList $ getDerefs $ T.unpack $ decodeUtf8With lenientDecode bs
  89. put $ Map.insert x newDerefs derefMap
  90. case Map.lookup x derefMap of
  91. Just oldDerefs | oldDerefs == newDerefs -> return False
  92. _ -> return True
  93. when isChanged $ forM_ ys $ \y -> do
  94. n <- liftIO $ x `isNewerThan` f y
  95. when n $ do
  96. liftIO $ putStrLn ("Forcing recompile for " ++ y ++ " because of " ++ x)
  97. liftIO $ action x y
  98. tell SomeFilesTouched
  99. -- | remove the .hi files for a .hs file, thereby forcing a recompile
  100. removeHi :: FilePath -> FilePath -> IO ()
  101. removeHi _ hs = mapM_ removeFile' hiFiles
  102. where
  103. removeFile' file = try' (removeFile file) >> return ()
  104. hiFiles = map (\e -> "dist/build" </> removeSrc (replaceExtension hs e))
  105. ["hi", "p_hi"]
  106. -- | change file mtime of .hs file to that of the dependency
  107. updateFileTime :: FilePath -> FilePath -> IO ()
  108. updateFileTime x hs = do
  109. (_ , modx) <- getFileStatus' x
  110. (access, _ ) <- getFileStatus' hs
  111. _ <- try' (setFileTimes hs access modx)
  112. return ()
  113. hiFile :: FilePath -> FilePath
  114. hiFile hs = "dist/build" </> removeSrc (replaceExtension hs "hi")
  115. removeSrc :: FilePath -> FilePath
  116. removeSrc f = case splitPath f of
  117. ("src/" : xs) -> joinPath xs
  118. _ -> f
  119. try' :: IO x -> IO (Either SomeException x)
  120. try' = try
  121. isNewerThan :: FilePath -> FilePath -> IO Bool
  122. isNewerThan f1 f2 = do
  123. (_, mod1) <- getFileStatus' f1
  124. (_, mod2) <- getFileStatus' f2
  125. return (mod1 > mod2)
  126. getFileStatus' :: FilePath ->
  127. IO (System.Posix.Types.EpochTime, System.Posix.Types.EpochTime)
  128. getFileStatus' fp = do
  129. efs <- try' $ getFileStatus fp
  130. case efs of
  131. Left _ -> return (0, 0)
  132. Right fs -> return (accessTime fs, modificationTime fs)
  133. fixDeps :: [(FilePath, [(ComparisonType, FilePath)])] -> Deps
  134. fixDeps =
  135. Map.unionsWith combine . map go
  136. where
  137. go :: (FilePath, [(ComparisonType, FilePath)]) -> Deps
  138. go (x, ys) = Map.fromList $ map (\(ct, y) -> (y, ([x], ct))) ys
  139. combine (ys1, ct) (ys2, _) = (ys1 `mappend` ys2, ct)
  140. findHaskellFiles :: FilePath -> IO [FilePath]
  141. findHaskellFiles path = do
  142. contents <- getDirectoryContents path
  143. fmap concat $ mapM go contents
  144. where
  145. go ('.':_) = return []
  146. go filename = do
  147. d <- doesDirectoryExist full
  148. if not d
  149. then if isHaskellFile
  150. then return [full]
  151. else return []
  152. else if isHaskellDir
  153. then findHaskellFiles full
  154. else return []
  155. where
  156. -- this could fail on unicode
  157. isHaskellDir = isUpper (head filename)
  158. isHaskellFile = takeExtension filename `elem` watch_files
  159. full = path </> filename
  160. watch_files = [".hs", ".lhs"]
  161. data TempType = StaticFiles FilePath
  162. | Verbatim | Messages FilePath | Hamlet | Widget | Julius | Cassius | Lucius
  163. deriving Show
  164. -- | How to tell if a file is outdated.
  165. data ComparisonType = AlwaysOutdated
  166. | CompareUsedIdentifiers (String -> [Deref])
  167. determineDeps :: FilePath -> IO [(ComparisonType, FilePath)]
  168. determineDeps x = do
  169. y <- safeReadFile x
  170. case y of
  171. Left _ -> return []
  172. Right bs -> do
  173. let z = A.parseOnly (many $ (parser <|> (A.anyChar >> return Nothing)))
  174. $ decodeUtf8With lenientDecode bs
  175. case z of
  176. Left _ -> return []
  177. Right r -> mapM go r >>= filterM (doesFileExist . snd) . concat
  178. where
  179. go (Just (StaticFiles fp, _)) = map ((,) AlwaysOutdated) <$> getFolderContents fp
  180. go (Just (Hamlet, f)) = return [(AlwaysOutdated, f)]
  181. go (Just (Widget, f)) = return
  182. [ (AlwaysOutdated, "templates/" ++ f ++ ".hamlet")
  183. , (CompareUsedIdentifiers $ map fst . juliusUsedIdentifiers, "templates/" ++ f ++ ".julius")
  184. , (CompareUsedIdentifiers $ map fst . luciusUsedIdentifiers, "templates/" ++ f ++ ".lucius")
  185. , (CompareUsedIdentifiers $ map fst . cassiusUsedIdentifiers, "templates/" ++ f ++ ".cassius")
  186. ]
  187. go (Just (Julius, f)) = return [(CompareUsedIdentifiers $ map fst . juliusUsedIdentifiers, f)]
  188. go (Just (Cassius, f)) = return [(CompareUsedIdentifiers $ map fst . cassiusUsedIdentifiers, f)]
  189. go (Just (Lucius, f)) = return [(CompareUsedIdentifiers $ map fst . luciusUsedIdentifiers, f)]
  190. go (Just (Verbatim, f)) = return [(AlwaysOutdated, f)]
  191. go (Just (Messages f, _)) = map ((,) AlwaysOutdated) <$> getFolderContents f
  192. go Nothing = return []
  193. parser = do
  194. ty <- (do _ <- A.string "\nstaticFiles \""
  195. x' <- A.many1 $ A.satisfy (/= '"')
  196. return $ StaticFiles x')
  197. <|> (A.string "$(parseRoutesFile " >> return Verbatim)
  198. <|> (A.string "$(hamletFile " >> return Hamlet)
  199. <|> (A.string "$(ihamletFile " >> return Hamlet)
  200. <|> (A.string "$(whamletFile " >> return Hamlet)
  201. <|> (A.string "$(html " >> return Hamlet)
  202. <|> (A.string "$(widgetFile " >> return Widget)
  203. <|> (A.string "$(Settings.hamletFile " >> return Hamlet)
  204. <|> (A.string "$(Settings.widgetFile " >> return Widget)
  205. <|> (A.string "$(juliusFile " >> return Julius)
  206. <|> (A.string "$(cassiusFile " >> return Cassius)
  207. <|> (A.string "$(luciusFile " >> return Lucius)
  208. <|> (A.string "$(persistFile " >> return Verbatim)
  209. <|> (
  210. A.string "$(persistFileWith " >>
  211. A.many1 (A.satisfy (/= '"')) >>
  212. return Verbatim)
  213. <|> (do
  214. _ <- A.string "\nmkMessage \""
  215. A.skipWhile (/= '"')
  216. _ <- A.string "\" \""
  217. x' <- A.many1 $ A.satisfy (/= '"')
  218. _ <- A.string "\" \""
  219. _y <- A.many1 $ A.satisfy (/= '"')
  220. _ <- A.string "\""
  221. return $ Messages x')
  222. case ty of
  223. Messages{} -> return $ Just (ty, "")
  224. StaticFiles{} -> return $ Just (ty, "")
  225. _ -> do
  226. A.skipWhile isSpace
  227. _ <- A.char '"'
  228. y <- A.many1 $ A.satisfy (/= '"')
  229. _ <- A.char '"'
  230. A.skipWhile isSpace
  231. _ <- A.char ')'
  232. return $ Just (ty, y)
  233. getFolderContents :: FilePath -> IO [FilePath]
  234. getFolderContents fp = do
  235. cs <- getDirectoryContents fp
  236. let notHidden ('.':_) = False
  237. notHidden ('t':"mp") = False
  238. notHidden ('f':"ay") = False
  239. notHidden _ = True
  240. fmap concat $ forM (filter notHidden cs) $ \c -> do
  241. let f = fp ++ '/' : c
  242. isFile <- doesFileExist f
  243. if isFile then return [f] else getFolderContents f