Skip to content

Commit b5480f2

Browse files
committed
Add support for spawning a compile job that reads input from stdin.
This change builds on swiftlang/swift-tools-support-core#208 to allow forwarding of the driver's Standard Input to a compile job. Resolves rdar://76454784
1 parent 903ae6a commit b5480f2

File tree

4 files changed

+146
-11
lines changed

4 files changed

+146
-11
lines changed

Sources/SwiftDriver/Execution/ProcessProtocol.swift

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

1415
/// Abstraction for functionality that allows working with subprocesses.
1516
public protocol ProcessProtocol {
@@ -19,7 +20,7 @@ public protocol ProcessProtocol {
1920
/// a negative number to represent a "quasi-pid".
2021
///
2122
/// - SeeAlso: https://github.com/apple/swift/blob/main/docs/DriverParseableOutput.rst#quasi-pids
22-
var processID: Process.ProcessID { get }
23+
var processID: TSCBasic.Process.ProcessID { get }
2324

2425
/// Wait for the process to finish execution.
2526
@discardableResult
@@ -29,15 +30,39 @@ public protocol ProcessProtocol {
2930
arguments: [String],
3031
env: [String: String]
3132
) throws -> Self
33+
34+
static func launchProcessAndWriteInput(
35+
arguments: [String],
36+
env: [String: String],
37+
inputFileHandle: FileHandle
38+
) throws -> Self
3239
}
3340

34-
extension Process: ProcessProtocol {
41+
extension TSCBasic.Process: ProcessProtocol {
3542
public static func launchProcess(
3643
arguments: [String],
3744
env: [String: String]
38-
) throws -> Process {
45+
) throws -> TSCBasic.Process {
3946
let process = Process(arguments: arguments, environment: env)
4047
try process.launch()
4148
return process
4249
}
50+
51+
public static func launchProcessAndWriteInput(
52+
arguments: [String],
53+
env: [String: String],
54+
inputFileHandle: FileHandle
55+
) throws -> TSCBasic.Process {
56+
let process = Process(arguments: arguments, environment: env)
57+
let processInputStream = try process.launch()
58+
var input: Data
59+
// Write out the contents of the input handle and close the input stream
60+
repeat {
61+
input = inputFileHandle.availableData
62+
processInputStream.write(input)
63+
} while (input.count > 0)
64+
processInputStream.flush()
65+
try processInputStream.close()
66+
return process
67+
}
4368
}

Sources/SwiftDriverExecution/MultiJobExecutor.swift

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ public final class MultiJobExecutor {
8282
/// The type to use when launching new processes. This mostly serves as an override for testing.
8383
let processType: ProcessProtocol.Type
8484

85+
/// The standard input `FileHandle` override for testing.
86+
let testInputHandle: FileHandle?
87+
8588
/// If a job fails, the driver needs to stop running jobs.
8689
private(set) var isBuildCancelled = false
8790

@@ -100,7 +103,8 @@ public final class MultiJobExecutor {
100103
forceResponseFiles: Bool,
101104
recordedInputModificationDates: [TypedVirtualPath: Date],
102105
diagnosticsEngine: DiagnosticsEngine,
103-
processType: ProcessProtocol.Type = Process.self
106+
processType: ProcessProtocol.Type = Process.self,
107+
inputHandleOverride: FileHandle? = nil
104108
) {
105109
(
106110
jobs: self.jobs,
@@ -121,6 +125,7 @@ public final class MultiJobExecutor {
121125
self.recordedInputModificationDates = recordedInputModificationDates
122126
self.diagnosticsEngine = diagnosticsEngine
123127
self.processType = processType
128+
self.testInputHandle = inputHandleOverride
124129
}
125130

126131
private static func fillInJobsAndProducers(_ workload: DriverExecutorWorkload
@@ -248,6 +253,9 @@ public final class MultiJobExecutor {
248253
/// The type to use when launching new processes. This mostly serves as an override for testing.
249254
private let processType: ProcessProtocol.Type
250255

256+
/// The standard input `FileHandle` override for testing.
257+
let testInputHandle: FileHandle?
258+
251259
public init(
252260
workload: DriverExecutorWorkload,
253261
resolver: ArgsResolver,
@@ -257,7 +265,8 @@ public final class MultiJobExecutor {
257265
processSet: ProcessSet? = nil,
258266
forceResponseFiles: Bool = false,
259267
recordedInputModificationDates: [TypedVirtualPath: Date] = [:],
260-
processType: ProcessProtocol.Type = Process.self
268+
processType: ProcessProtocol.Type = Process.self,
269+
inputHandleOverride: FileHandle? = nil
261270
) {
262271
self.workload = workload
263272
self.argsResolver = resolver
@@ -268,6 +277,7 @@ public final class MultiJobExecutor {
268277
self.forceResponseFiles = forceResponseFiles
269278
self.recordedInputModificationDates = recordedInputModificationDates
270279
self.processType = processType
280+
self.testInputHandle = inputHandleOverride
271281
}
272282

273283
/// Execute all jobs.
@@ -314,7 +324,8 @@ public final class MultiJobExecutor {
314324
forceResponseFiles: forceResponseFiles,
315325
recordedInputModificationDates: recordedInputModificationDates,
316326
diagnosticsEngine: diagnosticsEngine,
317-
processType: processType
327+
processType: processType,
328+
inputHandleOverride: testInputHandle
318329
)
319330
}
320331
}
@@ -566,9 +577,20 @@ class ExecuteJobRule: LLBuildRule {
566577
let arguments: [String] = try resolver.resolveArgumentList(for: job,
567578
forceResponseFiles: context.forceResponseFiles)
568579

569-
let process = try context.processType.launchProcess(
570-
arguments: arguments, env: env
571-
)
580+
581+
let process : ProcessProtocol
582+
// If the input comes from standard input, forward the driver's input to the compile job.
583+
if job.inputs.contains(TypedVirtualPath(file: .standardInput, type: .swift)) {
584+
let inputFileHandle = context.testInputHandle ?? FileHandle.standardInput
585+
process = try context.processType.launchProcessAndWriteInput(
586+
arguments: arguments, env: env, inputFileHandle: inputFileHandle
587+
)
588+
} else {
589+
process = try context.processType.launchProcess(
590+
arguments: arguments, env: env
591+
)
592+
}
593+
572594
pid = Int(process.processID)
573595

574596
// Add it to the process set if it's a real process.

Sources/SwiftDriverExecution/SwiftDriverExecutor.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,14 @@ public final class SwiftDriverExecutor: DriverExecutor {
5151
} else {
5252
var childEnv = env
5353
childEnv.merge(job.extraEnvironment, uniquingKeysWith: { (_, new) in new })
54-
55-
let process = try Process.launchProcess(arguments: arguments, env: childEnv)
54+
let process : ProcessProtocol
55+
if job.inputs.contains(TypedVirtualPath(file: .standardInput, type: .swift)) {
56+
process = try Process.launchProcessAndWriteInput(
57+
arguments: arguments, env: childEnv, inputFileHandle: FileHandle.standardInput
58+
)
59+
} else {
60+
process = try Process.launchProcess(arguments: arguments, env: childEnv)
61+
}
5662
return try process.waitUntilExit()
5763
}
5864
}

Tests/SwiftDriverTests/JobExecutorTests.swift

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ class JobCollectingDelegate: JobExecutionDelegate {
2929
return .init()
3030
}
3131

32+
static func launchProcessAndWriteInput(arguments: [String], env: [String : String],
33+
inputFileHandle: FileHandle) throws -> StubProcess {
34+
return .init()
35+
}
36+
3237
var processID: TSCBasic.Process.ProcessID { .init(-1) }
3338

3439
func waitUntilExit() throws -> ProcessResult {
@@ -207,6 +212,83 @@ final class JobExecutorTests: XCTestCase {
207212
#endif
208213
}
209214

215+
/// Ensure the executor is capable of forwarding its standard input to the compile job that requires it.
216+
func testInputForwarding() throws {
217+
#if os(macOS)
218+
let executor = try SwiftDriverExecutor(diagnosticsEngine: DiagnosticsEngine(),
219+
processSet: ProcessSet(),
220+
fileSystem: localFileSystem,
221+
env: ProcessEnv.vars)
222+
let toolchain = DarwinToolchain(env: ProcessEnv.vars, executor: executor)
223+
try withTemporaryDirectory { path in
224+
let exec = path.appending(component: "main")
225+
let compile = Job(
226+
moduleName: "main",
227+
kind: .compile,
228+
tool: .absolute(try toolchain.getToolPath(.swiftCompiler)),
229+
commandLine: [
230+
"-frontend",
231+
"-c",
232+
"-primary-file",
233+
"-",
234+
"-target", "x86_64-apple-darwin18.7.0",
235+
"-enable-objc-interop",
236+
"-sdk",
237+
.path(.absolute(try toolchain.sdk.get())),
238+
"-module-name", "main",
239+
"-o", .path(.temporary(RelativePath("main.o"))),
240+
],
241+
inputs: [TypedVirtualPath(file: .standardInput, type: .swift )],
242+
primaryInputs: [TypedVirtualPath(file: .standardInput, type: .swift )],
243+
outputs: [.init(file: VirtualPath.temporary(RelativePath("main.o")).intern(),
244+
type: .object)]
245+
)
246+
let link = Job(
247+
moduleName: "main",
248+
kind: .link,
249+
tool: .absolute(try toolchain.getToolPath(.dynamicLinker)),
250+
commandLine: [
251+
.path(.temporary(RelativePath("main.o"))),
252+
.path(.absolute(try toolchain.clangRT.get())),
253+
"-syslibroot", .path(.absolute(try toolchain.sdk.get())),
254+
"-lobjc", "-lSystem", "-arch", "x86_64",
255+
"-force_load", .path(.absolute(try toolchain.compatibility50.get())),
256+
"-force_load", .path(.absolute(try toolchain.compatibilityDynamicReplacements.get())),
257+
"-L", .path(.absolute(try toolchain.resourcesDirectory.get())),
258+
"-L", .path(.absolute(try toolchain.sdkStdlib(sdk: toolchain.sdk.get()))),
259+
"-rpath", "/usr/lib/swift", "-macosx_version_min", "10.14.0", "-no_objc_category_merging",
260+
"-o", .path(.absolute(exec)),
261+
],
262+
inputs: [
263+
.init(file: VirtualPath.temporary(RelativePath("main.o")).intern(), type: .object),
264+
],
265+
primaryInputs: [],
266+
outputs: [.init(file: VirtualPath.relative(RelativePath("main")).intern(), type: .image)]
267+
)
268+
269+
// Create a file with inpuit
270+
let inputFile = path.appending(component: "main.swift")
271+
try localFileSystem.writeFileContents(inputFile) {
272+
$0 <<< "print(\"Hello, World\")"
273+
}
274+
// We are going to override he executors standard input FileHandle to the above
275+
// input file, to simulate it being piped over standard input to this compilation.
276+
let testFile: FileHandle = FileHandle(forReadingAtPath: inputFile.description)!
277+
let delegate = JobCollectingDelegate()
278+
let resolver = try ArgsResolver(fileSystem: localFileSystem)
279+
let executor = MultiJobExecutor(workload: .all([compile, link]),
280+
resolver: resolver, executorDelegate: delegate,
281+
diagnosticsEngine: DiagnosticsEngine(),
282+
inputHandleOverride: testFile)
283+
try executor.execute(env: toolchain.env, fileSystem: localFileSystem)
284+
285+
// Execute the resulting program
286+
let output = try TSCBasic.Process.checkNonZeroExit(args: exec.pathString)
287+
XCTAssertEqual(output, "Hello, World\n")
288+
}
289+
#endif
290+
}
291+
210292
func testStubProcessProtocol() throws {
211293
// This test fails intermittently on Linux
212294
// rdar://70067844

0 commit comments

Comments
 (0)