Skip to content

Commit c23f9b2

Browse files
committed
Changes based on review feedback:
- use clearer names for callback queues to clarify that this queue is only used for callbacks - use `Result.tryMap` where appropriate Also: - simplify how any final `stderr` data after process termination is read - get rid of two warnings to a `default` clause in a switch that already covered all the cases - extend a unit test to cover both errors when compiling package plugin and then fixing it but still having a warning - when done with the plugin, just close the input channel (not need for a 'quit' mesage)
1 parent 1660815 commit c23f9b2

File tree

6 files changed

+145
-106
lines changed

6 files changed

+145
-106
lines changed

Sources/Commands/SwiftPackageTool.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ extension SwiftPackageTool {
985985
toolNamesToPaths: toolNamesToPaths,
986986
fileSystem: localFileSystem,
987987
observabilityScope: swiftTool.observabilityScope,
988-
on: DispatchQueue(label: "plugin-invocation"),
988+
callbackQueue: DispatchQueue(label: "plugin-invocation"),
989989
completion: $0) }
990990

991991
// Temporary: emit any output from the plugin.

Sources/PackagePlugin/Plugin.swift

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -166,14 +166,6 @@ extension Plugin {
166166
internalError("Couldn’t encode output JSON: \(error).")
167167
}
168168
try outputHandle.writePluginMessage(.provideResult(output: outputStruct.output))
169-
170-
// Exits the plugin logic.
171-
case .quit:
172-
exit(0)
173-
174-
// Ignore other messages
175-
default:
176-
continue
177169
}
178170
}
179171
}
@@ -187,7 +179,6 @@ extension Plugin {
187179
/// A message that the host can send to the plugin.
188180
enum HostToPluginMessage: Decodable {
189181
case performAction(input: WireInput)
190-
case quit
191182
}
192183

193184
/// A message that the plugin can send to the host.

Sources/SPMBuildCore/PluginInvocation.swift

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,18 @@ extension PluginTarget {
5454
toolNamesToPaths: [String: AbsolutePath],
5555
fileSystem: FileSystem,
5656
observabilityScope: ObservabilityScope,
57-
on queue: DispatchQueue,
57+
callbackQueue: DispatchQueue,
5858
completion: @escaping (Result<PluginInvocationResult, Error>) -> Void
5959
) {
6060
// Create the plugin working directory if needed (but don't do anything with it if it already exists).
6161
do {
6262
try fileSystem.createDirectory(outputDirectory, recursive: true)
6363
}
6464
catch {
65-
return completion(.failure(PluginEvaluationError.couldNotCreateOuputDirectory(path: outputDirectory, underlyingError: error)))
65+
return callbackQueue.async { completion(.failure(PluginEvaluationError.couldNotCreateOuputDirectory(path: outputDirectory, underlyingError: error))) }
6666
}
6767

6868
// Create the input context to send to the plugin.
69-
// TODO: Some of this could probably be cached.
7069
var serializer = PluginScriptRunnerInputSerializer(buildEnvironment: buildEnvironment)
7170
let inputStruct: PluginScriptRunnerInput
7271
do {
@@ -78,7 +77,7 @@ extension PluginTarget {
7877
pluginAction: action)
7978
}
8079
catch {
81-
return completion(.failure(PluginEvaluationError.couldNotSerializePluginInput(underlyingError: error)))
80+
return callbackQueue.async { completion(.failure(PluginEvaluationError.couldNotSerializePluginInput(underlyingError: error))) }
8281
}
8382

8483
// Call the plugin script runner to actually invoke the plugin.
@@ -90,12 +89,13 @@ extension PluginTarget {
9089
writableDirectories: [outputDirectory],
9190
fileSystem: fileSystem,
9291
observabilityScope: observabilityScope,
93-
on: DispatchQueue(label: "plugin-invocation"),
92+
callbackQueue: DispatchQueue(label: "plugin-invocation"),
9493
outputHandler: { data in
9594
outputText.append(contentsOf: data)
96-
}) { result in
97-
switch result {
98-
case .success(let output):
95+
},
96+
completion: { result in
97+
// Translate the PluginScriptRunnerOutput into a PluginInvocationResult.
98+
completion(result.tryMap { output in
9999
// Generate emittable Diagnostics from the plugin output.
100100
let diagnostics: [Diagnostic] = output.diagnostics.map { diag in
101101
let metadata: ObservabilityMetadata? = diag.file.map {
@@ -143,16 +143,14 @@ extension PluginTarget {
143143
}
144144

145145
// Create and return an evaluation result for the invocation.
146-
completion(.success(PluginInvocationResult(
146+
return PluginInvocationResult(
147147
plugin: self,
148148
diagnostics: diagnostics,
149149
textOutput: String(decoding: outputText, as: UTF8.self),
150150
buildCommands: buildCommands,
151-
prebuildCommands: prebuildCommands)))
152-
case .failure(let error):
153-
completion(.failure(error))
154-
}
155-
}
151+
prebuildCommands: prebuildCommands)
152+
})
153+
})
156154
}
157155
}
158156

