Skip to content

Commit 09d77a7

Browse files
authored
Clean up the code and some of the API of compiling plugins, and improve persistence of cached compiler outputs (#4290)
Clients now get the command line and compiler output even in the case of failure, and also when the plugin doesn't have to be recompiled because nothing changed (previously this information was lost). This is done by serializing a proper struct to JSON rather than just storing the hexadecimal representation of the configuration hash. Callers also have control over the name of the temporary directory to represent the plugin, rather than always having it be the last path component of the sources directory.
1 parent 383f815 commit 09d77a7

File tree

6 files changed

+372
-292
lines changed

6 files changed

+372
-292
lines changed

Sources/Basics/JSON+Extensions.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,10 @@ extension JSONDecoder {
123123
return try self.decode(kind, from: data)
124124
}
125125
}
126+
127+
extension JSONEncoder {
128+
public func encode<T: Encodable>(path: AbsolutePath, fileSystem: FileSystem, _ value: T) throws {
129+
let data = try self.encode(value)
130+
try fileSystem.writeFileContents(path, data: data)
131+
}
132+
}

Sources/Build/BuildOperation.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -235,14 +235,19 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
235235
// Compile the plugin, getting back a PluginCompilationResult.
236236
let preparationStepName = "Compiling plugin \(plugin.targetName)..."
237237
self.buildSystemDelegate?.preparationStepStarted(preparationStepName)
238-
let result = try self.pluginScriptRunner.compilePluginScript(
239-
sources: plugin.sources,
240-
toolsVersion: plugin.toolsVersion,
241-
observabilityScope: self.observabilityScope)
238+
let result = try tsc_await {
239+
self.pluginScriptRunner.compilePluginScript(
240+
sourceFiles: plugin.sources.paths,
241+
pluginName: plugin.targetName,
242+
toolsVersion: plugin.toolsVersion,
243+
observabilityScope: self.observabilityScope,
244+
callbackQueue: DispatchQueue.sharedConcurrent,
245+
completion: $0)
246+
}
242247
if !result.description.isEmpty {
243248
self.buildSystemDelegate?.preparationStepHadOutput(preparationStepName, output: result.description)
244249
}
245-
self.buildSystemDelegate?.preparationStepFinished(preparationStepName, result: result.wasCached ? .skipped : (result.succeeded ? .succeeded : .failed))
250+
self.buildSystemDelegate?.preparationStepFinished(preparationStepName, result: result.cached ? .skipped : (result.succeeded ? .succeeded : .failed))
246251

