Skip to content

Commit 70ed8a5

Browse files
authored
Merge pull request #11731 from sheaf/hooks-recomp
Implement recompilation checking for pre-build rules
2 parents ff9daa2 + e7cf0cb commit 70ed8a5

12 files changed

Lines changed: 343 additions & 27 deletions

File tree

Cabal-hooks/src/Distribution/Simple/SetupHooks.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ import Data.Map.Strict as Map
237237
A Cabal package with @Hooks@ @build-type@ must define the Haskell module
238238
@SetupHooks@ which defines a value @setupHooks :: 'SetupHooks'@.
239239
240-
These *setup hooks* allow package authors to customise the configuration and
240+
These __setup hooks__ allow package authors to customise the configuration and
241241
building of a package by providing certain hooks that get folded into the
242242
general package configuration and building logic within @Cabal@.
243243
@@ -359,8 +359,8 @@ following conditions apply:
359359
[N] the rule is new, or
360360
[S] the rule matches with an old rule, and either:
361361
362-
[S1] a file dependency of the rule has been modified/created/deleted, or
363-
a (transitive) rule dependency of the rule is itself stale, or
362+
[S1] a file dependency of the rule has been modified\/created\/deleted,
363+
or a (transitive) rule dependency of the rule is itself stale, or
364364
[S2] the rule is different from the old rule, e.g. the argument stored in
365365
the rule command has changed, or the pointer to the action to run the
366366
rule has changed. (This is determined using the @Eq Rule@ instance.)

Cabal/src/Distribution/Simple/BuildPaths.hs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ module Distribution.Simple.BuildPaths
2727
, haddockPref
2828
, autogenPackageModulesDir
2929
, autogenComponentModulesDir
30+
, preBuildRulesCacheFile
3031
, autogenPathsModuleName
3132
, autogenPackageInfoModuleName
3233
, cppHeaderName
@@ -160,6 +161,15 @@ autogenPackageModulesDir lbi = buildDir lbi </> makeRelativePathEx "global-autog
160161
autogenComponentModulesDir :: LocalBuildInfo -> ComponentLocalBuildInfo -> SymbolicPath Pkg (Dir Source)
161162
autogenComponentModulesDir lbi clbi = componentBuildDir lbi clbi </> makeRelativePathEx "autogen"
162163

164+
-- | The path to the pre-build rules cache file for a component, used to
165+
-- compute rule staleness across runs.
166+
preBuildRulesCacheFile
167+
:: LocalBuildInfo
168+
-> ComponentLocalBuildInfo
169+
-> SymbolicPath Pkg File
170+
preBuildRulesCacheFile lbi clbi =
171+
componentBuildDir lbi clbi </> makeRelativePathEx "setup-hooks-rules.cache"
172+
163173
-- NB: Look at 'checkForeignDeps' for where a simplified version of this
164174
-- has been copy-pasted.
165175

Cabal/src/Distribution/Simple/SetupHooks/Internal.hs

Lines changed: 100 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
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.
1821
module Distribution.Simple.SetupHooks.Internal
@@ -109,20 +112,27 @@ import qualified Distribution.Simple.SetupHooks.Rule as Rule
109112
import Distribution.Simple.Utils
110113
import Distribution.System (Platform (..))
111114
import Distribution.Utils.Path
115+
import Distribution.Utils.Structured
116+
( structuredDecodeOrFailIO
117+
, structuredEncodeFile
118+
)
112119

113120
import qualified Distribution.Types.BuildInfo.Lens as BI (buildInfo)
114121
import Distribution.Types.LocalBuildConfig as LBC
115122
import Distribution.Types.TargetInfo
116123
import Distribution.Verbosity
117124

125+
import qualified Data.ByteString as BS
118126
import qualified Data.ByteString.Lazy as LBS
119127
import Data.Coerce (coerce)
128+
import Data.Either (fromRight)
120129
import qualified Data.Graph as Graph
130+
import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
121131
import qualified Data.List.NonEmpty as NE
122132
import qualified Data.Map as Map
123133
import 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.
850860
executeRulesUserOrSystem
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 ()
860874
executeRulesUserOrSystem 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
9871010
directRuleDependencyMaybe (RuleDependency dep) = Just $ outputOfRule dep
9881011
directRuleDependencyMaybe (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+
9901066
resolveDependency :: Verbosity -> RuleId -> Map RuleId (RuleData scope) -> Rule.Dependency -> IO Location
9911067
resolveDependency verbosity rId allRules = \case
9921068
FileDependency l -> return l

Cabal/src/Distribution/Simple/SetupHooks/Rule.hs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@ deriving stock instance Eq (RuleData User)
267267
deriving stock instance Eq (RuleData System)
268268
deriving anyclass instance Binary (RuleData User)
269269
deriving anyclass instance Binary (RuleData System)
270+
deriving anyclass instance Structured (RuleData User)
271+
deriving anyclass instance Structured (RuleData System)
270272

271273
-- | Trimmed down 'Show' instance, mostly for error messages.
272274
instance Show RuleBinary where
@@ -1081,6 +1083,35 @@ instance
10811083
-- that involve existential quantification.
10821084
data family Tok (arg :: Symbol) :: k
10831085

1086+
instance
1087+
(Typeable scope, Typeable ruleCmd, Typeable deps)
1088+
=> Structured (RuleCommands scope deps ruleCmd)
1089+
where
1090+
structure _ =
1091+
Structure
1092+
tr
1093+
0
1094+
(show tr)
1095+
[
1096+
( "StaticRuleCommand"
1097+
,
1098+
[ nominalStructure $ Proxy @(ruleCmd scope (Tok "arg") (IO ()))
1099+
, nominalStructure $ Proxy @(Typeable.TypeRep (Tok "arg" :: Hs.Type))
1100+
]
1101+
)
1102+
,
1103+
( "DynamicRuleCommands"
1104+
,
1105+
[ nominalStructure $ Proxy @(Static scope (Dict (Binary (Tok "depsRes"), Show (Tok "depsRes"), Eq (Tok "depsRes"))))
1106+
, nominalStructure $ Proxy @(deps scope (Tok "depsArg") (Tok "depsRes"))
1107+
, nominalStructure $ Proxy @(ruleCmd scope (Tok "arg") (Tok "depsRes" -> IO ()))
1108+
, nominalStructure $ Proxy @(Typeable.TypeRep (Tok "depsArg", Tok "depsRes", Tok "arg"))
1109+
]
1110+
)
1111+
]
1112+
where
1113+
tr = Typeable.SomeTypeRep $ Typeable.typeRep @(RuleCommands scope deps ruleCmd)
1114+
10841115
instance
10851116
( forall res. Binary (ruleCmd System LBS.ByteString res)
10861117
, Binary (deps System LBS.ByteString LBS.ByteString)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
a = True
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
b = False
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
c = 'x'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module Main where
2+
3+
import Distribution.Simple ( defaultMainWithSetupHooks )
4+
import SetupHooks ( setupHooks )
5+
6+
main :: IO ()
7+
main = defaultMainWithSetupHooks setupHooks

0 commit comments

Comments
 (0)