@@ -244,7 +242,7 @@ extension PackageGraph {
244242
toolNamesToPaths: toolNamesToPaths,
245243
fileSystem: fileSystem,
246244
observabilityScope: observabilityScope,
247-
on: DispatchQueue(label: "plugin-invocation"),
245+
callbackQueue: DispatchQueue(label: "plugin-invocation"),
248246
completion: $0) }
249247
pluginResults.append(result)
250248
}
@@ -383,7 +381,7 @@ public protocol PluginScriptRunner {
383381
writableDirectories: [AbsolutePath],
384382
fileSystem: FileSystem,
385383
observabilityScope: ObservabilityScope,
386-
on queue: DispatchQueue,
384+
callbackQueue: DispatchQueue,
387385
outputHandler: @escaping (Data) -> Void,
388386
completion: @escaping (Result<PluginScriptRunnerOutput, Error>) -> Void
389387
)

Sources/Workspace/DefaultPluginScriptRunner.swift

Lines changed: 72 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -32,47 +32,60 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
3232
self.enableSandbox = enableSandbox
3333
}
3434

35+
/// Public protocol function that starts compiling the plugin script to an exectutable. The tools version controls the availability of APIs in PackagePlugin, and should be set to the tools version of the package that defines the plugin (not of the target to which it is being applied). This function returns immediately and then calls the completion handler on the callbackq queue when compilation ends.
3536
public func compilePluginScript(
3637
sources: Sources,
3738
toolsVersion: ToolsVersion,
3839
observabilityScope: ObservabilityScope,
39-
on queue: DispatchQueue,
40+
callbackQueue: DispatchQueue,
4041
completion: @escaping (Result<PluginCompilationResult, Error>) -> Void
4142
) {
42-
self.compile(sources: sources, toolsVersion: toolsVersion, cacheDir: self.cacheDir, observabilityScope: observabilityScope, on: queue, completion: completion)
43+
self.compile(
44+
sources: sources,
45+
toolsVersion: toolsVersion,
46+
cacheDir: self.cacheDir,
47+
observabilityScope: observabilityScope,
48+
callbackQueue: callbackQueue,
49+
completion: completion)
4350
}
4451

