Skip to content

Commit c6d3b39

Browse files
authored
Merge pull request #65 from owenv/modified-during-build
Diagnose when inputs are modified during the build
2 parents a82c351 + 6e3d9d5 commit c6d3b39

File tree

4 files changed

+119
-4
lines changed

4 files changed

+119
-4
lines changed

Sources/SwiftDriver/Driver/Driver.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212
import TSCBasic
1313
import TSCUtility
14+
import Foundation
1415

1516
/// How should the Swift module output be handled?
1617
public enum ModuleOutput: Equatable {
@@ -79,6 +80,9 @@ public struct Driver {
7980
/// The set of input files
8081
public let inputFiles: [TypedVirtualPath]
8182

83+
/// The last time each input file was modified, recorded at the start of the build.
84+
public let recordedInputModificationDates: [TypedVirtualPath: Date]
85+
8286
/// The mapping from input files to output files for each kind.
8387
internal let outputFileMap: OutputFileMap?
8488

@@ -246,6 +250,14 @@ public struct Driver {
246250
// Classify and collect all of the input files.
247251
let inputFiles = try Self.collectInputFiles(&self.parsedOptions)
248252
self.inputFiles = inputFiles
253+
self.recordedInputModificationDates = .init(uniqueKeysWithValues:
254+
Set(inputFiles).compactMap {
255+
if case .absolute(let absolutePath) = $0.file,
256+
let modTime = try? localFileSystem.getFileInfo(absolutePath).modTime {
257+
return ($0, modTime)
258+
}
259+
return nil
260+
})
249261

250262
// Initialize an empty output file map, which will be populated when we start creating jobs.
251263
if let outputFileMapArg = parsedOptions.getLastArgument(.outputFileMap)?.asSingle {
@@ -619,7 +631,8 @@ extension Driver {
619631
executorDelegate: executorDelegate,
620632
numParallelJobs: numParallelJobs,
621633
processSet: processSet,
622-
forceResponseFiles: forceResponseFiles
634+
forceResponseFiles: forceResponseFiles,
635+
recordedInputModificationDates: recordedInputModificationDates
623636
)
624637
try jobExecutor.execute(env: env)
625638
}
@@ -645,6 +658,8 @@ extension Driver {
645658
try ProcessEnv.setVar(envVar, value: value)
646659
}
647660

661+
try job.verifyInputsNotModified(since: self.recordedInputModificationDates)
662+
648663
return try exec(path: arguments[0], args: arguments)
649664
}
650665

Sources/SwiftDriver/Execution/JobExecutor.swift

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,18 @@ public final class JobExecutor {
138138
/// If true, always use response files to pass command line arguments.
139139
let forceResponseFiles: Bool
140140

141+
/// The last time each input file was modified, recorded at the start of the build.
142+
public let recordedInputModificationDates: [TypedVirtualPath: Date]
143+
141144
init(
142145
argsResolver: ArgsResolver,
143146
env: [String: String],
144147
producerMap: [VirtualPath: Job],
145148
executorDelegate: JobExecutorDelegate,
146149
jobQueue: OperationQueue,
147150
processSet: ProcessSet?,
148-
forceResponseFiles: Bool
151+
forceResponseFiles: Bool,
152+
recordedInputModificationDates: [TypedVirtualPath: Date]
149153
) {
150154
self.producerMap = producerMap
151155
self.argsResolver = argsResolver
@@ -154,6 +158,7 @@ public final class JobExecutor {
154158
self.jobQueue = jobQueue
155159
self.processSet = processSet
156160
self.forceResponseFiles = forceResponseFiles
161+
self.recordedInputModificationDates = recordedInputModificationDates
157162
}
158163
}
159164

@@ -175,20 +180,25 @@ public final class JobExecutor {
175180
/// If true, always use response files to pass command line arguments.
176181
let forceResponseFiles: Bool
177182

183+
/// The last time each input file was modified, recorded at the start of the build.
184+
public let recordedInputModificationDates: [TypedVirtualPath: Date]
185+
178186
public init(
179187
jobs: [Job],
180188
resolver: ArgsResolver,
181189
executorDelegate: JobExecutorDelegate,
182190
numParallelJobs: Int? = nil,
183191
processSet: ProcessSet? = nil,
184-
forceResponseFiles: Bool = false
192+
forceResponseFiles: Bool = false,
193+
recordedInputModificationDates: [TypedVirtualPath: Date] = [:]
185194
) {
186195
self.jobs = jobs
187196
self.argsResolver = resolver
188197
self.executorDelegate = executorDelegate
189198
self.numParallelJobs = numParallelJobs ?? 1
190199
self.processSet = processSet
191200
self.forceResponseFiles = forceResponseFiles
201+
self.recordedInputModificationDates = recordedInputModificationDates
192202
}
193203

194204
/// Execute all jobs.
@@ -227,7 +237,8 @@ public final class JobExecutor {
227237
executorDelegate: executorDelegate,
228238
jobQueue: jobQueue,
229239
processSet: processSet,
230-
forceResponseFiles: forceResponseFiles
240+
forceResponseFiles: forceResponseFiles,
241+
recordedInputModificationDates: recordedInputModificationDates
231242
)
232243
}
233244
}
@@ -382,6 +393,8 @@ class ExecuteJobRule: LLBuildRule {
382393
let arguments: [String] = try resolver.resolveArgumentList(for: job,
383394
forceResponseFiles: context.forceResponseFiles)
384395

396+
try job.verifyInputsNotModified(since: context.recordedInputModificationDates)
397+
385398
let process = try context.executorDelegate.launchProcess(
386399
for: job, arguments: arguments, env: env
387400
)

Sources/SwiftDriver/Jobs/Job.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212
import TSCBasic
13+
import Foundation
1314

1415
/// A job represents an individual subprocess that should be invoked during compilation.
1516
public struct Job: Codable, Equatable, Hashable {
@@ -89,6 +90,29 @@ public struct Job: Codable, Equatable, Hashable {
8990
}
9091
}
9192

93+
extension Job {
94+
public enum InputError: Error, Equatable, DiagnosticData {
95+
case inputUnexpectedlyModified(TypedVirtualPath)
96+
97+
public var description: String {
98+
switch self {
99+
case .inputUnexpectedlyModified(let input):
100+
return "input file '\(input.file.name)' was modified during the build"
101+
}
102+
}
103+
}
104+
105+
public func verifyInputsNotModified(since recordedInputModificationDates: [TypedVirtualPath: Date]) throws {
106+
for input in inputs {
107+
if case .absolute(let absolutePath) = input.file,
108+
let recordedModificationTime = recordedInputModificationDates[input],
109+
try localFileSystem.getFileInfo(absolutePath).modTime != recordedModificationTime {
110+
throw InputError.inputUnexpectedlyModified(input)
111+
}
112+
}
113+
}
114+
}
115+
92116
// MARK: - Job.ArgTemplate + Codable
93117

94118
extension Job.ArgTemplate: Codable {

Tests/SwiftDriverTests/JobExecutorTests.swift

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212
import XCTest
1313
import TSCBasic
14+
import TSCUtility
1415

1516
import SwiftDriver
1617

@@ -206,4 +207,66 @@ final class JobExecutorTests: XCTestCase {
206207
let unixOverriddenSwiftPath = try GenericUnixToolchain(env: env).getToolPath(.swiftCompiler)
207208
XCTAssertEqual(unixOverriddenSwiftPath, AbsolutePath(dummyPath))
208209
}
210+
211+
func testInputModifiedDuringSingleJobBuild() throws {
212+
try withTemporaryDirectory { path in
213+
let main = path.appending(component: "main.swift")
214+
try localFileSystem.writeFileContents(main) {
215+
$0 <<< "let foo = 1"
216+
}
217+
218+
var driver = try Driver(args: ["swift", main.pathString])
219+
let jobs = try driver.planBuild()
220+
XCTAssertTrue(jobs.count == 1 && jobs[0].requiresInPlaceExecution)
221+
let resolver = try ArgsResolver()
222+
223+
// Change the file
224+
try localFileSystem.writeFileContents(main) {
225+
$0 <<< "let foo = 1"
226+
}
227+
228+
let delegate = JobCollectingDelegate()
229+
delegate.useStubProcess = true
230+
XCTAssertThrowsError(try driver.run(jobs: jobs, resolver: resolver,
231+
executorDelegate: delegate)) {
232+
XCTAssertEqual($0 as? Job.InputError,
233+
.inputUnexpectedlyModified(TypedVirtualPath(file: .absolute(main), type: .swift)))
234+
}
235+
236+
}
237+
}
238+
239+
func testInputModifiedDuringMultiJobBuild() throws {
240+
try withTemporaryDirectory { path in
241+
let main = path.appending(component: "main.swift")
242+
try localFileSystem.writeFileContents(main) {
243+
$0 <<< "let foo = 1"
244+
}
245+
let other = path.appending(component: "other.swift")
246+
try localFileSystem.writeFileContents(other) {
247+
$0 <<< "let bar = 2"
248+
}
249+
250+
var driver = try Driver(args: ["swiftc", main.pathString, other.pathString])
251+
let jobs = try driver.planBuild()
252+
XCTAssertTrue(jobs.count > 1)
253+
let resolver = try ArgsResolver()
254+
255+
// Change the file
256+
try localFileSystem.writeFileContents(other) {
257+
$0 <<< "let bar = 3"
258+
}
259+
260+
let delegate = JobCollectingDelegate()
261+
delegate.useStubProcess = true
262+
XCTAssertThrowsError(try driver.run(jobs: jobs, resolver: resolver,
263+
executorDelegate: delegate)) {
264+
// FIXME: The JobExecutor needs a way of emitting diagnostics or
265+
// propagating errors through llbuild.
266+
XCTAssertEqual($0 as? Diagnostics, .fatalError)
267+
}
268+
269+
}
270+
}
271+
209272
}

0 commit comments

Comments
 (0)