22{-# LANGUAGE DeriveGeneric #-}
33{-# LANGUAGE DerivingStrategies #-}
44{-# LANGUAGE DuplicateRecordFields #-}
5+ {-# LANGUAGE FlexibleContexts #-}
56{-# LANGUAGE GADTs #-}
67{-# LANGUAGE GeneralizedNewtypeDeriving #-}
78{-# LANGUAGE LambdaCase #-}
9+ {-# LANGUAGE NamedFieldPuns #-}
810{-# LANGUAGE ScopedTypeVariables #-}
911{-# LANGUAGE StandaloneDeriving #-}
1012{-# LANGUAGE TypeApplications #-}
1113
1214-- |
1315-- Module: Distribution.Simple.SetupHooks.Internal
1416--
15- -- Internal implementation module.
17+ -- Internal implementation module for 'SetupHooks'.
18+ --
1619-- Users of @build-type: Hooks@ should import "Distribution.Simple.SetupHooks"
1720-- instead.
1821module Distribution.Simple.SetupHooks.Internal
@@ -109,20 +112,27 @@ import qualified Distribution.Simple.SetupHooks.Rule as Rule
109112import Distribution.Simple.Utils
110113import Distribution.System (Platform (.. ))
111114import Distribution.Utils.Path
115+ import Distribution.Utils.Structured
116+ ( structuredDecodeOrFailIO
117+ , structuredEncodeFile
118+ )
112119
113120import qualified Distribution.Types.BuildInfo.Lens as BI (buildInfo )
114121import Distribution.Types.LocalBuildConfig as LBC
115122import Distribution.Types.TargetInfo
116123import Distribution.Verbosity
117124
125+ import qualified Data.ByteString as BS
118126import qualified Data.ByteString.Lazy as LBS
119127import Data.Coerce (coerce )
128+ import Data.Either (fromRight )
120129import qualified Data.Graph as Graph
130+ import Data.IORef (IORef , modifyIORef' , newIORef , readIORef )
121131import qualified Data.List.NonEmpty as NE
122132import qualified Data.Map as Map
123133import qualified Data.Set as Set
124134
125- import System.Directory (doesFileExist )
135+ import System.Directory (doesFileExist , getModificationTime )
126136
127137--------------------------------------------------------------------------------
128138-- SetupHooks
@@ -849,7 +859,11 @@ executeRules =
849859-- an external hooks executable.
850860executeRulesUserOrSystem
851861 :: forall userOrSystem
852- . SScope userOrSystem
862+ . ( Binary (RuleData userOrSystem )
863+ , Structured (RuleData userOrSystem )
864+ , Eq (RuleData userOrSystem )
865+ )
866+ => SScope userOrSystem
853867 -> (RuleId -> RuleDynDepsCmd userOrSystem -> IO (Maybe ([Rule. Dependency ], LBS. ByteString )))
854868 -> (RuleId -> RuleExecCmd userOrSystem -> IO () )
855869 -> Verbosity
@@ -858,6 +872,12 @@ executeRulesUserOrSystem
858872 -> Map RuleId (RuleData userOrSystem )
859873 -> IO ()
860874executeRulesUserOrSystem scope runDepsCmdData runCmdData verbosity lbi tgtInfo allRules = do
875+ -- Load the rule cache from the previous build.
876+ -- Used to detect when rule definitions have changed.
877+ oldRules <- handleDoesNotExist Map. empty $ do
878+ -- NB: do a strict read to avoid retaining the file handle.
879+ bs <- BS. readFile rulesCacheFile
880+ fromRight Map. empty <$> structuredDecodeOrFailIO (LBS. fromStrict bs)
861881 -- Compute all extra dynamic dependency edges.
862882 dynDepsEdges <-
863883 flip Map. traverseMaybeWithKey allRules $
@@ -939,37 +959,39 @@ executeRulesUserOrSystem scope runDepsCmdData runCmdData verbosity lbi tgtInfo a
939959 , " it is not in the appropriate 'autogenComponentModules' directory)"
940960 ]
941961
942- -- Run all the demanded rules, in dependency order.
962+ -- Run all the demanded rules, in dependency order, propagating staleness.
963+ staleRulesRef <- newIORef Set. empty
943964 for_ sccs $ \ (Graph. Node ruleVertex _) ->
944965 -- Don't run a rule unless it is demanded.
945966 unless (ruleVertex `Set.member` nonDemandedRuleVerts) $ do
946- let ( r@ Rule
947- { ruleCommands = cmds
948- , staticDependencies = staticDeps
949- , results = reslts
950- }
951- , rId
952- , _staticRuleDepIds
953- ) =
954- ruleFromVertex ruleVertex
955- mbDyn = Map. lookup rId dynDepsEdges
956- allDeps = staticDeps ++ maybe [] fst mbDyn
967+ let (r, rId, _staticRuleDepIds) = ruleFromVertex ruleVertex
968+ Rule {ruleCommands, staticDependencies, results} = r
969+ mbDynDeps = Map. lookup rId dynDepsEdges
970+ allDeps = staticDependencies ++ maybe [] fst mbDynDeps
957971 -- Check that the dependencies the rule expects are indeed present.
958972 resolvedDeps <- traverse (resolveDependency verbosity rId allRules) allDeps
959973 missingRuleDeps <- filterM (missingDep mbWorkDir) resolvedDeps
960974 case NE. nonEmpty missingRuleDeps of
961975 Just missingDeps ->
962976 errorOut $ CantFindSourceForRuleDependencies (toRuleBinary r) missingDeps
963- -- Dependencies OK: run the associated action.
977+ -- Dependencies OK: check whether the rule is up to date before
978+ -- deciding to run it.
964979 Nothing -> do
965- let execCmd = ruleExecCmd scope cmds (snd <$> mbDyn)
966- runCmdData rId execCmd
967- -- Throw an error if running the action did not result in
968- -- the generation of outputs that we expected it to.
969- missingRuleResults <- filterM (missingDep mbWorkDir) $ NE. toList reslts
970- for_ (NE. nonEmpty missingRuleResults) $ \ missingResults ->
971- errorOut $ MissingRuleOutputs (toRuleBinary r) missingResults
972- return ()
980+ let dynDeps = maybe [] fst mbDynDeps
981+ ruleUpToDate mbWorkDir oldRules staleRulesRef rId r dynDeps >>= \ case
982+ True ->
983+ info verbosity $
984+ " Rule " ++ show rId ++ " is up to date; skipping."
985+ False -> do
986+ modifyIORef' staleRulesRef (Set. insert rId)
987+ runCmdData rId $ ruleExecCmd scope ruleCommands (snd <$> mbDynDeps)
988+ -- Throw an error if running the action did not result in
989+ -- the generation of outputs that we expected it to.
990+ missingRuleResults <- filterM (missingDep mbWorkDir) $ NE. toList results
991+ for_ (NE. nonEmpty missingRuleResults) $ \ missingResults ->
992+ errorOut $ MissingRuleOutputs (toRuleBinary r) missingResults
993+ -- Save the current rules to the cache for use in the next build.
994+ structuredEncodeFile rulesCacheFile allRules
973995 where
974996 toRuleBinary :: RuleData userOrSystem -> RuleBinary
975997 toRuleBinary = case scope of
@@ -978,6 +1000,7 @@ executeRulesUserOrSystem scope runDepsCmdData runCmdData verbosity lbi tgtInfo a
9781000 clbi = targetCLBI tgtInfo
9791001 mbWorkDir = mbWorkDirLBI lbi
9801002 compAutogenDir = autogenComponentModulesDir lbi clbi
1003+ rulesCacheFile = interpretSymbolicPath mbWorkDir (preBuildRulesCacheFile lbi clbi)
9811004 errorOut e =
9821005 dieWithException verbosity $
9831006 SetupHooksException $
@@ -987,6 +1010,59 @@ directRuleDependencyMaybe :: Rule.Dependency -> Maybe RuleId
9871010directRuleDependencyMaybe (RuleDependency dep) = Just $ outputOfRule dep
9881011directRuleDependencyMaybe (FileDependency {}) = Nothing
9891012
1013+ -- | Is the rule up to date (so that we can skip re-running it)?
1014+ --
1015+ -- As per the SetupHooks documentation, a rule must be re-run if:
1016+ --
1017+ -- - [N] the rule is new, or
1018+ -- - [S] the rule matches with an old rule, and either:
1019+ -- - [S1] an input to the rule has changed (either a file or rule dependency)
1020+ -- - [S2] the rule itself has changed
1021+ ruleUpToDate
1022+ :: Eq (RuleData userOrSystem )
1023+ => Maybe (SymbolicPath CWD (Dir Pkg ))
1024+ -- ^ working directory
1025+ -> Map RuleId (RuleData userOrSystem )
1026+ -- ^ old rules from the previous build
1027+ -> IORef (Set RuleId )
1028+ -- ^ rules that have been re-run
1029+ -> RuleId
1030+ -> RuleData userOrSystem
1031+ -> [Rule. Dependency ]
1032+ -- ^ dynamic dependencies of this rule
1033+ -> IO Bool
1034+ ruleUpToDate mbWorkDir oldRules staleRulesRef rId rule dynDeps = do
1035+ staleRules <- readIORef staleRulesRef
1036+ if ruleChanged || any (`Set.member` staleRules) ruleDeps
1037+ then return False
1038+ else do
1039+ let maybeModTime fp = handleDoesNotExist Nothing $ Just <$> getModificationTime fp
1040+ outMtimes <- traverse maybeModTime outputPaths
1041+ case sequenceA outMtimes of
1042+ -- At least one output is missing: must run the rule.
1043+ Nothing -> return False
1044+ Just outs ->
1045+ -- Re-run if an input is more recent than the oldest output.
1046+ case inputPaths of
1047+ [] -> return True
1048+ _ -> do
1049+ inMtimes <- traverse getModificationTime inputPaths
1050+ return (minimum outs >= maximum inMtimes)
1051+ where
1052+ i (Location dir file) = interpretSymbolicPath mbWorkDir (dir </> file)
1053+ allDeps = staticDependencies rule ++ dynDeps
1054+ ruleDeps = [outputOfRule ro | RuleDependency ro <- allDeps]
1055+ fileDeps = [loc | FileDependency loc <- allDeps]
1056+ inputPaths = map i fileDeps
1057+ outputPaths = fmap i (results rule)
1058+ ruleChanged =
1059+ case Map. lookup rId oldRules of
1060+ Just oldRule ->
1061+ -- Use the Eq instance to determine if the rule has changed
1062+ -- (as documented in the API).
1063+ oldRule /= rule
1064+ Nothing -> True
1065+
9901066resolveDependency :: Verbosity -> RuleId -> Map RuleId (RuleData scope ) -> Rule. Dependency -> IO Location
9911067resolveDependency verbosity rId allRules = \ case
9921068 FileDependency l -> return l
0 commit comments