Skip to content

Commit 262a20b

Browse files
committed
[Explicit Module Builds][Incremental Builds] Re-compile module dependnecies whose dependencies are up-to-date themselves but are themselves newer
For example consider the following module graph: test \ J \ G Where on an incremental build we detect that although G binary module product is *newer* than its textual source, said binary module product is also *newer* than a prior binary module product of J. Which means that although each of the modules is up-to-date with respect to its own textual source inputs, J's binary dependnecy input has been updated elsewhere and J needs to be re-built. Resolves rdar://129225956
1 parent b0300a8 commit 262a20b

File tree

2 files changed

+88
-8
lines changed

2 files changed

+88
-8
lines changed

Sources/SwiftDriver/IncrementalCompilation/FirstWaveComputer.swift

+29-7
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,14 @@ extension IncrementalCompilationState.FirstWaveComputer {
148148
private func computeInvalidatedModuleDependencies(on moduleDependencyGraph: InterModuleDependencyGraph)
149149
throws -> Set<ModuleDependencyId> {
150150
let mainModuleInfo = moduleDependencyGraph.mainModule
151+
let reachabilityMap = try moduleDependencyGraph.computeTransitiveClosure()
151152
var modulesRequiringRebuild: Set<ModuleDependencyId> = []
152-
var visitedModules: Set<ModuleDependencyId> = []
153+
var visited: Set<ModuleDependencyId> = []
153154
// Scan from the main module's dependencies to avoid reporting
154155
// the main module itself in the results.
155156
for dependencyId in mainModuleInfo.directDependencies ?? [] {
156-
try outOfDateModuleScan(on: moduleDependencyGraph, from: dependencyId, visited: &visitedModules,
157+
try outOfDateModuleScan(on: moduleDependencyGraph, from: dependencyId,
158+
reachabilityMap: reachabilityMap, visited: &visited,
157159
modulesRequiringRebuild: &modulesRequiringRebuild)
158160
}
159161

@@ -164,8 +166,12 @@ extension IncrementalCompilationState.FirstWaveComputer {
164166
/// Perform a postorder DFS to locate modules which are out-of-date with respect
165167
/// to their inputs. Upon encountering such a module, add it to the set of invalidated
166168
/// modules, along with the path from the root to this module.
169+
///
170+
/// Returns the last-modification-time of the newest module dependency output file,
171+
/// direct or transitive.
167172
private func outOfDateModuleScan(on moduleDependencyGraph: InterModuleDependencyGraph,
168173
from moduleId: ModuleDependencyId,
174+
reachabilityMap: [ModuleDependencyId : Set<ModuleDependencyId>],
169175
visited: inout Set<ModuleDependencyId>,
170176
modulesRequiringRebuild: inout Set<ModuleDependencyId>) throws {
171177
let moduleInfo = try moduleDependencyGraph.moduleInfo(of: moduleId)
@@ -175,20 +181,36 @@ extension IncrementalCompilationState.FirstWaveComputer {
175181
// If we have not already visited this module, recurse.
176182
if !visited.contains(dependencyId) {
177183
try outOfDateModuleScan(on: moduleDependencyGraph, from: dependencyId,
178-
visited: &visited,
184+
reachabilityMap: reachabilityMap, visited: &visited,
179185
modulesRequiringRebuild: &modulesRequiringRebuild)
180186
}
181187
// Even if we're not revisiting a dependency, we must check if it's already known to be out of date.
182188
hasOutOfDateModuleDependency = hasOutOfDateModuleDependency || modulesRequiringRebuild.contains(dependencyId)
183189
}
184190

185191
if hasOutOfDateModuleDependency {
186-
reporter?.reportExplicitDependencyWillBeReBuilt(moduleId.moduleNameForDiagnostic, reason: "Invalidated by downstream dependency")
187-
modulesRequiringRebuild.insert(moduleId)
192+
reporter?.reportExplicitDependencyWillBeReBuilt(moduleId.moduleNameForDiagnostic, reason: "Invalidated by downstream dependency")
193+
modulesRequiringRebuild.insert(moduleId)
188194
} else if try !IncrementalCompilationState.IncrementalDependencyAndInputSetup.verifyModuleDependencyUpToDate(moduleID: moduleId, moduleInfo: moduleInfo,
189195
fileSystem: fileSystem, reporter: reporter) {
190-
reporter?.reportExplicitDependencyWillBeReBuilt(moduleId.moduleNameForDiagnostic, reason: "Out-of-date")
191-
modulesRequiringRebuild.insert(moduleId)
196+
reporter?.reportExplicitDependencyWillBeReBuilt(moduleId.moduleNameForDiagnostic, reason: "Out-of-date")
197+
modulesRequiringRebuild.insert(moduleId)
198+
}
199+
200+
// If a prior variant of this module dependnecy exists, and is older than any of its direct or transitive
201+
// module dependency outputs, it must also be re-built
202+
if let outputModTime = try? fileSystem.lastModificationTime(for: VirtualPath.lookup(moduleInfo.modulePath.path)) {
203+
// FIXME: This needs caching for mod times, otherwise there's too much repeated `stat` traffic
204+
for depId in reachabilityMap[moduleId] ?? [] {
205+
guard let depOutputTimeStamp = try? fileSystem.lastModificationTime(for: VirtualPath.lookup(moduleDependencyGraph.moduleInfo(of: depId).modulePath.path)) else {
206+
continue
207+
}
208+
if depOutputTimeStamp > outputModTime {
209+
reporter?.reportExplicitDependencyWillBeReBuilt(moduleId.moduleNameForDiagnostic, reason: "Has newer module dependency inputs")
210+
modulesRequiringRebuild.insert(moduleId)
211+
break
212+
}
213+
}
192214
}
193215

194216
// Now that we've determined if this module must be rebuilt, mark it as visited.

Tests/SwiftDriverTests/IncrementalCompilationTests.swift

+59-1
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,6 @@ extension IncrementalCompilationTests {
382382
// On this graph, inputs of 'G' are updated, causing it to be re-built
383383
// as well as all modules on paths from root to it: 'Y', 'H', 'T','J'
384384
func testExplicitIncrementalBuildChangedDependencyInvalidatesUpstreamDependencies() throws {
385-
// Add an import of 'B', 'C' to make sure followup changes has consistent inputs
386385
replace(contentsOf: "other", with: "import Y;import T")
387386
try buildInitialState(checkDiagnostics: false, explicitModuleBuild: true)
388387

@@ -431,6 +430,62 @@ extension IncrementalCompilationTests {
431430
linking
432431
}
433432
}
433+
434+
// A dependency has been re-built to be newer than its dependents
435+
// so we must ensure the dependents get re-built even though all the
436+
// modules are up-to-date with respect to their textual source inputs.
437+
//
438+
// test
439+
// \
440+
// J
441+
// \
442+
// G
443+
//
444+
// On this graph, after the initial build, if G module binary file is newer
445+
// than that of J, even if each of the modules is up-to-date w.r.t. their source inputs
446+
// we still expect that J gets re-built
447+
func testExplicitIncrementalBuildChangedDependencyBinaryInvalidatesUpstreamDependencies() throws {
448+
replace(contentsOf: "other", with: "import J;")
449+
try buildInitialState(checkDiagnostics: false, explicitModuleBuild: true)
450+
451+
let modCacheEntries = try localFileSystem.getDirectoryContents(explicitModuleCacheDir)
452+
let nameOfGModule = try XCTUnwrap(modCacheEntries.first { $0.hasPrefix("G") && $0.hasSuffix(".swiftmodule")})
453+
let pathToGModule = explicitModuleCacheDir.appending(component: nameOfGModule)
454+
// Just update the time-stamp of one of the module dependencies' outputs.
455+
// Also add a dependency to cause a re-scan.
456+
touch(pathToGModule)
457+
replace(contentsOf: "other", with: "import J;import R")
458+
459+
// Changing a dependency will mean that we both re-run the dependency scan,
460+
// and also ensure that all source-files are re-built with a non-cascading build
461+
// since the source files themselves have not changed.
462+
try doABuild(
463+
"update dependency (G) result timestamp",
464+
checkDiagnostics: true,
465+
extraArguments: explicitBuildArgs,
466+
whenAutolinking: autolinkLifecycleExpectedDiags
467+
) {
468+
readGraph
469+
enablingCrossModule
470+
readInterModuleGraph
471+
explicitMustReScanDueToChangedImports
472+
maySkip("main")
473+
schedulingChangedInitialQueuing("other")
474+
skipping("main")
475+
findingBatchingCompiling("other")
476+
reading(deps: "other")
477+
fingerprintsChanged("other")
478+
moduleOutputNotFound("R")
479+
moduleWillBeRebuiltOutOfDate("R")
480+
compilingExplicitSwiftDependency("R")
481+
explicitModulesWillBeRebuilt(["J", "R"])
482+
explicitDependencyNewerModuleInputs("J")
483+
compilingExplicitSwiftDependency("J")
484+
skipped("main")
485+
schedulingPostCompileJobs
486+
linking
487+
}
488+
}
434489
}
435490

436491
extension IncrementalCompilationTests {
@@ -1645,6 +1700,9 @@ extension DiagVerifiable {
16451700
@DiagsBuilder func explicitDependencyInvalidatedDownstream(_ moduleName: String) -> [Diagnostic.Message] {
16461701
"Incremental compilation: Dependency module '\(moduleName)' will be re-built: Invalidated by downstream dependency"
16471702
}
1703+
@DiagsBuilder func explicitDependencyNewerModuleInputs(_ moduleName: String) -> [Diagnostic.Message] {
1704+
"Incremental compilation: Dependency module '\(moduleName)' will be re-built: Has newer module dependency inputs"
1705+
}
16481706

16491707
// MARK: - misc
16501708
@DiagsBuilder var enablingCrossModule: [Diagnostic.Message] {

0 commit comments

Comments
 (0)