From ff727b47a83c258620f5302d06abf908d673e378 Mon Sep 17 00:00:00 2001 From: Ben Blackburne Date: Wed, 22 Oct 2025 15:35:31 +0100 Subject: [PATCH 1/2] Use Hashes for swiftmodule incremental compilation I missed this in my earlier work on using hashes for Swift Incremental compilation. --- .../FirstWaveComputer.swift | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftDriver/IncrementalCompilation/FirstWaveComputer.swift b/Sources/SwiftDriver/IncrementalCompilation/FirstWaveComputer.swift index a8a1147d4..0e1b30cfd 100644 --- a/Sources/SwiftDriver/IncrementalCompilation/FirstWaveComputer.swift +++ b/Sources/SwiftDriver/IncrementalCompilation/FirstWaveComputer.swift @@ -202,9 +202,41 @@ extension IncrementalCompilationState.FirstWaveComputer { return false } - // Ensure that no output is older than any of the inputs - let oldestOutputModTime: TimePoint = try emitModuleJob.outputs.map { try fileSystem.lastModificationTime(for: $0.file) }.min() ?? .distantPast - return try emitModuleJob.inputs.swiftSourceFiles.allSatisfy({ try fileSystem.lastModificationTime(for: $0.typedFile.file) < oldestOutputModTime }) + // If all the modules are older than the outputs, we can skip + let oldestOutputModTime = try emitModuleJob.outputs.map { try fileSystem.lastModificationTime(for: $0.file) }.min() ?? .distantPast + let areModulesOlderThanOutput = try emitModuleJob.inputs.swiftSourceFiles.allSatisfy({ try fileSystem.lastModificationTime(for: $0.typedFile.file) < oldestOutputModTime }) + guard !areModulesOlderThanOutput else { + return true + } + // If we are not using hashes, we cannot skip + guard useHashes else { + return false + } + let inputs = emitModuleJob.inputs + for input in inputs { + guard let currentDate = buildRecordInfo.compilationInputModificationDates[input] else { + reporter?.report("Missing file metadata for: \(input)") + return false + } + + guard let currentHash = currentDate.hash else { + reporter?.report("Missing file hash data for: \(input)") + return false + } + + let inputInfos = buildRecord.inputInfos + guard let inputInfo = inputInfos[input.file] else { + reporter?.report("Missing incremental info for: \(input)") + return false + } + + if currentHash != inputInfo.hash { + reporter?.report("Changed hash for: \(input)") + return false + } + } + return true + } /// Figure out which compilation inputs are *not* mandatory at the start From 1617fd2c12fa9b6d87637b2be5d6816a0709fa39 Mon Sep 17 00:00:00 2001 From: Ben Blackburne Date: Tue, 2 Dec 2025 10:00:41 +0000 Subject: [PATCH 2/2] Test Swiftmodules are not unnecessarily rebuilt --- .../IncrementalCompilationTests.swift | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Tests/SwiftDriverTests/IncrementalCompilationTests.swift b/Tests/SwiftDriverTests/IncrementalCompilationTests.swift index e6d1a495c..6cbfd69a5 100644 --- a/Tests/SwiftDriverTests/IncrementalCompilationTests.swift +++ b/Tests/SwiftDriverTests/IncrementalCompilationTests.swift @@ -360,6 +360,35 @@ extension IncrementalCompilationTests { XCTAssertFalse(mandatoryJobInputs.contains("other.swift")) } + // Source file timestamps updated but contents are the same, with file-hashing emit-module job should be skipped + func testNullBuildNoEmitModuleWithHashing() throws { + let extraArguments = ["-experimental-emit-module-separately", "-emit-module", "-enable-incremental-file-hashing"] + try buildInitialState(extraArguments: extraArguments) + touch("main") + touch("other") + touch(try AbsolutePath(validating: explicitSwiftDependenciesPath.appending(component: "E.swiftinterface").pathString)) + let driver = try doABuildWithoutExpectations(arguments: commonArgs + extraArguments + (try XCTUnwrap(Driver.sdkArgumentsForTesting()))) + let mandatoryJobs = try XCTUnwrap(driver.incrementalCompilationState?.mandatoryJobsInOrder) + let mandatoryJobInputs = mandatoryJobs.flatMap { $0.inputs }.map { $0.file.basename } + XCTAssertFalse(mandatoryJobs.contains { $0.kind == .emitModule }, "emit-module should be skipped when using hashes and content unchanged") + XCTAssertFalse(mandatoryJobInputs.contains("main.swift")) + XCTAssertFalse(mandatoryJobInputs.contains("other.swift")) + } + + // Source file updated, emit-module job should not be skipped regardless of file-hashing + func testEmitModuleWithHashingWhenContentChanges() throws { + let extraArguments = ["-experimental-emit-module-separately", "-emit-module", "-enable-incremental-file-hashing"] + try buildInitialState(extraArguments: extraArguments) + replace(contentsOf: "main", with: "let foo = 2") + let driver = try doABuildWithoutExpectations(arguments: commonArgs + extraArguments + (try XCTUnwrap(Driver.sdkArgumentsForTesting()))) + let mandatoryJobs = try XCTUnwrap(driver.incrementalCompilationState?.mandatoryJobsInOrder) + let mandatoryJobInputs = mandatoryJobs.flatMap { $0.inputs }.map { $0.file.basename } + XCTAssertTrue(mandatoryJobs.contains { $0.kind == .emitModule }, "emit-module should run when using hashes and content has changed") + XCTAssertTrue(mandatoryJobInputs.contains("main.swift")) + } + + + // External deps timestamp updated but contents are the same, and file-hashing is explicitly disabled func testExplicitIncrementalBuildExternalDepsWithoutHashing() throws { replace(contentsOf: "other", with: "import E;let bar = foo")