packages feed

hakyll 4.16.0.0 → 4.16.1.0

raw patch · 10 files changed

+534/−242 lines, 10 filesdep −lifted-asyncPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies removed: lifted-async

API changes (from Hackage documentation)

- Hakyll.Core.Runtime: instance GHC.Base.Monoid Hakyll.Core.Runtime.Progress
- Hakyll.Core.Runtime: instance GHC.Base.Semigroup Hakyll.Core.Runtime.Progress
- Hakyll.Core.Runtime: instance GHC.Classes.Eq Hakyll.Core.Runtime.Progress
+ Hakyll.Core.Logger: newInMem :: IO (Logger, IO [(Verbosity, String)])
+ Hakyll.Web.Feed: instance GHC.Classes.Eq Hakyll.Web.Feed.FeedType
+ Hakyll.Web.Feed: instance GHC.Show.Show Hakyll.Web.Feed.FeedType
+ Hakyll.Web.Feed: renderJson :: FeedConfiguration -> Context String -> [Item String] -> Compiler (Item String)
+ Hakyll.Web.Feed: renderJsonWithTemplates :: Template -> Template -> FeedConfiguration -> Context String -> [Item String] -> Compiler (Item String)
+ Hakyll.Web.Template.Context: mapContextBy :: (String -> Bool) -> (String -> String) -> Context a -> Context a
- Hakyll.Core.Logger: flush :: Logger -> IO ()
+ Hakyll.Core.Logger: flush :: Logger -> forall m. MonadIO m => m ()

Files

