-- | This is the module which binds it all together -- {-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings #-} module Hakyll.Core.Run ( run ) where import Prelude hiding (reverse) import Control.Monad (filterM, forM_) import Control.Monad.Trans (liftIO) import Control.Applicative (Applicative, (<$>)) import Control.Monad.Reader (ReaderT, runReaderT, ask) import Control.Monad.State.Strict (StateT, runStateT, get, put) import Data.Map (Map) import qualified Data.Map as M import Data.Monoid (mempty, mappend) import System.FilePath (()) import qualified Data.Set as S import Hakyll.Core.Routes import Hakyll.Core.Identifier import Hakyll.Core.Util.File import Hakyll.Core.Compiler import Hakyll.Core.Compiler.Internal import Hakyll.Core.Resource import Hakyll.Core.Resource.Provider import Hakyll.Core.Resource.Provider.File import Hakyll.Core.Rules.Internal import Hakyll.Core.DirectedGraph import Hakyll.Core.DependencyAnalyzer import Hakyll.Core.Writable import Hakyll.Core.Store import Hakyll.Core.Configuration import Hakyll.Core.Logger -- | Run all rules needed, return the rule set used -- run :: HakyllConfiguration -> RulesM a -> IO RuleSet run configuration rules = do logger <- makeLogger putStrLn section logger "Initialising" store <- timed logger "Creating store" $ makeStore $ storeDirectory configuration provider <- timed logger "Creating provider" $ fileResourceProvider configuration -- Fetch the old graph from the store. If we don't find it, we consider this -- to be the first run graph <- storeGet store "Hakyll.Core.Run.run" "dependencies" let (firstRun, oldGraph) = case graph of Found g -> (False, g) _ -> (True, mempty) let ruleSet = runRules rules provider compilers = rulesCompilers ruleSet -- Extract the reader/state reader = unRuntime $ addNewCompilers compilers stateT = runReaderT reader $ RuntimeEnvironment { hakyllLogger = logger , hakyllConfiguration = configuration , hakyllRoutes = rulesRoutes ruleSet , hakyllResourceProvider = provider , hakyllStore = store , hakyllFirstRun = firstRun } -- Run the program and fetch the resulting state ((), state') <- runStateT stateT $ RuntimeState { hakyllAnalyzer = makeDependencyAnalyzer mempty (const False) oldGraph , hakyllCompilers = M.empty } -- We want to save the final dependency graph for the next run storeSet store "Hakyll.Core.Run.run" "dependencies" $ analyzerGraph $ hakyllAnalyzer state' -- Flush and return flushLogger logger return ruleSet data RuntimeEnvironment = RuntimeEnvironment { hakyllLogger :: Logger , hakyllConfiguration :: HakyllConfiguration , hakyllRoutes :: Routes , hakyllResourceProvider :: ResourceProvider , hakyllStore :: Store , hakyllFirstRun :: Bool } data RuntimeState = RuntimeState { hakyllAnalyzer :: DependencyAnalyzer (Identifier ()) , hakyllCompilers :: Map (Identifier ()) (Compiler () CompileRule) } newtype Runtime a = Runtime { unRuntime :: ReaderT RuntimeEnvironment (StateT RuntimeState IO) a } deriving (Functor, Applicative, Monad) -- | Add a number of compilers and continue using these compilers -- addNewCompilers :: [(Identifier (), Compiler () CompileRule)] -- ^ Compilers to add -> Runtime () addNewCompilers newCompilers = Runtime $ do -- Get some information logger <- hakyllLogger <$> ask section logger "Adding new compilers" provider <- hakyllResourceProvider <$> ask store <- hakyllStore <$> ask firstRun <- hakyllFirstRun <$> ask -- Old state information oldCompilers <- hakyllCompilers <$> get oldAnalyzer <- hakyllAnalyzer <$> get let -- All known compilers universe = M.keys oldCompilers ++ map fst newCompilers -- Create a new partial dependency graph dependencies = flip map newCompilers $ \(id', compiler) -> let deps = runCompilerDependencies compiler id' universe in (id', deps) -- Create the dependency graph newGraph = fromList dependencies -- Check which items have been modified modified <- fmap S.fromList $ flip filterM (map fst newCompilers) $ liftIO . resourceModified provider store . fromIdentifier let checkModified = if firstRun then const True else (`S.member` modified) -- Create a new analyzer and append it to the currect one let newAnalyzer = makeDependencyAnalyzer newGraph checkModified $ analyzerPreviousGraph oldAnalyzer analyzer = mappend oldAnalyzer newAnalyzer -- Update the state put $ RuntimeState { hakyllAnalyzer = analyzer , hakyllCompilers = M.union oldCompilers (M.fromList newCompilers) } -- Continue unRuntime stepAnalyzer stepAnalyzer :: Runtime () stepAnalyzer = Runtime $ do -- Step the analyzer state <- get let (signal, analyzer') = step $ hakyllAnalyzer state put $ state { hakyllAnalyzer = analyzer' } case signal of Done -> return () Cycle c -> unRuntime $ dumpCycle c Build id' -> unRuntime $ build id' -- | Dump cyclic error and quit -- dumpCycle :: [Identifier ()] -> Runtime () dumpCycle cycle' = Runtime $ do logger <- hakyllLogger <$> ask section logger "Dependency cycle detected! Conflict:" forM_ (zip cycle' $ drop 1 cycle') $ \(x, y) -> report logger $ show x ++ " -> " ++ show y build :: Identifier () -> Runtime () build id' = Runtime $ do logger <- hakyllLogger <$> ask routes <- hakyllRoutes <$> ask provider <- hakyllResourceProvider <$> ask store <- hakyllStore <$> ask compilers <- hakyllCompilers <$> get section logger $ "Compiling " ++ show id' -- Fetch the right compiler from the map let compiler = compilers M.! id' -- Check if the resource was modified isModified <- liftIO $ resourceModified provider store $ fromIdentifier id' -- Run the compiler result <- timed logger "Total compile time" $ liftIO $ runCompiler compiler id' provider (M.keys compilers) routes store isModified logger case result of -- Compile rule for one item, easy stuff Right (CompileRule compiled) -> do case runRoutes routes id' of Nothing -> return () Just url -> timed logger ("Routing to " ++ url) $ do destination <- destinationDirectory . hakyllConfiguration <$> ask let path = destination url liftIO $ makeDirectories path liftIO $ write path compiled -- Continue for the remaining compilers unRuntime stepAnalyzer -- Metacompiler, slightly more complicated Right (MetaCompileRule newCompilers) -> -- Actually I was just kidding, it's not hard at all unRuntime $ addNewCompilers newCompilers -- Some error happened, log and continue Left err -> do thrown logger err unRuntime stepAnalyzer