feat: lake: cache built file hashes

This commit is contained in:
tydeu 2023-08-22 18:03:34 -04:00 committed by Mac Malone
parent c6299eef45
commit a0440ea4ea
8 changed files with 97 additions and 47 deletions

View file

@ -14,24 +14,49 @@ namespace Lake
def inputFile (path : FilePath) : SchedulerM (BuildJob FilePath) :=
Job.async <| (path, ·) <$> computeTrace path
@[specialize] def buildUnlessUpToDate [CheckExists ι] [GetMTime ι] (info : ι)
/-- Check if the `info` is up-to-date by comparing `depTrace` with `traceFile`. -/
@[specialize] def BuildTrace.checkUpToDate [CheckExists ι] [GetMTime ι]
(info : ι) (depTrace : BuildTrace) (traceFile : FilePath) : JobM Bool := do
if (← getIsOldMode) then
depTrace.checkAgainstTime info
else
depTrace.checkAgainstFile info traceFile
@[inline] def buildUnlessUpToDate [CheckExists ι] [GetMTime ι] (info : ι)
(depTrace : BuildTrace) (traceFile : FilePath) (build : JobM PUnit) : JobM PUnit := do
let isOldMode ← getIsOldMode
let upToDate ←
if isOldMode then
depTrace.checkAgainstTime info
else
depTrace.checkAgainstFile info traceFile
unless upToDate do
unless (← depTrace.checkUpToDate info traceFile) do
build
unless isOldMode do
depTrace.writeToFile traceFile
depTrace.writeToFile traceFile
/-- Fetch the trace of a file that may have its hash already cached in a `.hash` file. -/
def fetchFileTrace (file : FilePath) : BuildM BuildTrace := do
if (← getTrustHash) then
let hashFile := FilePath.mk <| file.toString ++ ".hash"
if let some hash ← Hash.load? hashFile then
return .mk hash (← getMTime file)
else
let hash ← computeHash file
IO.FS.writeFile hashFile hash.toString
return .mk hash (← getMTime file)
else
computeTrace file
/-- Compute the hash of a file and save it to a `.hash` file. -/
def cacheFileHash (file : FilePath) : IO Hash := do
let hash ← computeHash file
let hashFile := FilePath.mk <| file.toString ++ ".hash"
IO.FS.writeFile hashFile hash.toString
return hash
def buildFileUnlessUpToDate (file : FilePath)
(depTrace : BuildTrace) (build : BuildM PUnit) : BuildM BuildTrace := do
let traceFile := FilePath.mk <| file.toString ++ ".trace"
buildUnlessUpToDate file depTrace traceFile build
computeTrace file
if (← depTrace.checkUpToDate file traceFile) then
fetchFileTrace file
else
build
depTrace.writeToFile traceFile
return .mk (← cacheFileHash file) (← getMTime file)
@[inline] def buildFileAfterDep
(file : FilePath) (dep : BuildJob α) (build : α → BuildM PUnit)

View file

@ -15,16 +15,32 @@ import Lake.Build.Topological
open System
namespace Lake
/-- A Lake context with some additional caching for builds. -/
structure BuildContext extends Context where
leanTrace : BuildTrace
/-- Configuration options for a Lake build. -/
structure BuildConfig where
oldMode : Bool := false
trustHash : Bool := true
/-- A Lake context with a build configuration and additional build data. -/
structure BuildContext extends BuildConfig, Context where
leanTrace : BuildTrace
startedBuilds : IO.Ref Nat
finishedBuilds : IO.Ref Nat
/-- A transformer to equip a monad with a `BuildContext`. -/
abbrev BuildT := ReaderT BuildContext
@[inline] def getLeanTrace [Monad m] : BuildT m BuildTrace :=
(·.leanTrace) <$> readThe BuildContext
@[inline] def getBuildConfig [Monad m] : BuildT m BuildConfig :=
(·.toBuildConfig) <$> readThe BuildContext
@[inline] def getIsOldMode [Monad m] : BuildT m Bool :=
(·.oldMode) <$> getBuildConfig
@[inline] def getTrustHash [Monad m] : BuildT m Bool :=
(·.trustHash) <$> getBuildConfig
/-- The monad for the Lake build manager. -/
abbrev SchedulerM := BuildT <| LogT BaseIO

View file

