From a52491bd7a9969e276f88fc11b835fb1fcc0ef70 Mon Sep 17 00:00:00 2001 From: Artem Chikin Date: Fri, 11 Aug 2023 16:12:24 -0700 Subject: [PATCH] [Explicit Module Builds][Incremental Builds] Only re-built module dependencies which have changed or whose dependencies have changed. Resolves rdar://113638007 --- .../CommonDependencyOperations.swift | 11 +- .../InterModuleDependencyGraph.swift | 9 ++ .../FirstWaveComputer.swift | 130 ++++++++++++++++-- ...crementalCompilationState+Extensions.swift | 18 ++- .../IncrementalCompilationState.swift | 1 + .../IncrementalDependencyAndInputSetup.swift | 125 +++++++++-------- Sources/SwiftDriver/Jobs/Planning.swift | 4 +- .../Swift/E.swiftinterface | 2 +- .../Swift/H.swiftinterface | 4 + .../Swift/J.swiftinterface | 4 + .../Swift/R.swiftinterface | 3 + .../Swift/T.swiftinterface | 5 + .../Swift/Y.swiftinterface | 5 + .../IncrementalCompilationTests.swift | 90 ++++++++++-- 14 files changed, 329 insertions(+), 82 deletions(-) create mode 100644 TestInputs/ExplicitModuleBuilds/Swift/H.swiftinterface create mode 100644 TestInputs/ExplicitModuleBuilds/Swift/J.swiftinterface create mode 100644 TestInputs/ExplicitModuleBuilds/Swift/R.swiftinterface create mode 100644 TestInputs/ExplicitModuleBuilds/Swift/T.swiftinterface create mode 100644 TestInputs/ExplicitModuleBuilds/Swift/Y.swiftinterface diff --git a/Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/CommonDependencyOperations.swift b/Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/CommonDependencyOperations.swift index cb3b7cbd6..8105d8a98 100644 --- a/Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/CommonDependencyOperations.swift +++ b/Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/CommonDependencyOperations.swift @@ -54,6 +54,13 @@ import func TSCBasic.topologicalSort } extension InterModuleDependencyGraph { + var topologicalSorting: [ModuleDependencyId] { + get throws { + try topologicalSort(Array(modules.keys), + successors: { try moduleInfo(of: $0).directDependencies! }) + } + } + /// Compute a set of modules that are "reachable" (form direct or transitive dependency) /// from each module in the graph. /// This routine relies on the fact that the dependency graph is acyclic. A lack of cycles means @@ -65,9 +72,7 @@ extension InterModuleDependencyGraph { /// } /// } func computeTransitiveClosure() throws -> [ModuleDependencyId : Set] { - let topologicalIdList = - try topologicalSort(Array(modules.keys), - successors: { try moduleInfo(of: $0).directDependencies! }) + let topologicalIdList = try self.topologicalSorting // This structure will contain the final result var transitiveClosureMap = topologicalIdList.reduce(into: [ModuleDependencyId : Set]()) { diff --git a/Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/InterModuleDependencyGraph.swift b/Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/InterModuleDependencyGraph.swift index 2904df07e..ffb83cae0 100644 --- a/Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/InterModuleDependencyGraph.swift +++ b/Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/InterModuleDependencyGraph.swift @@ -30,6 +30,15 @@ public enum ModuleDependencyId: Hashable { case .clang(let name): return name } } + + internal var moduleNameForDiagnostic: String { + switch self { + case .swift(let name): return name + case .swiftPlaceholder(let name): return name + "(placeholder)" + case .swiftPrebuiltExternal(let name): return name + "(swiftmodule)" + case .clang(let name): return name + "(pcm)" + } + } } extension ModuleDependencyId: Codable { diff --git a/Sources/SwiftDriver/IncrementalCompilation/FirstWaveComputer.swift b/Sources/SwiftDriver/IncrementalCompilation/FirstWaveComputer.swift index 5ec069cf3..22a17798b 100644 --- a/Sources/SwiftDriver/IncrementalCompilation/FirstWaveComputer.swift +++ b/Sources/SwiftDriver/IncrementalCompilation/FirstWaveComputer.swift @@ -25,7 +25,8 @@ extension IncrementalCompilationState { let fileSystem: FileSystem let showJobLifecycle: Bool let alwaysRebuildDependents: Bool - let rebuildExplicitModuleDependencies: Bool + let interModuleDependencyGraph: InterModuleDependencyGraph? + let explicitModuleDependenciesGuaranteedUpToDate: Bool /// If non-null outputs information for `-driver-show-incremental` for input path private let reporter: Reporter? @@ -33,6 +34,7 @@ extension IncrementalCompilationState { initialState: IncrementalCompilationState.InitialStateForPlanning, jobsInPhases: JobsInPhases, driver: Driver, + interModuleDependencyGraph: InterModuleDependencyGraph?, reporter: Reporter? ) { self.moduleDependencyGraph = initialState.graph @@ -44,8 +46,9 @@ extension IncrementalCompilationState { self.showJobLifecycle = driver.showJobLifecycle self.alwaysRebuildDependents = initialState.incrementalOptions.contains( .alwaysRebuildDependents) - self.rebuildExplicitModuleDependencies = - initialState.maybeUpToDatePriorInterModuleDependencyGraph != nil ? false : true + self.interModuleDependencyGraph = interModuleDependencyGraph + self.explicitModuleDependenciesGuaranteedUpToDate = + initialState.upToDatePriorInterModuleDependencyGraph != nil ? true : false self.reporter = reporter } @@ -118,11 +121,7 @@ extension IncrementalCompilationState.FirstWaveComputer { : compileGroups[input] } - // If module dependencies are known to be up-to-date, do not rebuild them - let mandatoryBeforeCompilesJobs = self.rebuildExplicitModuleDependencies ? - jobsInPhases.beforeCompiles : - jobsInPhases.beforeCompiles.filter { $0.kind != .generatePCM && $0.kind != .compileModuleFromInterface } - + let mandatoryBeforeCompilesJobs = try computeMandatoryBeforeCompilesJobs() let batchedCompilationJobs = try batchJobFormer.formBatchedJobs( mandatoryCompileGroupsInOrder.flatMap {$0.allJobs()}, showJobLifecycle: showJobLifecycle) @@ -136,6 +135,121 @@ extension IncrementalCompilationState.FirstWaveComputer { mandatoryJobsInOrder: mandatoryJobsInOrder) } + /// We must determine if any of the module dependencies require re-compilation + /// Since we know that a prior dependency graph was not completely up-to-date, + /// there must be at least *some* dependencies that require being re-built. + /// + /// If a dependency is deemed as requiring a re-build, then every module + /// between it and the root (source module being built by this driver + /// instance) must also be re-built. + private func computeInvalidatedModuleDependencies(on moduleDependencyGraph: InterModuleDependencyGraph) + throws -> Set { + let mainModuleInfo = moduleDependencyGraph.mainModule + var modulesRequiringRebuild: Set = [] + var validatedModules: Set = [] + // Scan from the main module's dependencies to avoid reporting + // the main module itself in the results. + for dependencyId in mainModuleInfo.directDependencies ?? [] { + try outOfDateModuleScan(on: moduleDependencyGraph, from: dependencyId, + pathSoFar: [], visitedValidated: &validatedModules, + modulesRequiringRebuild: &modulesRequiringRebuild) + } + + reporter?.reportExplicitDependencyReBuildSet(Array(modulesRequiringRebuild)) + return modulesRequiringRebuild + } + + /// Perform a postorder DFS to locate modules which are out-of-date with respect + /// to their inputs. Upon encountering such a module, add it to the set of invalidated + /// modules, along with the path from the root to this module. + private func outOfDateModuleScan(on moduleDependencyGraph: InterModuleDependencyGraph, + from moduleId: ModuleDependencyId, + pathSoFar: [ModuleDependencyId], + visitedValidated: inout Set, + modulesRequiringRebuild: inout Set) throws { + let moduleInfo = try moduleDependencyGraph.moduleInfo(of: moduleId) + let isMainModule = moduleId == .swift(moduleDependencyGraph.mainModuleName) + + // Routine to invalidate the path from root to this node + let invalidatePath = { (modulesRequiringRebuild: inout Set) in + if let reporter { + for pathModuleId in pathSoFar { + if !modulesRequiringRebuild.contains(pathModuleId) && + !isMainModule { + let pathModuleInfo = try moduleDependencyGraph.moduleInfo(of: pathModuleId) + reporter.reportExplicitDependencyWillBeReBuilt(pathModuleId.moduleNameForDiagnostic, + reason: "Invalidated by downstream dependency") + } + } + } + modulesRequiringRebuild.formUnion(pathSoFar) + } + + // Routine to invalidate this node and the path that led to it + let invalidateOutOfDate = { (modulesRequiringRebuild: inout Set) in + reporter?.reportExplicitDependencyWillBeReBuilt(moduleId.moduleNameForDiagnostic, reason: "Out-of-date") + modulesRequiringRebuild.insert(moduleId) + try invalidatePath(&modulesRequiringRebuild) + } + + // Visit the module's dependencies + for dependencyId in moduleInfo.directDependencies ?? [] { + if !visitedValidated.contains(dependencyId) { + let newPath = pathSoFar + [moduleId] + try outOfDateModuleScan(on: moduleDependencyGraph, from: dependencyId, pathSoFar: newPath, + visitedValidated: &visitedValidated, + modulesRequiringRebuild: &modulesRequiringRebuild) + } + } + + if modulesRequiringRebuild.contains(moduleId) { + try invalidatePath(&modulesRequiringRebuild) + } else if try !IncrementalCompilationState.IncrementalDependencyAndInputSetup.verifyModuleDependencyUpToDate(moduleID: moduleId, moduleInfo: moduleInfo, + fileSystem: fileSystem, reporter: reporter) { + try invalidateOutOfDate(&modulesRequiringRebuild) + } else { + // Only if this module is known to be up-to-date with respect to its inputs + // and dependencies do we mark it as visited. We may need to re-visit + // out-of-date modules in order to collect all possible paths to them. + visitedValidated.insert(moduleId) + } + } + + /// In an explicit module build, filter out dependency module pre-compilation tasks + /// for modules up-to-date from a prior compile. + private func computeMandatoryBeforeCompilesJobs() throws -> [Job] { + // In an implicit module build, we have nothing to filter/compute here + guard let moduleDependencyGraph = interModuleDependencyGraph else { + return jobsInPhases.beforeCompiles + } + + // If a prior compile's dependency graph was fully up-to-date, we can skip + // re-building all dependency modules. + guard !self.explicitModuleDependenciesGuaranteedUpToDate else { + return jobsInPhases.beforeCompiles.filter { $0.kind != .generatePCM && + $0.kind != .compileModuleFromInterface } + } + + // Determine which module pre-build jobs must be re-run + let modulesRequiringReBuild = + try computeInvalidatedModuleDependencies(on: moduleDependencyGraph) + + // Filter the `.generatePCM` and `.compileModuleFromInterface` jobs for + // modules which do *not* need re-building. + let mandatoryBeforeCompilesJobs = jobsInPhases.beforeCompiles.filter { job in + switch job.kind { + case .generatePCM: + return modulesRequiringReBuild.contains(.clang(job.moduleName)) + case .compileModuleFromInterface: + return modulesRequiringReBuild.contains(.swift(job.moduleName)) + default: + return true + } + } + + return mandatoryBeforeCompilesJobs + } + /// Determine if any of the jobs in the `afterCompiles` group depend on outputs produced by jobs in /// `beforeCompiles` group, which are not also verification jobs. private func nonVerifyAfterCompileJobsDependOnBeforeCompileJobs() -> Bool { diff --git a/Sources/SwiftDriver/IncrementalCompilation/IncrementalCompilationState+Extensions.swift b/Sources/SwiftDriver/IncrementalCompilation/IncrementalCompilationState+Extensions.swift index 134ff6dd4..a8731eda9 100644 --- a/Sources/SwiftDriver/IncrementalCompilation/IncrementalCompilationState+Extensions.swift +++ b/Sources/SwiftDriver/IncrementalCompilation/IncrementalCompilationState+Extensions.swift @@ -50,8 +50,8 @@ extension IncrementalCompilationState { let graph: ModuleDependencyGraph /// Information about the last known compilation, incl. the location of build artifacts such as the dependency graph. let buildRecordInfo: BuildRecordInfo - /// Record about the compiled module's module dependencies from the last compile. - let maybeUpToDatePriorInterModuleDependencyGraph: InterModuleDependencyGraph? + /// Record about the compiled module's explicit module dependencies from a prior compile. + let upToDatePriorInterModuleDependencyGraph: InterModuleDependencyGraph? /// A set of inputs invalidated by external changes. let inputsInvalidatedByExternals: TransitivelyInvalidatedSwiftSourceFileSet /// Compiler options related to incremental builds. @@ -299,9 +299,17 @@ extension IncrementalCompilationState { } func reportExplicitDependencyOutOfDate(_ moduleName: String, - outputPath: String, - updatedInputPath: String) { - report("Dependency module \(moduleName) is older than input file \(updatedInputPath) at \(outputPath)") + inputPath: String) { + report("Dependency module \(moduleName) is older than input file \(inputPath)") + } + + func reportExplicitDependencyWillBeReBuilt(_ moduleOutputPath: String, + reason: String) { + report("Dependency module '\(moduleOutputPath)' will be re-built: \(reason)") + } + + func reportExplicitDependencyReBuildSet(_ modules: [ModuleDependencyId]) { + report("Following explicit module dependencies will be re-built: [\(modules.map { $0.moduleNameForDiagnostic }.sorted().joined(separator: ", "))]") } // Emits a remark indicating incremental compilation has been disabled. diff --git a/Sources/SwiftDriver/IncrementalCompilation/IncrementalCompilationState.swift b/Sources/SwiftDriver/IncrementalCompilation/IncrementalCompilationState.swift index 7be7fcc03..709562bb9 100644 --- a/Sources/SwiftDriver/IncrementalCompilation/IncrementalCompilationState.swift +++ b/Sources/SwiftDriver/IncrementalCompilation/IncrementalCompilationState.swift @@ -68,6 +68,7 @@ public final class IncrementalCompilationState { initialState: initialState, jobsInPhases: jobsInPhases, driver: driver, + interModuleDependencyGraph: interModuleDependencyGraph, reporter: reporter) .compute(batchJobFormer: &driver) diff --git a/Sources/SwiftDriver/IncrementalCompilation/IncrementalDependencyAndInputSetup.swift b/Sources/SwiftDriver/IncrementalCompilation/IncrementalDependencyAndInputSetup.swift index 475b9ddbc..e09fd069b 100644 --- a/Sources/SwiftDriver/IncrementalCompilation/IncrementalDependencyAndInputSetup.swift +++ b/Sources/SwiftDriver/IncrementalCompilation/IncrementalDependencyAndInputSetup.swift @@ -131,81 +131,94 @@ extension IncrementalCompilationState.IncrementalDependencyAndInputSetup { return priorInterModuleDependencyGraph } - /// For each direct and transitive module dependency, check if any of the inputs are newer than the output - static func verifyInterModuleDependenciesUpToDate(in graph: InterModuleDependencyGraph, - buildRecordInfo: BuildRecordInfo, - reporter: IncrementalCompilationState.Reporter?) throws -> Bool { + static func verifyModuleDependencyUpToDate(moduleID: ModuleDependencyId, moduleInfo: ModuleInfo, + fileSystem: FileSystem, + reporter: IncrementalCompilationState.Reporter?) throws -> Bool { // Verify that the specified input exists and is older than the specified output let verifyInputOlderThanOutputModTime: (String, VirtualPath, VirtualPath, TimePoint) -> Bool = { moduleName, inputPath, outputPath, outputModTime in guard let inputModTime = - try? buildRecordInfo.fileSystem.lastModificationTime(for: inputPath) else { + try? fileSystem.lastModificationTime(for: inputPath) else { reporter?.report("Unable to 'stat' \(inputPath.description)") return false } if inputModTime > outputModTime { reporter?.reportExplicitDependencyOutOfDate(moduleName, - outputPath: outputPath.description, - updatedInputPath: inputPath.description) + inputPath: inputPath.description) return false } return true } - for module in graph.modules { - switch module.value.details { - case .swift(let swiftDetails): - if module.key.moduleName == graph.mainModuleName { - continue - } - guard let outputModTime = try? buildRecordInfo.fileSystem.lastModificationTime(for: VirtualPath.lookup(module.value.modulePath.path)) else { - reporter?.report("Unable to 'stat' \(module.value.modulePath.description)") + switch moduleInfo.details { + case .swift(let swiftDetails): + guard let outputModTime = try? fileSystem.lastModificationTime(for: VirtualPath.lookup(moduleInfo.modulePath.path)) else { + reporter?.report("Module output not found: '\(moduleID.moduleNameForDiagnostic)'") + return false + } + if let moduleInterfacePath = swiftDetails.moduleInterfacePath { + if !verifyInputOlderThanOutputModTime(moduleID.moduleName, + VirtualPath.lookup(moduleInterfacePath.path), + VirtualPath.lookup(moduleInfo.modulePath.path), + outputModTime) { return false } - if let moduleInterfacePath = swiftDetails.moduleInterfacePath { - if !verifyInputOlderThanOutputModTime(module.key.moduleName, - VirtualPath.lookup(moduleInterfacePath.path), - VirtualPath.lookup(module.value.modulePath.path), - outputModTime) { - return false - } - } - if let bridgingHeaderPath = swiftDetails.bridgingHeaderPath { - if !verifyInputOlderThanOutputModTime(module.key.moduleName, - VirtualPath.lookup(bridgingHeaderPath.path), - VirtualPath.lookup(module.value.modulePath.path), - outputModTime) { - return false - } - } - for bridgingSourceFile in swiftDetails.bridgingSourceFiles ?? [] { - if !verifyInputOlderThanOutputModTime(module.key.moduleName, - VirtualPath.lookup(bridgingSourceFile.path), - VirtualPath.lookup(module.value.modulePath.path), - outputModTime) { - return false - } + } + if let bridgingHeaderPath = swiftDetails.bridgingHeaderPath { + if !verifyInputOlderThanOutputModTime(moduleID.moduleName, + VirtualPath.lookup(bridgingHeaderPath.path), + VirtualPath.lookup(moduleInfo.modulePath.path), + outputModTime) { + return false } - case .clang(_): - guard let outputModTime = try? buildRecordInfo.fileSystem.lastModificationTime(for: VirtualPath.lookup(module.value.modulePath.path)) else { - reporter?.report("Unable to 'stat' \(module.value.modulePath.description)") + } + for bridgingSourceFile in swiftDetails.bridgingSourceFiles ?? [] { + if !verifyInputOlderThanOutputModTime(moduleID.moduleName, + VirtualPath.lookup(bridgingSourceFile.path), + VirtualPath.lookup(moduleInfo.modulePath.path), + outputModTime) { return false } - for inputSourceFile in module.value.sourceFiles ?? [] { - if !verifyInputOlderThanOutputModTime(module.key.moduleName, - try VirtualPath(path: inputSourceFile), - VirtualPath.lookup(module.value.modulePath.path), - outputModTime) { - return false - } + } + case .clang(_): + guard let outputModTime = try? fileSystem.lastModificationTime(for: VirtualPath.lookup(moduleInfo.modulePath.path)) else { + reporter?.report("Module output not found: '\(moduleID.moduleNameForDiagnostic)'") + return false + } + for inputSourceFile in moduleInfo.sourceFiles ?? [] { + if !verifyInputOlderThanOutputModTime(moduleID.moduleName, + try VirtualPath(path: inputSourceFile), + VirtualPath.lookup(moduleInfo.modulePath.path), + outputModTime) { + return false } - case .swiftPrebuiltExternal(_): - // TODO: We have to give-up here until we have a way to verify the timestamp of the binary module. - reporter?.report("Unable to verify binary module dependency: \(module.value.modulePath.description)") - return false; - case .swiftPlaceholder(_): - // TODO: This should never ever happen. Hard error? - return false; + } + case .swiftPrebuiltExternal(_): + // TODO: We have to give-up here until we have a way to verify the timestamp of the binary module. + // We can do better here by knowing if this module hasn't change - which would allows us to not + // invalidate any of the dependencies that depend on it. + reporter?.report("Unable to verify binary module dependency up-to-date: \(moduleID.moduleNameForDiagnostic)") + return false; + case .swiftPlaceholder(_): + // TODO: This should never ever happen. Hard error? + return false; + } + return true + } + + /// For each direct and transitive module dependency, check if any of the inputs are newer than the output + static func verifyInterModuleDependenciesUpToDate(in graph: InterModuleDependencyGraph, + buildRecordInfo: BuildRecordInfo, + reporter: IncrementalCompilationState.Reporter?) throws -> Bool { + for module in graph.modules { + if module.key.moduleName == graph.mainModuleName { + continue + } + if try !verifyModuleDependencyUpToDate(moduleID: module.key, + moduleInfo: module.value, + fileSystem: buildRecordInfo.fileSystem, + reporter: reporter) { + return false } } return true @@ -298,7 +311,7 @@ extension IncrementalCompilationState { return InitialStateForPlanning( graph: priors.graph, buildRecordInfo: buildRecordInfo, - maybeUpToDatePriorInterModuleDependencyGraph: priorInterModuleDependencyGraph, + upToDatePriorInterModuleDependencyGraph: priorInterModuleDependencyGraph, inputsInvalidatedByExternals: priors.fileSet, incrementalOptions: options) } diff --git a/Sources/SwiftDriver/Jobs/Planning.swift b/Sources/SwiftDriver/Jobs/Planning.swift index 5179474d1..bb385c78e 100644 --- a/Sources/SwiftDriver/Jobs/Planning.swift +++ b/Sources/SwiftDriver/Jobs/Planning.swift @@ -142,8 +142,10 @@ extension Driver { let interModuleDependencyGraph: InterModuleDependencyGraph? if parsedOptions.contains(.driverExplicitModuleBuild) || parsedOptions.contains(.explainModuleDependency) { + // If the incremental build record's module dependency graph is up-to-date, we + // can skip dependency scanning entirely. interModuleDependencyGraph = - try initialIncrementalState?.maybeUpToDatePriorInterModuleDependencyGraph ?? + try initialIncrementalState?.upToDatePriorInterModuleDependencyGraph ?? gatherModuleDependencies() } else { interModuleDependencyGraph = nil diff --git a/TestInputs/ExplicitModuleBuilds/Swift/E.swiftinterface b/TestInputs/ExplicitModuleBuilds/Swift/E.swiftinterface index 54c10742e..b80630e6d 100644 --- a/TestInputs/ExplicitModuleBuilds/Swift/E.swiftinterface +++ b/TestInputs/ExplicitModuleBuilds/Swift/E.swiftinterface @@ -2,4 +2,4 @@ // swift-module-flags: -module-name E import Swift public func funcE() -public let modlueEValue = 42 +public let modlueEValue = 42 \ No newline at end of file diff --git a/TestInputs/ExplicitModuleBuilds/Swift/H.swiftinterface b/TestInputs/ExplicitModuleBuilds/Swift/H.swiftinterface new file mode 100644 index 000000000..fdcdb1913 --- /dev/null +++ b/TestInputs/ExplicitModuleBuilds/Swift/H.swiftinterface @@ -0,0 +1,4 @@ +// swift-interface-format-version: 1.0 +// swift-module-flags: -module-name H +import G +public func funcH() { } \ No newline at end of file diff --git a/TestInputs/ExplicitModuleBuilds/Swift/J.swiftinterface b/TestInputs/ExplicitModuleBuilds/Swift/J.swiftinterface new file mode 100644 index 000000000..a92936a41 --- /dev/null +++ b/TestInputs/ExplicitModuleBuilds/Swift/J.swiftinterface @@ -0,0 +1,4 @@ +// swift-interface-format-version: 1.0 +// swift-module-flags: -module-name J +import G +public func funcJ() { } \ No newline at end of file diff --git a/TestInputs/ExplicitModuleBuilds/Swift/R.swiftinterface b/TestInputs/ExplicitModuleBuilds/Swift/R.swiftinterface new file mode 100644 index 000000000..69c169bfd --- /dev/null +++ b/TestInputs/ExplicitModuleBuilds/Swift/R.swiftinterface @@ -0,0 +1,3 @@ +// swift-interface-format-version: 1.0 +// swift-module-flags: -module-name R +import Swift \ No newline at end of file diff --git a/TestInputs/ExplicitModuleBuilds/Swift/T.swiftinterface b/TestInputs/ExplicitModuleBuilds/Swift/T.swiftinterface new file mode 100644 index 000000000..6d0ed9579 --- /dev/null +++ b/TestInputs/ExplicitModuleBuilds/Swift/T.swiftinterface @@ -0,0 +1,5 @@ +// swift-interface-format-version: 1.0 +// swift-module-flags: -module-name T +import R +import J +public func funcC() { } \ No newline at end of file diff --git a/TestInputs/ExplicitModuleBuilds/Swift/Y.swiftinterface b/TestInputs/ExplicitModuleBuilds/Swift/Y.swiftinterface new file mode 100644 index 000000000..8ca76af82 --- /dev/null +++ b/TestInputs/ExplicitModuleBuilds/Swift/Y.swiftinterface @@ -0,0 +1,5 @@ +// swift-interface-format-version: 1.0 +// swift-module-flags: -module-name Y +import H +import R +public func funcB() { } \ No newline at end of file diff --git a/Tests/SwiftDriverTests/IncrementalCompilationTests.swift b/Tests/SwiftDriverTests/IncrementalCompilationTests.swift index 756560ed6..dde19e747 100644 --- a/Tests/SwiftDriverTests/IncrementalCompilationTests.swift +++ b/Tests/SwiftDriverTests/IncrementalCompilationTests.swift @@ -312,9 +312,9 @@ extension IncrementalCompilationTests { reading(deps: "other") fingerprintsChanged("other") fingerprintsMissingOfTopLevelName(name: "bar", "other") - compilingExplicitClangDependency("SwiftShims") - compilingExplicitSwiftDependency("Swift") - compilingExplicitSwiftDependency("SwiftOnoneSupport") + moduleOutputNotFound("E") + moduleWillBeRebuiltOutOfDate("E") + explicitModulesWillBeRebuilt(["E"]) compilingExplicitSwiftDependency("E") skipped("main") schedLinking @@ -356,12 +356,74 @@ extension IncrementalCompilationTests { queuingInitial("main", "other") notSchedulingDependentsUnknownChanges("other") findingBatchingCompiling("main", "other") + explicitDependencyModuleOlderThanInput("E") + moduleWillBeRebuiltOutOfDate("E") + explicitModulesWillBeRebuilt(["E"]) compilingExplicitSwiftDependency("E") - // TODO: We can do better with being more-selective on which dependencies get re-built - // But for now ensure all modules are built. - compilingExplicitClangDependency("SwiftShims") - compilingExplicitSwiftDependency("Swift") - compilingExplicitSwiftDependency("SwiftOnoneSupport") + } + } + + // A dependency has changed one of its inputs, ensure + // other modules that depend on it are invalidated also. + // + // test + // / \ + // Y T + // / \ / \ + // H R J + // \ \ + // \-------G + // + // On this graph, inputs of 'G' are updated, causing it to be re-built + // as well as all modules on paths from root to it: 'Y', 'H', 'T','J' + func testExplicitIncrementalBuildChangedDependencyInvalidatesUpstreamDependencies() throws { + // Add an import of 'B', 'C' to make sure followup changes has consistent inputs + replace(contentsOf: "other", with: "import Y;import T") + try buildInitialState(checkDiagnostics: false, explicitModuleBuild: true) + + // Just update the time-stamp of one of the module dependencies + touch(try AbsolutePath(validating: explicitSwiftDependenciesPath.appending(component: "G.swiftinterface").pathString)) + + // Changing a dependency will mean that we both re-run the dependency scan, + // and also ensure that all source-files are re-built with a non-cascading build + // since the source files themselves have not changed. + try doABuild( + "update dependency (G) interface timestamp", + checkDiagnostics: true, + extraArguments: explicitBuildArgs, + whenAutolinking: autolinkLifecycleExpectedDiags + ) { + readGraph + enablingCrossModule + readInterModuleGraph + // Ensure the above 'touch' is detected and causes a re-scan + explicitDependencyModuleOlderThanInput("G") + explicitMustReScanDueToChangedDependencyInput + noFingerprintInSwiftModule("G.swiftinterface") + dependencyNewerThanNode("G.swiftinterface") + dependencyNewerThanNode("G.swiftinterface") // FIXME: Why do we see this twice? + reading(deps: "main") + reading(deps: "other") + fingerprintsMissingOfTopLevelName(name: "foo", "main") + maySkip("main") + maySkip("other") + invalidatedExternally("main", "other") + queuingInitial("main", "other") + findingBatchingCompiling("main", "other") + explicitDependencyModuleOlderThanInput("G") + explicitDependencyInvalidatedDownstream("J") + explicitDependencyInvalidatedDownstream("T") + explicitDependencyInvalidatedDownstream("Y") + explicitDependencyInvalidatedDownstream("H") + explicitModulesWillBeRebuilt(["G", "H", "J", "T", "Y"]) + moduleWillBeRebuiltOutOfDate("G") + compilingExplicitSwiftDependency("G") + compilingExplicitSwiftDependency("J") + compilingExplicitSwiftDependency("T") + compilingExplicitSwiftDependency("Y") + compilingExplicitSwiftDependency("H") + schedulingPostCompileJobs + linking } } } @@ -1568,6 +1630,18 @@ extension DiagVerifiable { startCompilingExplicitSwiftDependency(dependencyModuleName) finishCompilingExplicitSwiftDependency(dependencyModuleName) } + @DiagsBuilder func moduleOutputNotFound(_ moduleName: String) -> [Diagnostic.Message] { + "Incremental compilation: Module output not found: '\(moduleName)'" + } + @DiagsBuilder func moduleWillBeRebuiltOutOfDate(_ moduleName: String) -> [Diagnostic.Message] { + "Incremental compilation: Dependency module '\(moduleName)' will be re-built: Out-of-date" + } + @DiagsBuilder func explicitModulesWillBeRebuilt(_ moduleNames: [String]) -> [Diagnostic.Message] { + "Incremental compilation: Following explicit module dependencies will be re-built: [\(moduleNames.joined(separator: ", "))]" + } + @DiagsBuilder func explicitDependencyInvalidatedDownstream(_ moduleName: String) -> [Diagnostic.Message] { + "Incremental compilation: Dependency module '\(moduleName)' will be re-built: Invalidated by downstream dependency" + } // MARK: - misc @DiagsBuilder var enablingCrossModule: [Diagnostic.Message] {