CHANGELOG.md view
@@ -4,6 +4,14 @@  # Releases +## Hakyll 4.16.1.0 (2023-08-23)++- Rewrite async scheduler; improves scaling and resource usage+- Add JSON Feed support (contribution by Berk Özkütük)+- Bump `aeson` upper bound to aeson 2.3 (contribution by Alexander Batischev)+- Bump `optparse-applicative` upper bound to aeson 0.19 (contribution by+    Alexander Batischev)+ ## Hakyll 4.16.0.0 (2023-04-27)  - Bump `base` *lower* bound to 4.12 (GHC >= 8.6). Hakyll already failed to build
+ data/templates/feed-item.json view
@@ -0,0 +1,8 @@+{+    "id": "$root$$url$",+    "url": "$root$$url$",+    "content_html": "$description$",+    "title": "$title$",+    "date_published": "$published$",+    "date_modified": "$updated$"+}
+ data/templates/feed.json view
@@ -0,0 +1,17 @@+{+    "version": "https://www.jsonfeed.org/version/1.1",+    "title": "$title$",+    "home_page_url": "$root$",+    "feed_url": "$root$$url$",+    $if(authorName)$+    "authors": [+        {+            "name": "$authorName$"+            $if(authorEmail)$+            , "url": "mailto:$authorEmail$"+            $endif$+        }+    ],+    $endif$+    "items": [ $body$ ]+}
hakyll.cabal view
@@ -1,5 +1,5 @@ Name:    hakyll-Version: 4.16.0.0+Version: 4.16.1.0  Synopsis: A static website compiler library Description:@@ -92,6 +92,8 @@   data/templates/atom.xml   data/templates/rss-item.xml   data/templates/rss.xml+  data/templates/feed.json+  data/templates/feed-item.json  Source-Repository head   Type:     git@@ -179,7 +181,7 @@     Paths_hakyll    Build-Depends:-    aeson                >= 1.0      && < 1.6 || >= 2.0 && < 2.2,+    aeson                >= 1.0      && < 1.6 || >= 2.0 && < 2.3,     base                 >= 4.12     && < 5,     binary               >= 0.5      && < 0.10,     blaze-html           >= 0.5      && < 0.10,@@ -192,11 +194,10 @@     file-embed           >= 0.0.10.1 && < 0.0.16,     filepath             >= 1.0      && < 1.5,     hashable             >= 1.0      && < 2,-    lifted-async         >= 0.10     && < 1,     lrucache             >= 1.1.1    && < 1.3,     mtl                  >= 1        && < 2.4,     network-uri          >= 2.6      && < 2.7,-    optparse-applicative >= 0.12     && < 0.18,+    optparse-applicative >= 0.12     && < 0.19,     parsec               >= 3.0      && < 3.2,     process              >= 1.6      && < 1.7,     random               >= 1.0      && < 1.3,@@ -286,7 +287,7 @@     tasty-hunit                >= 0.9  && < 0.11,     tasty-quickcheck           >= 0.8  && < 0.11,     -- Copy pasted from hakyll dependencies:-    aeson                >= 1.0      && < 1.6 || >= 2.0 && < 2.2,+    aeson                >= 1.0      && < 1.6 || >= 2.0 && < 2.3,     base                 >= 4.12     && < 5,     bytestring           >= 0.9      && < 0.12,     containers           >= 0.3      && < 0.7,
lib/Hakyll/Core/Logger.hs view
@@ -1,5 +1,6 @@ -------------------------------------------------------------------------------- -- | Produce pretty, thread-safe logs+{-# LANGUAGE Rank2Types #-} module Hakyll.Core.Logger     ( Verbosity (..)     , Logger@@ -9,15 +10,19 @@     , header     , message     , debug++    -- * Testing utilities+    , newInMem     ) where   -------------------------------------------------------------------------------- import           Control.Concurrent      (forkIO)-import           Control.Concurrent.Chan (Chan, newChan, readChan, writeChan)-import           Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar)-import           Control.Monad           (forever)+import           Control.Concurrent.Chan (newChan, readChan, writeChan)+import           Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)+import           Control.Monad           (forever, when) import           Control.Monad.Trans     (MonadIO, liftIO)+import qualified Data.IORef              as IORef import           Data.List               (intercalate) import           Prelude                 hiding (error) @@ -31,12 +36,10 @@   ----------------------------------------------------------------------------------- | Logger structure. Very complicated. data Logger = Logger-    { loggerChan      :: Chan (Maybe String)  -- ^ Nothing marks the end-    , loggerSync      :: MVar ()              -- ^ Used for sync on quit-    , loggerSink      :: String -> IO ()      -- ^ Out sink-    , loggerVerbosity :: Verbosity            -- ^ Verbosity+    { -- | Flush the logger (blocks until flushed)+      flush  :: forall m. MonadIO m => m ()+    , string :: forall m. MonadIO m => Verbosity -> String -> m ()     }  @@ -44,38 +47,23 @@ -- | Create a new logger new :: Verbosity -> IO Logger new vbty = do-    logger <- Logger <$>-        newChan <*> newEmptyMVar <*> pure putStrLn <*> pure vbty-    _      <- forkIO $ loggerThread logger-    return logger-  where-    loggerThread logger = forever $ do-        msg <- readChan $ loggerChan logger+    chan <- newChan+    sync <- newEmptyMVar+    _    <- forkIO $ forever $ do+        msg <- readChan chan         case msg of             -- Stop: sync-            Nothing -> putMVar (loggerSync logger) ()+            Nothing -> putMVar sync ()             -- Print and continue-            Just m  -> loggerSink logger m-------------------------------------------------------------------------------------- | Flush the logger (blocks until flushed)-flush :: Logger -> IO ()-flush logger = do-    writeChan (loggerChan logger) Nothing-    () <- takeMVar $ loggerSync logger-    return ()------------------------------------------------------------------------------------string :: MonadIO m-       => Logger     -- ^ Logger-       -> Verbosity  -- ^ Verbosity of the string-       -> String     -- ^ Section name-       -> m ()       -- ^ No result-string l v m-    | loggerVerbosity l >= v = liftIO $ writeChan (loggerChan l) (Just m)-    | otherwise              = return ()+            Just m  -> putStrLn m+    return $ Logger+        { flush = liftIO $ do+            writeChan chan Nothing+            () <- takeMVar sync+            return ()+        , string = \v m -> when (vbty >= v) $+            liftIO $ writeChan chan (Just m)+        }   --------------------------------------------------------------------------------@@ -101,3 +89,19 @@ -------------------------------------------------------------------------------- indent :: String -> String indent = intercalate "\n    " . lines+++--------------------------------------------------------------------------------+-- | Create a new logger that just stores all the messages, useful for writing+-- tests.+newInMem :: IO (Logger, IO [(Verbosity, String)])+newInMem = do+    ref <- IORef.newIORef []+    pure+        ( Logger+            { string = \vbty msg -> liftIO $ IORef.atomicModifyIORef' ref $+                \msgs -> ((vbty, msg) : msgs, ())+            , flush  = pure ()+            }+        , reverse <$> IORef.readIORef ref+        )
lib/Hakyll/Core/Runtime.hs view
@@ -1,4 +1,5 @@ --------------------------------------------------------------------------------+{-# LANGUAGE RecordWildCards #-} module Hakyll.Core.Runtime     ( run     , RunMode(..)@@ -6,21 +7,26 @@   ---------------------------------------------------------------------------------import           Control.Concurrent.Async.Lifted (forConcurrently)-import           Control.Concurrent.MVar         (modifyMVar_, readMVar, newMVar, MVar)-import           Control.Monad                   (join, unless, when)-import           Control.Monad.Except            (ExceptT, runExceptT, throwError)-import           Control.Monad.Reader            (ReaderT, ask, runReaderT)-import           Control.Monad.Trans             (liftIO)-import           Data.Foldable                   (traverse_)-import           Data.List                       (intercalate)-import           Data.Map                        (Map)-import qualified Data.Map                        as M-import           Data.Set                        (Set)-import qualified Data.Set                        as S-import           Data.Traversable                (for)-import           System.Exit                     (ExitCode (..))-import           System.FilePath                 ((</>))+import           Control.Concurrent            (forkIO, getNumCapabilities,+                                                rtsSupportsBoundThreads)+import qualified Control.Concurrent.MVar       as MVar+import           Control.Monad                 (replicateM_, unless, void)+import           Control.Monad.Reader          (ReaderT, ask, runReaderT)+import           Control.Monad.Trans           (liftIO)+import           Data.Foldable                 (for_, traverse_)+import qualified Data.Graph                    as Graph+import           Data.IORef                    (IORef)+import qualified Data.IORef                    as IORef+import           Data.List                     (foldl', intercalate)+import           Data.Map                      (Map)+import qualified Data.Map                      as Map+import           Data.Maybe                    (fromMaybe)+import           Data.Sequence                 (Seq)+import qualified Data.Sequence                 as Seq+import           Data.Set                      (Set)+import qualified Data.Set                      as Set+import           System.Exit                   (ExitCode (..))+import           System.FilePath               ((</>))   --------------------------------------------------------------------------------@@ -70,40 +76,34 @@     let (oldFacts) = case mOldFacts of Store.Found f -> f                                        _             -> mempty -    state <- newMVar $ RuntimeState-            { runtimeDone         = S.empty-            , runtimeSnapshots    = S.empty-            , runtimeTodo         = M.empty-            , runtimeFacts        = oldFacts-            , runtimeDependencies = M.empty-            }-     -- Build runtime read/state+    scheduler <- IORef.newIORef $ emptyScheduler {schedulerFacts = oldFacts}     let compilers = rulesCompilers ruleSet         read'     = RuntimeRead             { runtimeConfiguration = config             , runtimeLogger        = logger             , runtimeProvider      = provider-            , runtimeState         = state             , runtimeStore         = store             , runtimeRoutes        = rulesRoutes ruleSet-            , runtimeUniverse      = M.fromList compilers+            , runtimeUniverse      = Map.fromList compilers+            , runtimeScheduler     = scheduler             }      -- Run the program and fetch the resulting state-    result <- runExceptT $ runReaderT (build mode) read'-    case result of-        Left e          -> do-            Logger.error logger e-            Logger.flush logger-            return (ExitFailure 1, ruleSet)--        Right _ -> do-            Logger.debug logger "Removing tmp directory..."-            removeDirectory $ tmpDirectory config+    runReaderT (build mode) read'+    errors <- schedulerErrors <$> IORef.readIORef scheduler+    if null errors then do+        Logger.debug logger "Removing tmp directory..."+        removeDirectory $ tmpDirectory config -            Logger.flush logger-            return (ExitSuccess, ruleSet)+        Logger.flush logger+        return (ExitSuccess, ruleSet)+    else do+        for_ errors $ \(mbId, err) -> Logger.error logger $ case mbId of+            Just identifier -> show identifier <> ": " <> err+            Nothing         -> err+        Logger.flush logger+        return (ExitFailure 1, ruleSet)   --------------------------------------------------------------------------------@@ -111,163 +111,352 @@     { runtimeConfiguration :: Configuration     , runtimeLogger        :: Logger     , runtimeProvider      :: Provider-    , runtimeState         :: MVar RuntimeState     , runtimeStore         :: Store     , runtimeRoutes        :: Routes     , runtimeUniverse      :: Map Identifier (Compiler SomeItem)+    , runtimeScheduler     :: IORef Scheduler     }   ---------------------------------------------------------------------------------data RuntimeState = RuntimeState-    { runtimeDone         :: Set Identifier-    , runtimeSnapshots    :: Set (Identifier, Snapshot)-    , runtimeTodo         :: Map Identifier (Compiler SomeItem)-    , runtimeFacts        :: DependencyFacts-    , runtimeDependencies :: Map Identifier (Set (Identifier, Snapshot))+-- | A Scheduler is a pure representation of work going on, works that needs+-- to be done, and work already done.  Workers can obtain things to do+-- by interacting with the Scheduler, and execute them synchronously or+-- asynchronously.+--+-- All operations on Scheduler look like 'Scheduler -> (Scheduler, a)' and+-- should be used with atomicModifyIORef'.+data Scheduler = Scheduler+    { -- | Items to work on next.  Identifiers may appear multiple times.+      schedulerQueue     :: !(Seq Identifier)+    , -- | Items that we haven't started yet.+      schedulerTodo      :: !(Map Identifier (Compiler SomeItem))+    , -- | Currently processing+      schedulerWorking   :: !(Set Identifier)+    , -- | Finished+      schedulerDone      :: !(Set Identifier)+    , -- | Any snapshots stored.+      schedulerSnapshots :: !(Set (Identifier, Snapshot))+    , -- | Currently blocked compilers.+      schedulerBlocked   :: !(Set Identifier)+    , -- | Compilers that may resume on triggers+      schedulerTriggers  :: !(Map Identifier (Set Identifier))+    , -- | Number of starved pops; tracking this allows us to start a new+      -- number of threads again later.+      schedulerStarved   :: !Int+    , -- | Dynamic dependency info.+      schedulerFacts     :: !DependencyFacts+    , -- | Errors encountered.+      schedulerErrors    :: ![(Maybe Identifier, String)]     }   ---------------------------------------------------------------------------------type Runtime a = ReaderT RuntimeRead (ExceptT String IO) a+emptyScheduler :: Scheduler+emptyScheduler = Scheduler {..}+  where+    schedulerTodo      = Map.empty+    schedulerDone      = Set.empty+    schedulerQueue     = Seq.empty+    schedulerWorking   = Set.empty+    schedulerSnapshots = Set.empty+    schedulerBlocked   = Set.empty+    schedulerTriggers  = Map.empty+    schedulerStarved   = 0+    schedulerFacts     = Map.empty+    schedulerErrors    = []   ----------------------------------------------------------------------------------- Because compilation of rules often revolves around IO,--- be very careful when modifying the state-modifyRuntimeState :: (RuntimeState -> RuntimeState) -> Runtime ()-modifyRuntimeState f = liftIO . flip modifyMVar_ (pure . f) . runtimeState =<< ask+schedulerError :: Maybe Identifier -> String -> Scheduler -> (Scheduler, ())+schedulerError i e s = (s {schedulerErrors = (i, e) : schedulerErrors s}, ())   ---------------------------------------------------------------------------------getRuntimeState :: Runtime RuntimeState-getRuntimeState = liftIO . readMVar . runtimeState =<< ask+schedulerMarkOutOfDate+    :: Map Identifier (Compiler SomeItem)+    -> Set Identifier+    -> Scheduler+    -> (Scheduler, [String])+schedulerMarkOutOfDate universe modified scheduler@Scheduler {..} =+    ( scheduler+        { schedulerQueue = schedulerQueue <> Seq.fromList (Map.keys todo)+        , schedulerDone  = schedulerDone <>+            (Map.keysSet universe `Set.difference` ood)+        , schedulerTodo  = schedulerTodo <> todo+        , schedulerFacts = facts'+        }+    , msgs+    )+  where+    (ood, facts', msgs) = outOfDate (Map.keys universe) modified schedulerFacts+    todo = Map.filterWithKey (\id' _ -> id' `Set.member` ood) universe   ---------------------------------------------------------------------------------build :: RunMode -> Runtime ()+data SchedulerStep+    -- | The scheduler instructs to offer some work on the given item.  It+    -- also returns the number of threads that can be resumed after they have+    -- starved.+    = SchedulerWork Identifier (Compiler SomeItem) Int+    -- | There's currently no work available, but there will be after other+    -- threads have finished whatever they are doing.+    | SchedulerStarve+    -- | We've finished all work.+    | SchedulerFinish+    -- | An error occurred.  You can retrieve the errors from 'schedulerErrors'.+    | SchedulerError+++--------------------------------------------------------------------------------+schedulerPop :: Scheduler -> (Scheduler, SchedulerStep)+schedulerPop scheduler@Scheduler {..} = case Seq.viewl schedulerQueue of+    Seq.EmptyL+        | not $ Set.null schedulerWorking ->+            ( scheduler {schedulerStarved = schedulerStarved + 1}+            , SchedulerStarve+            )+        | not $ Set.null schedulerBlocked ->+            let cycles = schedulerCycles scheduler+                msg | null cycles = "Possible dependency cycle in: " <>+                        intercalate ", " (show <$> Set.toList schedulerBlocked)+                    | otherwise = "Dependency cycles: " <>+                        intercalate "; "+                            (map (intercalate " -> " . map show) cycles) in+            SchedulerError <$ schedulerError Nothing msg scheduler+        | otherwise -> (scheduler, SchedulerFinish)+    x Seq.:< xs+        | x `Set.member` schedulerDone ->+            schedulerPop scheduler {schedulerQueue = xs}+        | x `Set.member` schedulerWorking ->+            schedulerPop scheduler {schedulerQueue = xs}+        | x `Set.member` schedulerBlocked ->+            schedulerPop scheduler {schedulerQueue = xs}+        | otherwise -> case Map.lookup x schedulerTodo of+            Nothing -> SchedulerError <$+                schedulerError (Just x) "Compiler not found" scheduler+            Just c  ->+                ( scheduler+                    { schedulerQueue   = xs+                    , schedulerWorking = Set.insert x schedulerWorking+                    }+                , SchedulerWork x c 0+                )+++--------------------------------------------------------------------------------+schedulerCycles :: Scheduler -> [[Identifier]]+schedulerCycles Scheduler {..} =+    [c | Graph.CyclicSCC c <- Graph.stronglyConnComp graph]+  where+    graph = [(x, x, Set.toList ys) | (x, ys) <- Map.toList edges]+    edges = Map.fromListWith Set.union $ do+        (dep, xs) <- Map.toList $ schedulerTriggers+        x <- Set.toList xs+        pure (x, Set.singleton dep)+++--------------------------------------------------------------------------------+schedulerBlock+    :: Identifier+    -> [(Identifier, Snapshot)]+    -> Compiler SomeItem+    -> Scheduler+    -> (Scheduler, SchedulerStep)+schedulerBlock identifier deps0 compiler scheduler@Scheduler {..}+    | null deps1 = (scheduler, SchedulerWork identifier compiler 0)+    | otherwise  = schedulerPop $ scheduler+         { schedulerQueue    =+             -- Optimization: move deps to the front and item to the back+             Seq.fromList depIds <>+             schedulerQueue <>+             Seq.singleton identifier+         , schedulerTodo     =+             Map.insert identifier+                 (Compiler $ \_ -> pure $ CompilerRequire deps0 compiler)+                 schedulerTodo+         , schedulerWorking  = Set.delete identifier schedulerWorking+         , schedulerBlocked  = Set.insert identifier schedulerBlocked+         , schedulerTriggers = foldl'+             (\acc (depId, _) ->+                 Map.insertWith Set.union depId (Set.singleton identifier) acc)+             schedulerTriggers+             deps1+         }+  where+    deps1  = filter (not . done) deps0+    depIds = map fst deps1++    -- Done if we either completed the entire item (runtimeDone) or+    -- if we previously saved the snapshot (runtimeSnapshots).+    done (depId, depSnapshot) =+        depId `Set.member` schedulerDone ||+        (depId, depSnapshot) `Set.member` schedulerSnapshots+++--------------------------------------------------------------------------------+schedulerUnblock :: Identifier -> Scheduler -> (Scheduler, Int)+schedulerUnblock identifier scheduler@Scheduler {..} =+    ( scheduler+        { schedulerQueue    =+            schedulerQueue <> Seq.fromList (Set.toList triggered)+        , schedulerStarved  = 0+        , schedulerBlocked  = Set.delete identifier $+            schedulerBlocked `Set.difference` triggered+        , schedulerTriggers = Map.delete identifier schedulerTriggers+        }+    , schedulerStarved+    )+  where+    triggered = fromMaybe Set.empty $ Map.lookup identifier schedulerTriggers+++--------------------------------------------------------------------------------+schedulerSnapshot+    :: Identifier -> Snapshot -> Compiler SomeItem+    -> Scheduler -> (Scheduler, SchedulerStep)+schedulerSnapshot identifier snapshot compiler scheduler@Scheduler {..} =+    let (scheduler', resume) = schedulerUnblock identifier scheduler+            { schedulerSnapshots =+                Set.insert (identifier, snapshot) schedulerSnapshots+            } in+    (scheduler', SchedulerWork identifier compiler resume)+++--------------------------------------------------------------------------------+schedulerWrite+    :: Identifier+    -> [Dependency]+    -> Scheduler+    -> (Scheduler, SchedulerStep)+schedulerWrite identifier depFacts scheduler0@Scheduler {..} =+    let (scheduler1, resume) = schedulerUnblock identifier scheduler0+            { schedulerWorking = Set.delete identifier schedulerWorking+            , schedulerFacts   = Map.insert identifier depFacts schedulerFacts+            , schedulerDone    =+                Set.insert identifier schedulerDone+            , schedulerTodo    =+                Map.delete identifier schedulerTodo+            }+        (scheduler2, step) = schedulerPop scheduler1 in+    case step of+        SchedulerWork i c n -> (scheduler2, SchedulerWork i c (n + resume))+        _                   -> (scheduler2, step)+++--------------------------------------------------------------------------------+build :: RunMode -> ReaderT RuntimeRead IO () build mode = do     logger <- runtimeLogger <$> ask     Logger.header logger "Checking for out-of-date items"+    schedulerRef <- runtimeScheduler <$> ask     scheduleOutOfDate     case mode of         RunModeNormal -> do             Logger.header logger "Compiling"-            pickAndChase+            if rtsSupportsBoundThreads then pickAndChaseAsync else pickAndChase             Logger.header logger "Success"-            facts <- runtimeFacts <$> getRuntimeState+            facts <- liftIO $ schedulerFacts <$> IORef.readIORef schedulerRef             store <- runtimeStore <$> ask             liftIO $ Store.set store factsKey facts         RunModePrintOutOfDate -> do             Logger.header logger "Out of date items:"-            todo <- runtimeTodo <$> getRuntimeState-            traverse_ (Logger.message logger . show) (M.keys todo)+            todo <- liftIO $ schedulerTodo <$> IORef.readIORef schedulerRef+            traverse_ (Logger.message logger . show) (Map.keys todo)   ---------------------------------------------------------------------------------scheduleOutOfDate :: Runtime ()+scheduleOutOfDate :: ReaderT RuntimeRead IO () scheduleOutOfDate = do-    logger   <- runtimeLogger   <$> ask-    provider <- runtimeProvider <$> ask-    universe <- runtimeUniverse <$> ask--    let identifiers = M.keys universe-        modified    = S.filter (resourceModified provider) (M.keysSet universe)--    state <- getRuntimeState-    let facts = runtimeFacts state-        todo  = runtimeTodo state-        done  = runtimeDone state--    let (ood, facts', msgs) = outOfDate identifiers modified facts-        todo'               = M.filterWithKey (\id' _ -> id' `S.member` ood) universe-        done'               = done `S.union` (M.keysSet universe `S.difference` ood)+    logger       <- runtimeLogger    <$> ask+    provider     <- runtimeProvider  <$> ask+    universe     <- runtimeUniverse  <$> ask+    schedulerRef <- runtimeScheduler <$> ask+    let modified  = Set.filter (resourceModified provider) (Map.keysSet universe)+    msgs <- liftIO . IORef.atomicModifyIORef' schedulerRef $+        schedulerMarkOutOfDate universe modified      -- Print messages     mapM_ (Logger.debug logger) msgs -    -- Update facts and todo items-    modifyRuntimeState $ \s -> s-        { runtimeDone  = done'-        , runtimeTodo  = todo `M.union` todo'-        , runtimeFacts = facts'-        } - ---------------------------------------------------------------------------------pickAndChase :: Runtime ()+pickAndChase :: ReaderT RuntimeRead IO () pickAndChase = do-    todo <- runtimeTodo <$> getRuntimeState-    unless (null todo) $ do-        acted <- mconcat <$> forConcurrently (M.keys todo) chase-        when (acted == Idled) $ do-            -- This clause happens when chasing *every item* in `todo` resulted in -            -- idling because tasks are all waiting on something: a dependency cycle  -            deps <- runtimeDependencies <$> getRuntimeState-            throwError $ "Hakyll.Core.Runtime.pickAndChase: Dependency cycle detected: " ++ -                intercalate ", " [show k ++ " depends on " ++ show (S.toList v) | (k, v) <- M.toList deps]-        pickAndChase+    scheduler <- runtimeScheduler <$> ask+    let go SchedulerFinish       = pure ()+        go SchedulerError        = pure ()+        go (SchedulerWork i c _) = work i c >>= go+        go SchedulerStarve       =+            liftIO . IORef.atomicModifyIORef' scheduler $+            schedulerError Nothing "Starved, possible dependency cycle?"+    pop <- liftIO . IORef.atomicModifyIORef' scheduler $ schedulerPop+    go pop   ----------------------------------------------------------------------------------- | Tracks whether a set of tasks has progressed overall (at least one task progressed)--- or has idled-data Progress = Progressed | Idled deriving (Eq)+pickAndChaseAsync :: ReaderT RuntimeRead IO ()+pickAndChaseAsync = do+    runtimeRead <- ask+    numThreads  <- liftIO getNumCapabilities+    let scheduler = runtimeScheduler runtimeRead+    Logger.message (runtimeLogger runtimeRead) $+        "Using async runtime with " <> show numThreads <> " threads..."+    liftIO $ do+        signal     <- MVar.newEmptyMVar -instance Semigroup Progress where-    Idled      <> Idled      = Idled-    Progressed <> _          = Progressed-    _          <> Progressed = Progressed+        let spawnN :: Int -> IO ()+            spawnN n = replicateM_ n $ forkIO $+                IORef.atomicModifyIORef' scheduler schedulerPop >>= go -instance Monoid Progress where-    mempty = Idled+            go :: SchedulerStep -> IO ()+            go step = case step of+                SchedulerFinish       -> void $ MVar.tryPutMVar signal ()+                SchedulerStarve       -> pure ()+                SchedulerError        -> void $ MVar.tryPutMVar signal ()+                (SchedulerWork i c n) -> do+                    spawnN n+                    step' <- runReaderT (work i c) runtimeRead+                    go step' +        spawnN numThreads+        MVar.readMVar signal + ---------------------------------------------------------------------------------chase :: Identifier -> Runtime Progress-chase id' = do+work :: Identifier -> Compiler SomeItem -> ReaderT RuntimeRead IO SchedulerStep+work id' compiler = do     logger    <- runtimeLogger        <$> ask     provider  <- runtimeProvider      <$> ask     universe  <- runtimeUniverse      <$> ask     routes    <- runtimeRoutes        <$> ask     store     <- runtimeStore         <$> ask     config    <- runtimeConfiguration <$> ask--    state     <- getRuntimeState--    Logger.debug logger $ "Processing " ++ show id'+    scheduler <- runtimeScheduler     <$> ask -    let compiler = (runtimeTodo state) M.! id'-        read' = CompilerRead+    let cread = CompilerRead             { compilerConfig     = config             , compilerUnderlying = id'             , compilerProvider   = provider-            , compilerUniverse   = M.keysSet universe+            , compilerUniverse   = Map.keysSet universe             , compilerRoutes     = routes             , compilerStore      = store             , compilerLogger     = logger             }--    result <- liftIO $ runCompiler compiler read'+    result <- liftIO $ runCompiler compiler cread     case result of-        -- Rethrow error-        CompilerError e -> throwError $ case compilerErrorMessages e of-            [] -> "Compiler failed but no info given, try running with -v?"-            es -> intercalate "; " es--        -- Signal that a snapshot was saved ->-        CompilerSnapshot snapshot c -> do-            -- Update info. The next 'chase' will pick us again at some-            -- point so we can continue then.-            modifyRuntimeState $ \s -> s-                { runtimeSnapshots = S.insert (id', snapshot) (runtimeSnapshots s)-                , runtimeTodo      = M.insert id' c (runtimeTodo s)-                }--            return Progressed+        CompilerError e -> do+            let msgs = case compilerErrorMessages e of+                    [] -> ["Compiler failed but no info given, try running with -v?"]+                    es -> es+            for_ msgs $ \msg -> liftIO . IORef.atomicModifyIORef' scheduler $+                schedulerError (Just id') msg+            return SchedulerError +        CompilerSnapshot snapshot c ->+            liftIO . IORef.atomicModifyIORef' scheduler $+            schedulerSnapshot id' snapshot c -        -- Huge success         CompilerDone (SomeItem item) cwrite -> do             -- Print some info             let facts = compilerDependencies cwrite@@ -277,11 +466,13 @@             Logger.message logger $ cacheHits ++ " " ++ show id'              -- Sanity check-            unless (itemIdentifier item == id') $ throwError $-                "The compiler yielded an Item with Identifier " ++-                show (itemIdentifier item) ++ ", but we were expecting " ++-                "an Item with Identifier " ++ show id' ++ " " ++-                "(you probably want to call makeItem to solve this problem)"+            liftIO . unless (itemIdentifier item == id') $+                IORef.atomicModifyIORef' scheduler $ schedulerError+                    (Just id') $+                    "The compiler yielded an Item with Identifier " +++                    show (itemIdentifier item) ++ ", but we were expecting " +++                    "an Item with Identifier " ++ show id' ++ " " +++                    "(you probably want to call makeItem to solve this problem)"              -- Write if necessary             (mroute, _) <- liftIO $ runRoutes routes provider id'@@ -293,51 +484,10 @@                     liftIO $ write path item                     Logger.debug logger $ "Routed to " ++ path -            -- Save! (For load)             liftIO $ save store item--            modifyRuntimeState $ \s -> s-                { runtimeDone         = S.insert id' (runtimeDone s)-                , runtimeTodo         = M.delete id' (runtimeTodo s)-                , runtimeFacts        = M.insert id' facts (runtimeFacts s)-                , runtimeDependencies = M.delete id' (runtimeDependencies s)-                }-            -            return Progressed--        -- Try something else first-        CompilerRequire reqs c -> do-            let done      = runtimeDone state-                snapshots = runtimeSnapshots state--            deps <- fmap join . for reqs $ \(depId, depSnapshot) -> do-                Logger.debug logger $-                    "Compiler requirement found for: " ++ show id' ++-                    ": " ++ show depId ++ " (snapshot " ++ depSnapshot ++ ")"--                -- Done if we either completed the entire item (runtimeDone) or-                -- if we previously saved the snapshot (runtimeSnapshots).-                let depDone =-                        depId `S.member` done ||-                        (depId, depSnapshot) `S.member` snapshots-                    actualDep = [(depId, depSnapshot) | not depDone]--                return actualDep  --            modifyRuntimeState $ \s -> s-                { runtimeTodo         = M.insert id'-                    (if null deps then c else compilerResult result)-                    (runtimeTodo s)-                 -- We track dependencies only to inform users when an infinite loop is detected-                , runtimeDependencies = M.insertWith S.union id' (S.fromList deps) (runtimeDependencies s)-                }--            -- Progress has been made if at least one of the -            -- requirements can move forwards at the next pass-            -- In some cases, dependencies have been processed in parallel in which case `deps` -            -- can be empty, and we can progress to the next stage. See issue #907-            let progress | null deps    = Progressed-                         | deps == reqs = Idled-                         | otherwise    = Progressed+            liftIO . IORef.atomicModifyIORef' scheduler $+                schedulerWrite id' facts -            return progress+        CompilerRequire reqs c ->+            liftIO . IORef.atomicModifyIORef' scheduler $+            schedulerBlock id' reqs c
lib/Hakyll/Web/Feed.hs view
@@ -23,8 +23,10 @@     ( FeedConfiguration (..)     , renderRss     , renderAtom+    , renderJson     , renderRssWithTemplates     , renderAtomWithTemplates+    , renderJsonWithTemplates     ) where  @@ -40,6 +42,7 @@ -------------------------------------------------------------------------------- import           Data.FileEmbed              (makeRelativeToProject) import           System.FilePath             ((</>))+import Text.Printf (printf)   --------------------------------------------------------------------------------@@ -63,7 +66,17 @@     $(makeRelativeToProject ("data" </> "templates" </> "atom-item.xml")         >>= embedTemplate) +jsonTemplate :: Template+jsonTemplate =+    $(makeRelativeToProject ("data" </> "templates" </> "feed.json")+        >>= embedTemplate) +jsonItemTemplate :: Template+jsonItemTemplate =+    $(makeRelativeToProject ("data" </> "templates" </> "feed-item.json")+        >>= embedTemplate)++ -------------------------------------------------------------------------------- -- | This is a data structure to keep the configuration of a feed. data FeedConfiguration = FeedConfiguration@@ -82,16 +95,30 @@   --------------------------------------------------------------------------------+-- | Different types a feed can have.+data FeedType = XmlFeed | JsonFeed+  deriving (Show, Eq)+++-------------------------------------------------------------------------------- -- | Abstract function to render any feed.-renderFeed :: Template                -- ^ Default feed template+renderFeed :: FeedType                -- ^ Feed type+           -> Template                -- ^ Default feed template            -> Template                -- ^ Default item template            -> FeedConfiguration       -- ^ Feed configuration            -> Context String          -- ^ Context for the items            -> [Item String]           -- ^ Input items            -> Compiler (Item String)  -- ^ Resulting item-renderFeed feedTpl itemTpl config itemContext items = do-    protectedItems <- mapM (applyFilter protectCDATA) items-    body <- makeItem =<< applyTemplateList itemTpl itemContext' protectedItems+renderFeed feedType feedTpl itemTpl config itemContext items = do+    protectedItems <-+      case feedType of+        XmlFeed  -> mapM (applyFilter protectCDATA) items+        JsonFeed -> pure items+    let itemDelim = case feedType of+          XmlFeed  -> ""+          JsonFeed -> ", "++    body <- makeItem =<< applyJoinTemplateList itemDelim itemTpl itemContext' protectedItems     applyTemplate feedTpl feedContext body   where     applyFilter :: (Monad m,Functor f) => (String -> String) -> f String -> m (f String)@@ -100,7 +127,7 @@     protectCDATA = replaceAll "]]>" (const "]]&gt;")      itemContext' = mconcat-        [ itemContext+        [ escapeDescription itemContext         , constField "root" (feedRoot config)         , constField "authorName"  (feedAuthorName config)         , emailField@@ -130,6 +157,10 @@         ""    -> missingField         email -> constField "authorEmail" email +    escapeDescription = case feedType of+        XmlFeed  -> id+        JsonFeed -> mapContextBy (== "description") escapeString+ -------------------------------------------------------------------------------- -- | Render an RSS feed using given templates with a number of items. renderRssWithTemplates ::@@ -140,7 +171,7 @@     -> [Item String]           -- ^ Feed items     -> Compiler (Item String)  -- ^ Resulting feed renderRssWithTemplates feedTemplate itemTemplate config context = renderFeed-    feedTemplate itemTemplate config+    XmlFeed feedTemplate itemTemplate config     (makeItemContext "%a, %d %b %Y %H:%M:%S UT" context)  @@ -154,11 +185,25 @@     -> [Item String]           -- ^ Feed items     -> Compiler (Item String)  -- ^ Resulting feed renderAtomWithTemplates feedTemplate itemTemplate config context = renderFeed-    feedTemplate itemTemplate config+    XmlFeed feedTemplate itemTemplate config     (makeItemContext "%Y-%m-%dT%H:%M:%SZ" context)   --------------------------------------------------------------------------------+-- | Render a JSON feed using given templates with a number of items.+renderJsonWithTemplates ::+       Template                -- ^ Feed template+    -> Template                -- ^ Item template+    -> FeedConfiguration       -- ^ Feed configuration+    -> Context String          -- ^ Item context+    -> [Item String]           -- ^ Feed items+    -> Compiler (Item String)  -- ^ Resulting feed+renderJsonWithTemplates feedTemplate itemTemplate config context = renderFeed+    JsonFeed feedTemplate itemTemplate config+    (makeItemContext "%Y-%m-%dT%H:%M:%SZ" context)+++-------------------------------------------------------------------------------- -- | Render an RSS feed with a number of items. renderRss :: FeedConfiguration       -- ^ Feed configuration           -> Context String          -- ^ Item context@@ -177,7 +222,40 @@   --------------------------------------------------------------------------------+-- | Render a JSON feed with a number of items.+--+-- Items' bodies will be put into @content_html@ field of the resulting JSON;+-- the @content@ field will not be set.+renderJson :: FeedConfiguration       -- ^ Feed configuration+           -> Context String          -- ^ Item context+           -> [Item String]           -- ^ Feed items+           -> Compiler (Item String)  -- ^ Resulting feed+renderJson = renderJsonWithTemplates jsonTemplate jsonItemTemplate+++-------------------------------------------------------------------------------- -- | Copies @$updated$@ from @$published$@ if it is not already set. makeItemContext :: String -> Context a -> Context a makeItemContext fmt context = mconcat     [context, dateField "published" fmt, dateField "updated" fmt]+++--------------------------------------------------------------------------------+-- | Escape the string according to [RFC8259 §7](https://www.rfc-editor.org/rfc/rfc8259#section-7). In other words,+--   * quotation marks and backslashes are prefixed with a backslash+--   * control characters (i.e. 0x00 - 0x1F) are escaped s.t. their+--   hex representation are prefixed with "\u00" (e.g. 0x15 -> \u0015)+--   * the rest of the characters are untouched.+escapeString :: String -> String+escapeString = flip escapeString' ""+  where+    escapeString' :: String -> ShowS+    escapeString' [] s = s+    escapeString' ('"' : cs) s = showString "\\\"" (escapeString' cs s)+    escapeString' ('\\' : cs) s = showString "\\\\" (escapeString' cs s)+    escapeString' (c : cs) s+      | c < ' ' = escapeChar c (escapeString' cs s)+      | otherwise = showChar c (escapeString' cs s)++    escapeChar :: Char -> ShowS+    escapeChar = showString . printf "\\u%04X"
lib/Hakyll/Web/Template/Context.hs view
@@ -31,6 +31,7 @@     , listFieldWith     , functionField     , mapContext+    , mapContextBy      , defaultContext     , bodyField@@ -201,11 +202,26 @@ -- > constField "x" "ac" <> constField "y" "bc" -- mapContext :: (String -> String) -> Context a -> Context a-mapContext f (Context c) = Context $ \k a i -> do+mapContext = mapContextBy (const True)+++--------------------------------------------------------------------------------+-- | Transform the respective string results of all fields in a context+-- satisfying a predicate. For example,+--+-- > mapContextBy (=="y") (++"c") (constField "x" "a" <> constField "y" "b")+--+-- is equivalent to+--+-- > constField "x" "a" <> constField "y" "bc"+--+mapContextBy :: (String -> Bool) -> (String -> String) -> Context a -> Context a+mapContextBy p f (Context c) = Context $ \k a i -> do     fld <- c k a i     case fld of         EmptyField      -> wrongType "boolField"-        StringField str -> return $ StringField (f str)+        StringField str -> return $ StringField $+                             if p k then f str else str         _               -> wrongType "ListField"   where     wrongType typ = fail $ "Hakyll.Web.Template.Context.mapContext: " ++
tests/Hakyll/Core/Runtime/Tests.hs view
@@ -8,8 +8,9 @@ -------------------------------------------------------------------------------- import           Control.Monad       (void) import qualified Data.ByteString     as B-import           System.FilePath     ((</>))+import           Data.List           (isInfixOf) import           System.Exit         (ExitCode (..))+import           System.FilePath     ((</>)) import           Test.Tasty          (TestTree, testGroup) import           Test.Tasty.HUnit    (Assertion, (@?=)) @@ -102,7 +103,7 @@ -- Test that dependency cycles are correctly identified case03 :: Assertion case03 = do-    logger  <- Logger.new Logger.Error+    (logger, inMemLog) <- Logger.newInMem     (ec, _) <- run RunModeNormal testConfiguration logger $ do          create ["partial.html.out1"] $ do@@ -119,19 +120,23 @@                 makeItem example                     >>= loadAndApplyTemplate "partial.html" defaultContext -     ec @?= ExitFailure 1+    msgs <- inMemLog+    length+        [ msg+        | (Logger.Error, msg) <- msgs, "Dependency cycles:" `isInfixOf` msg+        ] @?= 1      cleanTestEnv   ----------------------------------------------------------------------------------- Test that dependency cycles are correctly identified when snapshots +-- Test that dependency cycles are correctly identified when snapshots -- are also involved. See issue #878. case04 :: Assertion case04 = do-    logger  <- Logger.new Logger.Error-    (ec, _) <- run RunModeNormal testConfiguration logger $ do+    (logger, inMemLog) <- Logger.newInMem+    (ec, _)            <- run RunModeNormal testConfiguration logger $ do          create ["partial.html.out1"] $ do             route idRoute@@ -148,16 +153,21 @@                     >>= loadAndApplyTemplate "partial.html" defaultContext      ec @?= ExitFailure 1+    msgs <- inMemLog+    length+        [ msg+        | (Logger.Error, msg) <- msgs, "Dependency cycles:" `isInfixOf` msg+        ] @?= 1      cleanTestEnv   ----------------------------------------------------------------------------------- Test that dependency cycles are correctly identified in the presence of +-- Test that dependency cycles are correctly identified in the presence of -- snapshots. See issue #878.-case05 :: Assertion +case05 :: Assertion case05 = do-    logger  <- Logger.new Logger.Error +    logger  <- Logger.new Logger.Error     (ec, _) <- run RunModeNormal testConfiguration logger $ do          match "posts/*" $ do@@ -185,18 +195,18 @@                  makeItem ""                     >>= loadAndApplyTemplate "template-empty.html" footerCtx-        +         create ["template-empty.html"] $ compile templateCompiler -    ec @?= ExitSuccess +    ec @?= ExitSuccess      cleanTestEnv   ----------------------------------------------------------------------------------- Test that dependency cycles are correctly identified in the presence of +-- Test that dependency cycles are correctly identified in the presence of -- snapshots. The test case below was presented as an example which invalidated--- a previous approach to dependency cycle checking. +-- a previous approach to dependency cycle checking. -- See https://github.com/jaspervdj/hakyll/pull/880#discussion_r708650172 case06 :: Assertion case06 = do
tests/TestSuite/Util.hs view
@@ -38,7 +38,7 @@                -> [Assertion]  -- ^ Cases                -> [TestTree]   -- ^ Result tests fromAssertions name =-    zipWith testCase [printf "[%2d] %s" n name | n <- [1 :: Int ..]]+    zipWith testCase [printf "%02d_%s" n name | n <- [1 :: Int ..]]   --------------------------------------------------------------------------------