purescript-0.13.3: src/Language/PureScript/Make/BuildPlan.hs
module Language.PureScript.Make.BuildPlan
( BuildPlan()
, BuildJobResult(..)
, buildJobSuccess
, buildJobFailure
, construct
, getResult
, collectResults
, markComplete
, needsRebuild
) where
import Prelude
import Control.Concurrent.Async.Lifted as A
import Control.Concurrent.Lifted as C
import Control.Monad hiding (sequence)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Control (MonadBaseControl(..))
import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
import Data.Foldable (foldl')
import qualified Data.Map as M
import Data.Maybe (catMaybes, fromMaybe)
import Data.Time.Clock (UTCTime)
import Language.PureScript.AST
import Language.PureScript.Crash
import qualified Language.PureScript.CST as CST
import Language.PureScript.Errors
import Language.PureScript.Externs
import Language.PureScript.Make.Actions as Actions
import Language.PureScript.Names (ModuleName)
-- | The BuildPlan tracks information about our build progress, and holds all
-- prebuilt modules for incremental builds.
data BuildPlan = BuildPlan
{ bpPrebuilt :: M.Map ModuleName Prebuilt
, bpBuildJobs :: M.Map ModuleName BuildJob
}
data Prebuilt = Prebuilt
{ pbModificationTime :: UTCTime
, pbExternsFile :: ExternsFile
}
newtype BuildJob = BuildJob
{ bjResult :: C.MVar BuildJobResult
-- ^ Note: an empty MVar indicates that the build job has not yet finished.
}
data BuildJobResult
= BuildJobSucceeded !MultipleErrors !ExternsFile
-- ^ Succeeded, with warnings and externs
--
| BuildJobFailed !MultipleErrors
-- ^ Failed, with errors
| BuildJobSkipped
-- ^ The build job was not run, because an upstream build job failed
buildJobSuccess :: BuildJobResult -> Maybe (MultipleErrors, ExternsFile)
buildJobSuccess (BuildJobSucceeded warnings externs) = Just (warnings, externs)
buildJobSuccess _ = Nothing
buildJobFailure :: BuildJobResult -> Maybe MultipleErrors
buildJobFailure (BuildJobFailed errors) = Just errors
buildJobFailure _ = Nothing
-- | Called when we finished compiling a module and want to report back the
-- compilation result, as well as any potential errors that were thrown.
markComplete
:: (MonadBaseControl IO m)
=> BuildPlan
-> ModuleName
-> BuildJobResult
-> m ()
markComplete buildPlan moduleName result = do
let BuildJob rVar = fromMaybe (internalError "make: markComplete no barrier") $ M.lookup moduleName (bpBuildJobs buildPlan)
putMVar rVar result
-- | Whether or not the module with the given ModuleName needs to be rebuilt
needsRebuild :: BuildPlan -> ModuleName -> Bool
needsRebuild bp moduleName = M.member moduleName (bpBuildJobs bp)
-- | Collects results for all prebuilt as well as rebuilt modules. This will
-- block until all build jobs are finished. Prebuilt modules always return no
-- warnings.
collectResults
:: (MonadBaseControl IO m)
=> BuildPlan
-> m (M.Map ModuleName BuildJobResult)
collectResults buildPlan = do
let prebuiltResults = M.map (BuildJobSucceeded (MultipleErrors []) . pbExternsFile) (bpPrebuilt buildPlan)
barrierResults <- traverse (readMVar . bjResult) $ bpBuildJobs buildPlan
pure (M.union prebuiltResults barrierResults)
-- | Gets the the build result for a given module name independent of whether it
-- was rebuilt or prebuilt. Prebuilt modules always return no warnings.
getResult
:: (MonadBaseControl IO m)
=> BuildPlan
-> ModuleName
-> m (Maybe (MultipleErrors, ExternsFile))
getResult buildPlan moduleName =
case M.lookup moduleName (bpPrebuilt buildPlan) of
Just es ->
pure (Just (MultipleErrors [], pbExternsFile es))
Nothing -> do
r <- readMVar $ bjResult $ fromMaybe (internalError "make: no barrier") $ M.lookup moduleName (bpBuildJobs buildPlan)
pure $ buildJobSuccess r
-- | Constructs a BuildPlan for the given module graph.
--
-- The given MakeActions are used to collect various timestamps in order to
-- determine whether a module needs rebuilding.
construct
:: forall m. (Monad m, MonadBaseControl IO m)
=> MakeActions m
-> ([CST.PartialResult Module], [(ModuleName, [ModuleName])])
-> m BuildPlan
construct MakeActions{..} (sorted, graph) = do
prebuilt <- foldl' collectPrebuiltModules M.empty . catMaybes <$> A.forConcurrently sorted findExistingExtern
let toBeRebuilt = filter (not . flip M.member prebuilt . getModuleName . CST.resPartial) sorted
buildJobs <- foldM makeBuildJob M.empty (map (getModuleName . CST.resPartial) toBeRebuilt)
pure $ BuildPlan prebuilt buildJobs
where
makeBuildJob prev moduleName = do
buildJob <- BuildJob <$> C.newEmptyMVar
pure (M.insert moduleName buildJob prev)
findExistingExtern :: CST.PartialResult Module -> m (Maybe (ModuleName, Bool, Prebuilt))
findExistingExtern (getModuleName . CST.resPartial -> moduleName) = runMaybeT $ do
inputTimestamp <- lift $ getInputTimestamp moduleName
(rebuildNever, existingTimestamp) <-
case inputTimestamp of
Left RebuildNever ->
fmap (True,) $ MaybeT $ getOutputTimestamp moduleName
Right (Just t1) -> do
outputTimestamp <- MaybeT $ getOutputTimestamp moduleName
guard (t1 < outputTimestamp)
pure (False, outputTimestamp)
_ -> mzero
externsFile <- MaybeT $ decodeExterns . snd <$> readExterns moduleName
pure (moduleName, rebuildNever, Prebuilt existingTimestamp externsFile)
collectPrebuiltModules :: M.Map ModuleName Prebuilt -> (ModuleName, Bool, Prebuilt) -> M.Map ModuleName Prebuilt
collectPrebuiltModules prev (moduleName, rebuildNever, pb)
| rebuildNever = M.insert moduleName pb prev
| otherwise = do
let deps = fromMaybe (internalError "make: module not found in dependency graph.") (lookup moduleName graph)
case traverse (fmap pbModificationTime . flip M.lookup prev) deps of
Nothing ->
-- If we end up here, one of the dependencies didn't exist in the
-- prebuilt map and so we know a dependency needs to be rebuilt, which
-- means we need to be rebuilt in turn.
prev
Just modTimes ->
case maximumMaybe modTimes of
Just depModTime | pbModificationTime pb < depModTime ->
prev
_ -> M.insert moduleName pb prev
maximumMaybe :: Ord a => [a] -> Maybe a
maximumMaybe [] = Nothing
maximumMaybe xs = Just $ maximum xs