Skip to content

Commit 4e62341

Browse files
committed
source file hashing for incremental
1 parent 0c8711e commit 4e62341

File tree

16 files changed

+96
-62
lines changed

16 files changed

+96
-62
lines changed

Sources/SwiftDriver/Driver/Driver.swift

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import struct TSCBasic.ByteString
2424
import struct TSCBasic.Diagnostic
2525
import struct TSCBasic.FileInfo
2626
import struct TSCBasic.RelativePath
27+
import struct TSCBasic.SHA256
2728
import var TSCBasic.localFileSystem
2829
import var TSCBasic.stderrStream
2930
import var TSCBasic.stdoutStream
@@ -44,6 +45,14 @@ extension Driver.ErrorDiagnostics: CustomStringConvertible {
4445
}
4546
}
4647

48+
public struct FileMetadata {
49+
public let mTime: TimePoint
50+
public let hash: String
51+
init(mTime: TimePoint, hash: String = "") {
52+
self.mTime = mTime
53+
self.hash = hash
54+
}
55+
}
4756

4857
/// The Swift driver.
4958
public struct Driver {
@@ -209,8 +218,8 @@ public struct Driver {
209218
/// The set of input files
210219
@_spi(Testing) public let inputFiles: [TypedVirtualPath]
211220

212-
/// The last time each input file was modified, recorded at the start of the build.
213-
@_spi(Testing) public let recordedInputModificationDates: [TypedVirtualPath: TimePoint]
221+
/// The last time each input file was modified, and the file's SHA256 hash, recorded at the start of the build.
222+
@_spi(Testing) public let recordedInputMetadata: [TypedVirtualPath: FileMetadata]
214223

215224
/// The mapping from input files to output files for each kind.
216225
let outputFileMap: OutputFileMap?
@@ -978,11 +987,20 @@ public struct Driver {
978987
// Classify and collect all of the input files.
979988
let inputFiles = try Self.collectInputFiles(&self.parsedOptions, diagnosticsEngine: diagnosticsEngine, fileSystem: self.fileSystem)
980989
self.inputFiles = inputFiles
981-
self.recordedInputModificationDates = .init(uniqueKeysWithValues:
982-
Set(inputFiles).compactMap {
983-
guard let modTime = try? fileSystem
984-
.lastModificationTime(for: $0.file) else { return nil }
985-
return ($0, modTime)
990+
991+
let incrementalFileHashes = parsedOptions.hasFlag(positive: .enableIncrementalFileHashing,
992+
negative: .disableIncrementalFileHashing,
993+
default: false)
994+
self.recordedInputMetadata = .init(uniqueKeysWithValues:
995+
Set(inputFiles).compactMap { inputFile -> (TypedVirtualPath, FileMetadata)? in
996+
guard let modTime = try? fileSystem.lastModificationTime(for: inputFile.file) else { return nil }
997+
guard let data = try? fileSystem.readFileContents(inputFile.file) else { return nil }
998+
if incrementalFileHashes {
999+
let hash = SHA256().hash(data).hexadecimalRepresentation
1000+
return (inputFile, FileMetadata(mTime: modTime, hash: hash))
1001+
} else {
1002+
return (inputFile, FileMetadata(mTime: modTime))
1003+
}
9861004
})
9871005

9881006
do {
@@ -1101,7 +1119,7 @@ public struct Driver {
11011119
outputFileMap: outputFileMap,
11021120
incremental: self.shouldAttemptIncrementalCompilation,
11031121
parsedOptions: parsedOptions,
1104-
recordedInputModificationDates: recordedInputModificationDates)
1122+
recordedInputMetadata: recordedInputMetadata)
11051123

11061124
self.supportedFrontendFlags =
11071125
try Self.computeSupportedCompilerArgs(of: self.toolchain,
@@ -1949,7 +1967,7 @@ extension Driver {
19491967
}
19501968
try executor.execute(job: inPlaceJob,
19511969
forceResponseFiles: forceResponseFiles,
1952-
recordedInputModificationDates: recordedInputModificationDates)
1970+
recordedInputMetadata: recordedInputMetadata)
19531971
}
19541972

19551973
// If requested, warn for options that weren't used by the driver after the build is finished.
@@ -1994,7 +2012,7 @@ extension Driver {
19942012
delegate: jobExecutionDelegate,
19952013
numParallelJobs: numParallelJobs ?? 1,
19962014
forceResponseFiles: forceResponseFiles,
1997-
recordedInputModificationDates: recordedInputModificationDates)
2015+
recordedInputMetadata: recordedInputMetadata)
19982016
}
19992017

20002018
public func writeIncrementalBuildInformation(_ jobs: [Job]) {

Sources/SwiftDriver/Execution/DriverExecutor.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public protocol DriverExecutor {
2525
@discardableResult
2626
func execute(job: Job,
2727
forceResponseFiles: Bool,
28-
recordedInputModificationDates: [TypedVirtualPath: TimePoint]) throws -> ProcessResult
28+
recordedInputMetadata: [TypedVirtualPath: FileMetadata]) throws -> ProcessResult
2929

3030
/// Execute multiple jobs, tracking job status using the provided execution delegate.
3131
/// Pass in the `IncrementalCompilationState` to allow for incremental compilation.
@@ -34,15 +34,15 @@ public protocol DriverExecutor {
3434
delegate: JobExecutionDelegate,
3535
numParallelJobs: Int,
3636
forceResponseFiles: Bool,
37-
recordedInputModificationDates: [TypedVirtualPath: TimePoint]
37+
recordedInputMetadata: [TypedVirtualPath: FileMetadata]
3838
) throws
3939

4040
/// Execute multiple jobs, tracking job status using the provided execution delegate.
4141
func execute(jobs: [Job],
4242
delegate: JobExecutionDelegate,
4343
numParallelJobs: Int,
4444
forceResponseFiles: Bool,
45-
recordedInputModificationDates: [TypedVirtualPath: TimePoint]
45+
recordedInputMetadata: [TypedVirtualPath: FileMetadata]
4646
) throws
4747

4848
/// Launch a process with the given command line and report the result.
@@ -96,10 +96,10 @@ extension DriverExecutor {
9696
func execute<T: Decodable>(job: Job,
9797
capturingJSONOutputAs outputType: T.Type,
9898
forceResponseFiles: Bool,
99-
recordedInputModificationDates: [TypedVirtualPath: TimePoint]) throws -> T {
99+
recordedInputMetadata: [TypedVirtualPath: FileMetadata]) throws -> T {
100100
let result = try execute(job: job,
101101
forceResponseFiles: forceResponseFiles,
102-
recordedInputModificationDates: recordedInputModificationDates)
102+
recordedInputMetadata: recordedInputMetadata)
103103

104104
if (result.exitStatus != .terminated(code: EXIT_SUCCESS)) {
105105
let returnCode = Self.computeReturnCode(exitStatus: result.exitStatus)
@@ -121,14 +121,14 @@ extension DriverExecutor {
121121
delegate: JobExecutionDelegate,
122122
numParallelJobs: Int,
123123
forceResponseFiles: Bool,
124-
recordedInputModificationDates: [TypedVirtualPath: TimePoint]
124+
recordedInputMetadata: [TypedVirtualPath: FileMetadata]
125125
) throws {
126126
try execute(
127127
workload: .all(jobs),
128128
delegate: delegate,
129129
numParallelJobs: numParallelJobs,
130130
forceResponseFiles: forceResponseFiles,
131-
recordedInputModificationDates: recordedInputModificationDates)
131+
recordedInputMetadata: recordedInputMetadata)
132132
}
133133

134134
static func computeReturnCode(exitStatus: ProcessResult.ExitStatus) -> Int {

Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ public extension Driver {
227227
try self.executor.execute(job: preScanJob,
228228
capturingJSONOutputAs: InterModuleDependencyImports.self,
229229
forceResponseFiles: forceResponseFiles,
230-
recordedInputModificationDates: recordedInputModificationDates)
230+
recordedInputMetadata: recordedInputMetadata)
231231
}
232232
return imports
233233
}
@@ -304,7 +304,7 @@ public extension Driver {
304304
try self.executor.execute(job: scannerJob,
305305
capturingJSONOutputAs: InterModuleDependencyGraph.self,
306306
forceResponseFiles: forceResponseFiles,
307-
recordedInputModificationDates: recordedInputModificationDates)
307+
recordedInputMetadata: recordedInputMetadata)
308308
}
309309
return dependencyGraph
310310
}

Sources/SwiftDriver/IncrementalCompilation/BuildRecord.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ extension BuildRecord {
4646
init(jobs: [Job],
4747
finishedJobResults: [BuildRecordInfo.JobResult],
4848
skippedInputs: Set<TypedVirtualPath>?,
49-
compilationInputModificationDates: [TypedVirtualPath: TimePoint],
49+
compilationInputModificationDates: [TypedVirtualPath: FileMetadata],
5050
actualSwiftVersion: String,
5151
argsHash: String,
5252
timeBeforeFirstJob: TimePoint,
@@ -57,10 +57,10 @@ extension BuildRecord {
5757
entry.job.inputsGeneratingCode.map { ($0, entry.result) }
5858
})
5959
let inputInfosArray = compilationInputModificationDates
60-
.map { input, modDate -> (VirtualPath, InputInfo) in
60+
.map { input, metadata -> (VirtualPath, InputInfo) in
6161
let status = InputInfo.Status( wasSkipped: skippedInputs?.contains(input),
6262
jobResult: jobResultsByInput[input])
63-
return (input.file, InputInfo(status: status, previousModTime: modDate))
63+
return (input.file, InputInfo(status: status, previousModTime: metadata.mTime, hash: metadata.hash))
6464
}
6565

6666
self.init(

Sources/SwiftDriver/IncrementalCompilation/BuildRecordInfo.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import class Dispatch.DispatchQueue
4848
@_spi(Testing) public let actualSwiftVersion: String
4949
@_spi(Testing) public let timeBeforeFirstJob: TimePoint
5050
let diagnosticEngine: DiagnosticsEngine
51-
let compilationInputModificationDates: [TypedVirtualPath: TimePoint]
51+
let compilationInputModificationDates: [TypedVirtualPath: FileMetadata]
5252
private var explicitModuleDependencyGraph: InterModuleDependencyGraph? = nil
5353

5454
private var finishedJobResults = [JobResult]()
@@ -64,7 +64,7 @@ import class Dispatch.DispatchQueue
6464
actualSwiftVersion: String,
6565
timeBeforeFirstJob: TimePoint,
6666
diagnosticEngine: DiagnosticsEngine,
67-
compilationInputModificationDates: [TypedVirtualPath: TimePoint])
67+
compilationInputModificationDates: [TypedVirtualPath: FileMetadata])
6868
{
6969
self.buildRecordPath = buildRecordPath
7070
self.fileSystem = fileSystem
@@ -85,7 +85,7 @@ import class Dispatch.DispatchQueue
8585
outputFileMap: OutputFileMap?,
8686
incremental: Bool,
8787
parsedOptions: ParsedOptions,
88-
recordedInputModificationDates: [TypedVirtualPath: TimePoint]
88+
recordedInputMetadata: [TypedVirtualPath: FileMetadata]
8989
) {
9090
// Cannot write a buildRecord without a path.
9191
guard let buildRecordPath = try? Self.computeBuildRecordPath(
@@ -99,7 +99,7 @@ import class Dispatch.DispatchQueue
9999
}
100100
let currentArgsHash = BuildRecordArguments.computeHash(parsedOptions)
101101
let compilationInputModificationDates =
102-
recordedInputModificationDates.filter { input, _ in
102+
recordedInputMetadata.filter { input, _ in
103103
input.type.isPartOfSwiftCompilation
104104
}
105105

Sources/SwiftDriver/IncrementalCompilation/FirstWaveComputer.swift

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ extension IncrementalCompilationState {
2626
let showJobLifecycle: Bool
2727
let alwaysRebuildDependents: Bool
2828
let interModuleDependencyGraph: InterModuleDependencyGraph?
29+
let useHashes: Bool
2930
/// If non-null outputs information for `-driver-show-incremental` for input path
3031
private let reporter: Reporter?
3132

@@ -45,6 +46,7 @@ extension IncrementalCompilationState {
4546
self.showJobLifecycle = driver.showJobLifecycle
4647
self.alwaysRebuildDependents = initialState.incrementalOptions.contains(
4748
.alwaysRebuildDependents)
49+
self.useHashes = initialState.incrementalOptions.contains(.useFileHashesInModuleDependencyGraph)
4850
self.interModuleDependencyGraph = interModuleDependencyGraph
4951
self.reporter = reporter
5052
}
@@ -301,8 +303,9 @@ extension IncrementalCompilationState.FirstWaveComputer {
301303
/// The status of the input file.
302304
let status: InputInfo.Status
303305
/// If `true`, the modification time of this input matches the modification
304-
/// time recorded from the prior build in the build record.
305-
let datesMatch: Bool
306+
/// time recorded from the prior build in the build record, or the hash of
307+
/// its contents match.
308+
let metadataMatch: Bool
306309
}
307310

308311
// Find the inputs that have changed since last compilation, or were marked as needed a build
@@ -311,16 +314,19 @@ extension IncrementalCompilationState.FirstWaveComputer {
311314
) -> [ChangedInput] {
312315
jobsInPhases.compileJobs.compactMap { job in
313316
let input = job.primaryInputs[0]
314-
let modDate = buildRecordInfo.compilationInputModificationDates[input] ?? .distantFuture
317+
let metadata = buildRecordInfo.compilationInputModificationDates[input] ?? FileMetadata(mTime: .distantFuture)
315318
let inputInfo = outOfDateBuildRecord.inputInfos[input.file]
316319
let previousCompilationStatus = inputInfo?.status ?? .newlyAdded
317320
let previousModTime = inputInfo?.previousModTime
321+
let previousHash = inputInfo?.hash
318322

319323
switch previousCompilationStatus {
320-
case .upToDate where modDate == previousModTime:
324+
case .upToDate where metadata.mTime == previousModTime:
321325
reporter?.report("May skip current input:", input)
322326
return nil
323-
327+
case .upToDate where useHashes && (metadata.hash == previousHash):
328+
reporter?.report("May skip current input (identical hash):", input)
329+
return nil
324330
case .upToDate:
325331
reporter?.report("Scheduling changed input", input)
326332
case .newlyAdded:
@@ -330,9 +336,10 @@ extension IncrementalCompilationState.FirstWaveComputer {
330336
case .needsNonCascadingBuild:
331337
reporter?.report("Scheduling noncascading build", input)
332338
}
339+
let metadataMatch = metadata.mTime == previousModTime || (useHashes && metadata.hash == previousHash)
333340
return ChangedInput(typedFile: input,
334341
status: previousCompilationStatus,
335-
datesMatch: modDate == previousModTime)
342+
metadataMatch: metadataMatch )
336343
}
337344
}
338345

@@ -381,7 +388,7 @@ extension IncrementalCompilationState.FirstWaveComputer {
381388
) -> [TypedVirtualPath] {
382389
changedInputs.compactMap { changedInput in
383390
let inputIsUpToDate =
384-
changedInput.datesMatch && !inputsMissingOutputs.contains(changedInput.typedFile)
391+
changedInput.metadataMatch && !inputsMissingOutputs.contains(changedInput.typedFile)
385392
let basename = changedInput.typedFile.file.basename
386393

387394
// If we're asked to always rebuild dependents, all we need to do is

Sources/SwiftDriver/IncrementalCompilation/InputInfo.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import struct TSCBasic.ProcessResult
14+
import struct TSCBasic.SHA256
1415

1516
/// Contains information about the current status of an input to the incremental
1617
/// build.
@@ -25,9 +26,12 @@ import struct TSCBasic.ProcessResult
2526
/// The last known modification time of this input.
2627
/*@_spi(Testing)*/ public let previousModTime: TimePoint
2728

28-
/*@_spi(Testing)*/ public init(status: Status, previousModTime: TimePoint) {
29+
/*@_spi(Testing)*/ public let hash: String
30+
31+
/*@_spi(Testing)*/ public init(status: Status, previousModTime: TimePoint, hash: String) {
2932
self.status = status
3033
self.previousModTime = previousModTime
34+
self.hash = hash
3135
}
3236
}
3337

Sources/SwiftDriver/IncrementalCompilation/ModuleDependencyGraph.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,7 @@ extension ModuleDependencyGraph {
923923
case .inputInfo:
924924
guard
925925
record.fields.count == 5,
926+
case .blob(let hashBlob) = record.payload,
926927
let path = try nonemptyInternedString(field: 4)
927928
else {
928929
throw malformedError
@@ -931,12 +932,14 @@ extension ModuleDependencyGraph {
931932
lower: UInt32(record.fields[0]),
932933
upper: UInt32(record.fields[1]),
933934
nanoseconds: UInt32(record.fields[2]))
935+
934936
let status = try InputInfo.Status(code: UInt32(record.fields[3]))
935937
let pathString = path.lookup(in: internedStringTable)
936938
let pathHandle = try VirtualPath.intern(path: pathString)
939+
let hash = String(decoding: hashBlob, as: UTF8.self)
937940
self.inputInfos[VirtualPath.lookup(pathHandle)] = InputInfo(
938941
status: status,
939-
previousModTime: modTime)
942+
previousModTime: modTime, hash: hash)
940943
case .moduleDepGraphNode:
941944
guard record.fields.count == 6 else {
942945
throw malformedError
@@ -1162,12 +1165,12 @@ extension ModuleDependencyGraph {
11621165
let inputID = input.name.intern(in: self.internedStringTable)
11631166
let pathID = self.lookupIdentifierCode(for: inputID)
11641167

1165-
self.stream.writeRecord(self.abbreviations[.inputInfo]!) {
1168+
self.stream.writeRecord(self.abbreviations[.inputInfo]!, {
11661169
$0.append(RecordID.inputInfo)
11671170
$0.append(inputInfo.previousModTime)
11681171
$0.append(inputInfo.status.code)
11691172
$0.append(pathID)
1170-
}
1173+
}, blob: inputInfo.hash)
11711174
}
11721175
}
11731176

@@ -1254,6 +1257,8 @@ extension ModuleDependencyGraph {
12541257
.fixed(bitWidth: 3),
12551258
// path ID
12561259
.vbr(chunkBitWidth: 13),
1260+
// file hash
1261+
.blob,
12571262
])
12581263
self.abbreviate(.moduleDepGraphNode,
12591264
[Bitstream.Abbreviation.Operand.literal(RecordID.moduleDepGraphNode.rawValue)] +

Sources/SwiftDriver/Jobs/EmitSupportedFeaturesJob.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ extension Driver {
8585
job: frontendFeaturesJob,
8686
capturingJSONOutputAs: SupportedCompilerFeatures.self,
8787
forceResponseFiles: false,
88-
recordedInputModificationDates: [:]).SupportedArguments
88+
recordedInputMetadata: [:]).SupportedArguments
8989
return Set(decodedSupportedFlagList)
9090
}
9191

Sources/SwiftDriver/Jobs/Job.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,9 @@ extension Job {
152152
}
153153
}
154154

155-
public func verifyInputsNotModified(since recordedInputModificationDates: [TypedVirtualPath: TimePoint], fileSystem: FileSystem) throws {
155+
public func verifyInputsNotModified(since recordedInputMetadata: [TypedVirtualPath: TimePoint], fileSystem: FileSystem) throws {
156156
for input in inputs {
157-
if let recordedModificationTime = recordedInputModificationDates[input],
157+
if let recordedModificationTime = recordedInputMetadata[input],
158158
try fileSystem.lastModificationTime(for: input.file) != recordedModificationTime {
159159
throw InputError.inputUnexpectedlyModified(input)
160160
}

0 commit comments

Comments
 (0)