@ -141,6 +141,9 @@ def Module.recBuildLean (mod : Module) : IndexBuildM (BuildJob Unit) := do
buildUnlessUpToDate mod modTrace mod.traceFile do
compileLeanModule mod.name.toString mod.leanFile mod.oleanFile mod.ileanFile mod.cFile
(← getLeanPath) mod.rootDir dynlibs dynlibPath (mod.leanArgs ++ mod.weakLeanArgs) (← getLean)
discard <| cacheFileHash mod.oleanFile
discard <| cacheFileHash mod.ileanFile
discard <| cacheFileHash mod.cFile
return ((), depTrace)
/-- The `ModuleFacetConfig` for the builtin `leanArtsFacet`. -/
@ -151,20 +154,20 @@ def Module.leanArtsFacetConfig : ModuleFacetConfig leanArtsFacet :=
def Module.oleanFacetConfig : ModuleFacetConfig oleanFacet :=
mkFacetJobConfigSmall fun mod => do
(← mod.leanArts.fetch).bindSync fun _ depTrace =>
return (mod.oleanFile, mixTrace (← computeTrace mod.oleanFile) depTrace)
return (mod.oleanFile, mixTrace (← fetchFileTrace mod.oleanFile) depTrace)
/-- The `ModuleFacetConfig` for the builtin `ileanFacet`. -/
def Module.ileanFacetConfig : ModuleFacetConfig ileanFacet :=
mkFacetJobConfigSmall fun mod => do
(← mod.leanArts.fetch).bindSync fun _ depTrace =>
return (mod.ileanFile, mixTrace (← computeTrace mod.ileanFile) depTrace)
return (mod.ileanFile, mixTrace (← fetchFileTrace mod.ileanFile) depTrace)
/-- The `ModuleFacetConfig` for the builtin `cFacet`. -/
def Module.cFacetConfig : ModuleFacetConfig cFacet :=
mkFacetJobConfigSmall fun mod => do
(← mod.leanArts.fetch).bindSync fun _ _ =>
-- do content-aware hashing so that we avoid recompiling unchanged C files
return (mod.cFile, ← computeTrace mod.cFile)
return (mod.cFile, ← fetchFileTrace mod.cFile)
/-- Recursively build the module's object file from its C file produced by `lean`. -/
def Module.recBuildLeanO (self : Module) : IndexBuildM (BuildJob FilePath) := do

View file