45-
/// Public protocol function that starting evaluating a plugin by compiling it and running it as a subprocess. The tools version controls the availability of APIs in PackagePlugin, and should be set to the tools version of the package that defines the plugin (not of the target to which it is being applied). This function returns immediately and then calls the output handler as plain-text output is received from the plugin, and calls the completion handler once it finishes running.
52+
/// Public protocol function that starts evaluating a plugin by compiling it and running it as a subprocess. The tools version controls the availability of APIs in PackagePlugin, and should be set to the tools version of the package that defines the plugin (not of the target to which it is being applied). This function returns immediately and then calls the output handler on the given callback queue when any plain-text output is received from the plugin, and calls the completion handler once it finishes running.
4653
public func runPluginScript(
4754
sources: Sources,
4855
input: PluginScriptRunnerInput,
4956
toolsVersion: ToolsVersion,
5057
writableDirectories: [AbsolutePath],
5158
fileSystem: FileSystem,
5259
observabilityScope: ObservabilityScope,
53-
on queue: DispatchQueue,
60+
callbackQueue: DispatchQueue,
5461
outputHandler: @escaping (Data) -> Void,
5562
completion: @escaping (Result<PluginScriptRunnerOutput, Error>) -> Void
5663
) {
64+
// Asynchronously compile the plugin script to an executable.
5765
// TODO: Skip compiling the plugin script if it has already been compiled and hasn't changed.
58-
self.compile(sources: sources, toolsVersion: toolsVersion, cacheDir: self.cacheDir, observabilityScope: observabilityScope, on: queue) { result in
59-
switch result {
60-
case .success(let result):
61-
guard let compiledExecutable = result.compiledExecutable else {
62-
return completion(.failure(DefaultPluginScriptRunnerError.compilationFailed(result)))
66+
self.compile(
67+
sources: sources,
68+
toolsVersion: toolsVersion,
69+
cacheDir: self.cacheDir,
70+
observabilityScope: observabilityScope,
71+
callbackQueue: callbackQueue,
72+
completion: { result in
73+
// If compilation suceeded, asynchronously run the executable.
74+
switch result {
75+
case .success(let result):
76+
self.invoke(
77+
compiledExec: result.compiledExecutable,
78+
writableDirectories: writableDirectories,
79+
input: input,
80+
observabilityScope: observabilityScope,
81+
callbackQueue: callbackQueue,
82+
outputHandler: outputHandler,
83+
completion: completion)
84+
case .failure(let error):
85+
completion(.failure(error))
6386
}
64-
self.invoke(
65-
compiledExec: compiledExecutable,
66-
writableDirectories: writableDirectories,
67-
input: input,
68-
observabilityScope: observabilityScope,
69-
on: queue,
70-
outputHandler: outputHandler,
71-
completion: completion)
72-
case .failure(let error):
73-
completion(.failure(error))
7487
}
75-
}
88+
)
7689
}
7790

7891
public var hostTriple: Triple {
@@ -87,10 +100,10 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
87100
toolsVersion: ToolsVersion,
88101
cacheDir: AbsolutePath,
89102
observabilityScope: ObservabilityScope,
90-
on queue: DispatchQueue,
103+
callbackQueue: DispatchQueue,
91104
completion: @escaping (Result<PluginCompilationResult, Error>) -> Void
92105
) {
93-
// FIXME: Much of this is copied from the ManifestLoader and should be consolidated.
106+
// FIXME: Much of this is similar to what the ManifestLoader is doing. This should be consolidated.
94107

95108
// Get access to the path containing the PackagePlugin module and library.
96109
let runtimePath = self.toolchain.swiftPMLibrariesLocation.pluginAPI
@@ -174,20 +187,26 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
174187
let executableFile = cacheDir.appending(component: "compiled-plugin")
175188
command += ["-o", executableFile.pathString]
176189

177-
// Create the cache directory in which we'll be placing the compiled executable if needed. Any failure to create it will be reported by the compiler.
178-
try? FileManager.default.createDirectory(at: cacheDir.asURL, withIntermediateDirectories: true, attributes: nil)
190+
do {
191+
// Create the cache directory in which we'll be placing the compiled executable if needed.
192+
try FileManager.default.createDirectory(at: cacheDir.asURL, withIntermediateDirectories: true, attributes: nil)
179193

180-
Process.popen(arguments: command, environment: toolchain.swiftCompilerEnvironment, queue: queue) { result in
181-
switch result {
182-
case .success(let processResult):
183-
// We return the path of the compiled executable only if the compilation succeeded.
184-
let compiledExecutable = (processResult.exitStatus == .terminated(code: 0)) ? executableFile : nil
185-
let compilationResult = PluginCompilationResult(compiledExecutable: compiledExecutable, diagnosticsFile: diagnosticsFile, compilerResult: processResult)
186-
completion(.success(compilationResult))
187-
case .failure(let error):
188-
completion(.failure(error))
194+
// Compile the plugin script asynchronously.
195+
Process.popen(arguments: command, environment: toolchain.swiftCompilerEnvironment, queue: callbackQueue) {
196+
// We are now on our caller's requested callback queue, so we just call the completion handler directly.
197+
completion($0.tryMap {
198+
let result = PluginCompilationResult(compilerResult: $0, diagnosticsFile: diagnosticsFile, compiledExecutable: executableFile)
199+
guard $0.exitStatus == .terminated(code: 0) else {
200+
throw DefaultPluginScriptRunnerError.compilationFailed(result)
201+
}
202+
return result
203+
})
189204
}
190205
}
206+
catch {
207+
// We get here if we didn't even get far enough to invoke the compiler before hitting an error.
208+
callbackQueue.async { completion(.failure(DefaultPluginScriptRunnerError.compilationSetupFailed(error: error))) }
209+
}
191210
}
192211

193212
/// Returns path to the sdk, if possible.
@@ -227,7 +246,7 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
227246
writableDirectories: [AbsolutePath],
228247
input: PluginScriptRunnerInput,
229248
observabilityScope: ObservabilityScope,
230-
on queue: DispatchQueue,
249+
callbackQueue: DispatchQueue,
231250
outputHandler: @escaping (Data) -> Void,
232251
completion: @escaping (Result<PluginScriptRunnerOutput, Error>) -> Void
233252
) {
@@ -246,24 +265,12 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
246265
process.environment = ProcessInfo.processInfo.environment
247266
process.currentDirectoryURL = self.cacheDir.asURL
248267

249-
// Create a dispatch group for waiting on proces termination and all output from it.
250-
let waiters = DispatchGroup()
251-
252268
// Set up a pipe for receiving free-form text output from the plugin on its stderr.
253-
waiters.enter()
254269
let stderrPipe = Pipe()
255-
var stderrData = Data()
256270
stderrPipe.fileHandleForReading.readabilityHandler = { (fileHandle: FileHandle) -> Void in
257271
// We just pass the data on to our given output handler.
258272
let newData = fileHandle.availableData
259-
if newData.isEmpty {
260-
fileHandle.readabilityHandler = nil
261-
waiters.leave()
262-
}
263-
else {
264-
stderrData.append(contentsOf: newData)
265-
queue.async { outputHandler(newData) }
266-
}
273+
callbackQueue.async { outputHandler(newData) }
267274
}
268275
process.standardError = stderrPipe
269276

@@ -277,13 +284,6 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
277284
let outputHandle = stdinPipe.fileHandleForWriting
278285
process.standardInput = stdinPipe
279286

280-
// Set up a termination handler.
281-
process.terminationHandler = { _ in
282-
// We don't do anything special other than note the process exit.
283-
waiters.leave()
284-
}
285-
286-
waiters.enter()
287287
do {
288288
// Start the plugin process.
289289
try process.run()
@@ -297,56 +297,58 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
297297
switch message {
298298
case .provideResult(let output):
299299
result = output
300-
try outputHandle.writePluginMessage(.quit)
300+
try outputHandle.close()
301301
}
302302
}
303303

304+
// Read and pass on any remaining free-form text output from the plugin.
305+
stderrPipe.fileHandleForReading.readabilityHandler?(stderrPipe.fileHandleForReading)
306+
304307
// Wait for the process to terminate and the readers to finish collecting all output.
305-
waiters.wait()
308+
process.waitUntilExit()
306309

307-
switch process.terminationReason {
308-
case .uncaughtSignal:
310+
if process.terminationReason == .uncaughtSignal {
309311
throw StringError("plugin process ended by an uncaught signal")
310-
case .exit:
311-
if process.terminationStatus != 0 {
312-
throw StringError("plugin process ended with a non-zero exit code: \(process.terminationStatus)")
313-
}
314-
default:
315-
throw StringError("plugin process ended for unexpected reason")
312+
}
313+
else if process.terminationStatus != 0 {
314+
throw StringError("plugin process ended with a non-zero exit code: \(process.terminationStatus)")
316315
}
317316
guard let result = result else {
318317
throw StringError("didn’t receive output from plugin")
319318
}
320-
queue.async { completion(.success(result)) }
319+
callbackQueue.async { completion(.success(result)) }
321320
}
322321
catch {
323-
queue.async { completion(.failure(DefaultPluginScriptRunnerError.invocationFailed(error, command: command))) }
322+
callbackQueue.async { completion(.failure(DefaultPluginScriptRunnerError.invocationFailed(error, command: command))) }
324323
}
325324
}
326325
}
327326

328327
/// The result of compiling a plugin. The executable path will only be present if the compilation succeeds, while the other properties are present in all cases.
329328
public struct PluginCompilationResult {
330-
/// Path of the compiled executable, or .none if compilation failed.
331-
public var compiledExecutable: AbsolutePath?
329+
/// Process result of invoking the Swift compiler to produce the executable (contains command line, environment, exit status, and any output).
330+
public var compilerResult: ProcessResult
332331

333332
/// Path of the libClang diagnostics file emitted by the compiler (even if compilation succeded, it might contain warnings).
334333
public var diagnosticsFile: AbsolutePath
335334

336-
/// Process result of invoking the Swift compiler to produce the executable (contains command line, environment, exit status, and any output).
337-
public var compilerResult: ProcessResult
335+
/// Path of the compiled executable.
336+
public var compiledExecutable: AbsolutePath
338337
}
339338

340339

341340
/// An error encountered by the default plugin runner.
342341
public enum DefaultPluginScriptRunnerError: Error {
342+
case compilationSetupFailed(error: Error)
343343
case compilationFailed(PluginCompilationResult)
344344
case invocationFailed(_ error: Error, command: [String])
345345
}
346346

347347
extension DefaultPluginScriptRunnerError: CustomStringConvertible {
348348
public var description: String {
349349
switch self {
350+
case .compilationSetupFailed(let error):
351+
return "plugin script compilation failed: \(error)"
350352
case .compilationFailed(let result):
351353
return "plugin script compilation failed: \(result)"
352354
case .invocationFailed(let message, _):
@@ -358,7 +360,6 @@ extension DefaultPluginScriptRunnerError: CustomStringConvertible {
358360
/// A message that the host can send to the plugin.
359361
enum HostToPluginMessage: Codable {
360362
case performAction(input: PluginScriptRunnerInput)
361-
case quit
362363
}
363364

364365
/// A message that the plugin can send to the host.

Tests/FunctionalTests/PluginTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ class PluginTests: XCTestCase {
286286
toolNamesToPaths: [:],
287287
fileSystem: localFileSystem,
288288
observabilityScope: observability.topScope,
289-
on: DispatchQueue(label: "plugin-invocation"),
289+
callbackQueue: DispatchQueue(label: "plugin-invocation"),
290290
completion: $0) }
291291

292292
// Check the results.

0 commit comments

Comments
 (0)