247252
// Throw an error on failure; we will already have emitted the compiler's output in this case.
248253
if !result.succeeded {

Sources/SPMBuildCore/PluginInvocation.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,8 @@ extension PluginTarget {
236236

237237
// Call the plugin script runner to actually invoke the plugin.
238238
scriptRunner.runPluginScript(
239-
sources: sources,
239+
sourceFiles: sources.paths,
240+
pluginName: self.name,
240241
initialMessage: initialMessage,
241242
toolsVersion: self.apiVersion,
242243
workingDirectory: workingDirectory,

Sources/SPMBuildCore/PluginScriptRunner.swift

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ import struct TSCUtility.Triple
2121
/// Implements the mechanics of running and communicating with a plugin (implemented as a set of Swift source files). In most environments this is done by compiling the code to an executable, invoking it as a sandboxed subprocess, and communicating with it using pipes. Specific implementations are free to implement things differently, however.
2222
public protocol PluginScriptRunner {
2323

24-
/// 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.
24+
/// Public protocol function that starts compiling the plugin script to an exectutable. The name is used as the basename for the executable and auxiliary files. 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.
2525
func compilePluginScript(
26-
sources: Sources,
26+
sourceFiles: [AbsolutePath],
27+
pluginName: String,
2728
toolsVersion: ToolsVersion,
28-
observabilityScope: ObservabilityScope
29-
) throws -> PluginCompilationResult
29+
observabilityScope: ObservabilityScope,
30+
callbackQueue: DispatchQueue,
31+
completion: @escaping (Result<PluginCompilationResult, Error>) -> Void
32+
)
3033

3134
/// Implements the mechanics of running a plugin script implemented as a set of Swift source files, for use
3235
/// by the package graph when it is evaluating package plugins.
@@ -39,7 +42,8 @@ public protocol PluginScriptRunner {
3942
///
4043
/// Every concrete implementation should cache any intermediates as necessary to avoid redundant work.
4144
func runPluginScript(
42-
sources: Sources,
45+
sourceFiles: [AbsolutePath],
46+
pluginName: String,
4347
initialMessage: Data,
4448
toolsVersion: ToolsVersion,
4549
workingDirectory: AbsolutePath,
@@ -69,36 +73,44 @@ public protocol PluginScriptRunnerDelegate {
6973

7074
/// 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.
7175
public struct PluginCompilationResult {
72-
/// Process result of invoking the Swift compiler to produce the executable (contains command line, environment, exit status, and any output).
73-
public var compilerResult: ProcessResult?
76+
/// Whether compilation succeeded.
77+
public var succeeded: Bool
7478

75-
/// Path of the libClang diagnostics file emitted by the compiler (even if compilation succeded, it might contain warnings).
76-
public var diagnosticsFile: AbsolutePath
79+
/// Complete compiler command line.
80+
public var commandLine: [String]
7781

7882
/// Path of the compiled executable.
79-
public var compiledExecutable: AbsolutePath
80-
81-
/// Whether the compilation result was cached.
82-
public var wasCached: Bool
83+
public var executableFile: AbsolutePath
8384

84-
public init(compilerResult: ProcessResult?, diagnosticsFile: AbsolutePath, compiledExecutable: AbsolutePath, wasCached: Bool) {
85-
self.compilerResult = compilerResult
86-
self.diagnosticsFile = diagnosticsFile
87-
self.compiledExecutable = compiledExecutable
88-
self.wasCached = wasCached
89-
}
85+
/// Path of the libClang diagnostics file emitted by the compiler.
86+
public var diagnosticsFile: AbsolutePath
87+
88+
/// Any output emitted by the compiler (stdout and stderr combined).
89+
public var compilerOutput: String
9090

91-
/// Returns true if and only if the compilation succeeded or was cached
92-
public var succeeded: Bool {
93-
return self.wasCached || self.compilerResult?.exitStatus == .terminated(code: 0)
91+
/// Whether the compilation result came from the cache (false means that the compiler did run).
92+
public var cached: Bool
93+
94+
public init(
95+
succeeded: Bool,
96+
commandLine: [String],
97+
executableFile: AbsolutePath,
98+
diagnosticsFile: AbsolutePath,
99+
compilerOutput: String,
100+
cached: Bool
101+
) {
102+
self.succeeded = succeeded
103+
self.commandLine = commandLine
104+
self.executableFile = executableFile
105+
self.diagnosticsFile = diagnosticsFile
106+
self.compilerOutput = compilerOutput
107+
self.cached = cached
94108
}
95109
}
96110

97111
extension PluginCompilationResult: CustomStringConvertible {
98112
public var description: String {
99-
let stdout = (try? compilerResult?.utf8Output()) ?? ""
100-
let stderr = (try? compilerResult?.utf8stderrOutput()) ?? ""
101-
let output = (stdout + stderr).spm_chomp()
113+
let output = compilerOutput.spm_chomp()
102114
return output + (output.isEmpty || output.hasSuffix("\n") ? "" : "\n")
103115
}
104116
}
@@ -107,10 +119,11 @@ extension PluginCompilationResult: CustomDebugStringConvertible {
107119
public var debugDescription: String {
108120
return """
109121
<PluginCompilationResult(
110-
exitStatus: \(compilerResult.map{ "\($0.exitStatus)" } ?? "-"),
111-
stdout: \((try? compilerResult?.utf8Output()) ?? ""),
112-
stderr: \((try? compilerResult?.utf8stderrOutput()) ?? ""),
113-
executable: \(compiledExecutable.prettyPath())
122+
succeeded: \(succeeded),
123+
commandLine: \(commandLine.map{ $0.spm_shellEscaped() }.joined(separator: " ")),
124+
executable: \(executableFile.prettyPath())
125+
diagnostics: \(diagnosticsFile.prettyPath())
126+
compilerOutput: \(compilerOutput.spm_shellEscaped())
114127
)>
115128
"""
116129
}

0 commit comments

Comments
 (0)