@ -11,21 +11,15 @@ open System
namespace Lake
def mkBuildContext (ws : Workspace) (oldMode : Bool) : IO BuildContext := do
let lean := ws.lakeEnv.lean
let leanTrace := Hash.ofString lean.githash
def mkBuildContext (ws : Workspace) (config : BuildConfig) : IO BuildContext := do
return {
opaqueWs := ws, leanTrace, oldMode
opaqueWs := ws,
toBuildConfig := config,
startedBuilds := ← IO.mkRef 0
finishedBuilds := ← IO.mkRef 0
finishedBuilds := ← IO.mkRef 0,
leanTrace := Hash.ofString ws.lakeEnv.lean.githash
}
@[inline] def getLeanTrace : BuildM BuildTrace :=
(·.leanTrace) <$> readThe BuildContext
@[inline] def getIsOldMode : BuildM Bool :=
(·.oldMode) <$> readThe BuildContext
def failOnBuildCycle [ToString k] : Except (List k) α → BuildM α
| Except.ok a => pure a
| Except.error cycle => do
@ -92,10 +86,10 @@ where
self.root.buildDir / lockFileName
/-- Run the given build function in the Workspace's context. -/
@[inline] def Workspace.runBuild (ws : Workspace) (build : BuildM α) (oldMode := false) : LogIO α := do
let ctx ← mkBuildContext ws oldMode
@[inline] def Workspace.runBuild (ws : Workspace) (build : BuildM α) (config : BuildConfig := {}) : LogIO α := do
let ctx ← mkBuildContext ws config
/-
TODO:
TODO:
The lock file has been temporarily disabled (by lean4#2445)
until we have an API for catching `Ctrl-C` during a build.
Absent this, the lock file was too disruptive for users.
@ -104,5 +98,5 @@ where
build.run ctx
/-- Run the given build function in the Lake monad's workspace. -/
@[inline] def runBuild (build : BuildM α) (oldMode := false) : LakeT LogIO α := do
(← getWorkspace).runBuild build oldMode
@[inline] def runBuild (build : BuildM α) (config : BuildConfig := {}) : LakeT LogIO α := do
(← getWorkspace).runBuild build config

View file

@ -10,10 +10,10 @@ namespace Lake
def env (cmd : String) (args : Array String := #[]) : LakeT IO UInt32 := do
IO.Process.spawn {cmd, args, env := ← getAugmentedEnv} >>= (·.wait)
def exe (name : Name) (args : Array String := #[]) (oldMode := false) : LakeT LogIO UInt32 := do
def exe (name : Name) (args : Array String := #[]) (buildConfig : BuildConfig := {}) : LakeT LogIO UInt32 := do
let ws ← getWorkspace
if let some exe := ws.findLeanExe? name then
let exeFile ← ws.runBuild (exe.build >>= (·.await)) oldMode
let exeFile ← ws.runBuild (exe.build >>= (·.await)) buildConfig
env exeFile.toString args
else
error s!"unknown executable `{name}`"

View file

@ -23,6 +23,7 @@ OPTIONS:
--lean=cmd specify the `lean` command used by Lake
-K key[=value] set the configuration file option named key
--old only rebuild modified modules (ignore transitive deps)
--rehash, -H hash all files for traces (do not trust `.hash` files)
--update, -U update manifest before building
COMMANDS:

View file

@ -33,8 +33,9 @@ structure LakeOptions where
subArgs : List String := []
wantsHelp : Bool := false
verbosity : Verbosity := .normal
oldMode : Bool := false
updateDeps : Bool := false
oldMode : Bool := false
trustHash : Bool := true
/-- Get the Lean installation. Error if missing. -/
def LakeOptions.getLeanInstall (opts : LakeOptions) : Except CliError LeanInstall :=
@ -66,7 +67,12 @@ def LakeOptions.mkLoadConfig (opts : LakeOptions) : EIO CliError LoadConfig :=
leanOpts := Lean.Options.empty
}
export LakeOptions (mkLoadConfig)
/-- Make a `BuildConfig` from a `LakeOptions`. -/
def LakeOptions.mkBuildConfig (opts : LakeOptions) : BuildConfig where
oldMode := opts.oldMode
trustHash := opts.trustHash
export LakeOptions (mkLoadConfig mkBuildConfig)
/-! ## Monad -/
@ -134,6 +140,7 @@ def lakeShortOption : (opt : Char) → CliM PUnit
| 'K' => do setConfigOpt <| ← takeOptArg "-K" "key-value pair"
| 'U' => modifyThe LakeOptions ({· with updateDeps := true})
| 'h' => modifyThe LakeOptions ({· with wantsHelp := true})
| 'H' => modifyThe LakeOptions ({· with trustHash := false})
| opt => throw <| CliError.unknownShortOption opt
def lakeLongOption : (opt : String) → CliM PUnit
@ -141,6 +148,7 @@ def lakeLongOption : (opt : String) → CliM PUnit
| "--verbose" => modifyThe LakeOptions ({· with verbosity := .verbose})
| "--update" => modifyThe LakeOptions ({· with updateDeps := true})
| "--old" => modifyThe LakeOptions ({· with oldMode := true})
| "--rehash" => modifyThe LakeOptions ({· with trustHash := false})
| "--dir" => do let rootDir ← takeOptArg "--dir" "path"; modifyThe LakeOptions ({· with rootDir})
| "--file" => do let configFile ← takeOptArg "--file" "path"; modifyThe LakeOptions ({· with configFile})
| "--lean" => do setLean <| ← takeOptArg "--lean" "path or command"
@ -265,7 +273,8 @@ protected def build : CliM PUnit := do
let ws ← loadWorkspace config opts.updateDeps
let targetSpecs ← takeArgs
let specs ← parseTargetSpecs ws targetSpecs
ws.runBuild (buildSpecs specs) opts.oldMode |>.run (MonadLog.io opts.verbosity)
let buildConfig := mkBuildConfig opts
ws.runBuild (buildSpecs specs) buildConfig |>.run (MonadLog.io opts.verbosity)
protected def resolveDeps : CliM PUnit := do
processOptions lakeOption
@ -293,8 +302,9 @@ protected def upload : CliM PUnit := do
protected def printPaths : CliM PUnit := do
processOptions lakeOption
let opts ← getThe LakeOptions
let config ← mkLoadConfig opts
printPaths config (← takeArgs) opts.oldMode opts.verbosity
let loadConfig ← mkLoadConfig opts
let buildConfig := mkBuildConfig opts
printPaths loadConfig (← takeArgs) buildConfig opts.verbosity
protected def clean : CliM PUnit := do
processOptions lakeOption
@ -349,7 +359,8 @@ protected def exe : CliM PUnit := do
let config ← mkLoadConfig opts
let ws ← loadWorkspace config
let ctx := mkLakeContext ws
exit <| ← (exe exeName args.toArray opts.oldMode).run ctx
let buildConfig := mkBuildConfig opts
exit <| ← (exe exeName args.toArray buildConfig).run ctx
protected def selfCheck : CliM PUnit := do
processOptions lakeOption

View file

@ -26,15 +26,15 @@ If no configuration file exists, exit silently with `noConfigFileCode` (i.e, 2).
The `print-paths` command is used internally by Lean 4 server.
-/
def printPaths (config : LoadConfig) (imports : List String := [])
(oldMode : Bool := false) (verbosity : Verbosity := .normal) : MainM PUnit := do
if (← config.configFile.pathExists) then
def printPaths (loadConfig : LoadConfig) (imports : List String := [])
(buildConfig : BuildConfig := {}) (verbosity : Verbosity := .normal) : MainM PUnit := do
if (← loadConfig.configFile.pathExists) then
if let some errLog := (← IO.getEnv invalidConfigEnvVar) then
IO.eprint errLog
IO.eprintln s!"Invalid Lake configuration. Please restart the server after fixing the Lake configuration file."
exit 1
let ws ← MainM.runLogIO (loadWorkspace config) verbosity
let dynlibs ← ws.runBuild (buildImportsAndDeps imports) oldMode
let ws ← MainM.runLogIO (loadWorkspace loadConfig) verbosity
let dynlibs ← ws.runBuild (buildImportsAndDeps imports) buildConfig
|>.run (MonadLog.eio verbosity)
IO.println <| Json.compress <| toJson {
oleanPath := ws.leanPath