Skip to content

Commit 6376f3c

Browse files
committed
Split out compilation of package plugins so it can be done separately from invoking them (typically for collecting diagnostics up-front) and add a PluginCompilationResult type that can be returned to clients.
1 parent 52c852b commit 6376f3c

File tree

1 file changed

+55
-22
lines changed

1 file changed

+55
-22
lines changed

Sources/Workspace/DefaultPluginScriptRunner.swift

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,32 +31,37 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
3131
self.toolchain = toolchain
3232
self.enableSandbox = enableSandbox
3333
}
34+
35+
public func compilePluginScript(sources: Sources, toolsVersion: ToolsVersion) throws -> PluginCompilationResult {
36+
return try self.compile(sources: sources, toolsVersion: toolsVersion, cacheDir: self.cacheDir)
37+
}
3438

3539
/// Public protocol function that compiles and runs the plugin 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).
3640
public func runPluginScript(sources: Sources, inputJSON: Data, toolsVersion: ToolsVersion, writableDirectories: [AbsolutePath], diagnostics: DiagnosticsEngine, fileSystem: FileSystem) throws -> (outputJSON: Data, stdoutText: Data) {
37-
let compiledExec = try self.compile(sources: sources, toolsVersion: toolsVersion, cacheDir: self.cacheDir)
38-
return try self.invoke(compiledExec: compiledExec, toolsVersion: toolsVersion, writableDirectories: writableDirectories, input: inputJSON)
41+
// FIXME: We should only compile the plugin script again if needed.
42+
let result = try self.compile(sources: sources, toolsVersion: toolsVersion, cacheDir: self.cacheDir)
43+
guard let compiledExecutable = result.compiledExecutable else {
44+
throw DefaultPluginScriptRunnerError.compilationFailed(result)
45+
}
46+
return try self.invoke(compiledExec: compiledExecutable, toolsVersion: toolsVersion, writableDirectories: writableDirectories, input: inputJSON)
3947
}
4048

4149
public var hostTriple: Triple {
4250
return Self._hostTriple.memoize {
4351
Triple.getHostTriple(usingSwiftCompiler: self.toolchain.swiftCompilerPath)
4452
}
4553
}
46-
47-
/// Helper function that compiles a plugin script as an executable and returns the path to it.
48-
fileprivate func compile(sources: Sources, toolsVersion: ToolsVersion, cacheDir: AbsolutePath) throws -> AbsolutePath {
54+
55+
/// Helper function that compiles a plugin script as an executable and returns the path of the executable, any emitted diagnostics, etc. This function only throws an error if it wasn't even possible to start compiling the plugin — any regular compilation errors or warnings will be reflected in the returned compilation result.
56+
fileprivate func compile(sources: Sources, toolsVersion: ToolsVersion, cacheDir: AbsolutePath) throws -> PluginCompilationResult {
4957
// FIXME: Much of this is copied from the ManifestLoader and should be consolidated.
5058

59+
// Get access to the path containing the PackagePlugin module and library.
5160
let runtimePath = self.toolchain.swiftPMLibrariesLocation.pluginAPI
5261

53-
// Compile the package plugin script.
62+
// We use the toolchain's Swift compiler for compiling the plugin.
5463
var command = [self.toolchain.swiftCompilerPath.pathString]
5564

56-
// FIXME: Workaround for the module cache bug that's been haunting Swift CI
57-
// <rdar://problem/48443680>
58-
let moduleCachePath = ProcessEnv.vars["SWIFTPM_MODULECACHE_OVERRIDE"] ?? ProcessEnv.vars["SWIFTPM_TESTS_MODULECACHE"]
59-
6065
let macOSPackageDescriptionPath: AbsolutePath
6166
// if runtimePath is set to "PackageFrameworks" that means we could be developing SwiftPM in Xcode
6267
// which produces a framework for dynamic package products.
@@ -95,7 +100,12 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
95100
// Add any extra flags required as indicated by the ManifestLoader.
96101
command += self.toolchain.swiftCompilerFlags
97102

103+
// Add the Swift language version implied by the package tools version.
98104
command += ["-swift-version", toolsVersion.swiftLanguageVersion.rawValue]
105+
106+
// Add the PackageDescription version specified by the package tools version, which controls what PackagePlugin API is seen.
107+
command += ["-package-description-version", toolsVersion.description]
108+
99109
// if runtimePath is set to "PackageFrameworks" that means we could be developing SwiftPM in Xcode
100110
// which produces a framework for dynamic package products.
101111
if runtimePath.extension == "framework" {
@@ -108,26 +118,33 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
108118
command += ["-sdk", sdkRoot.pathString]
109119
}
110120
#endif
111-
command += ["-package-description-version", toolsVersion.description]
121+
122+
// Honor any module cache override that's set in the environment.
123+
let moduleCachePath = ProcessEnv.vars["SWIFTPM_MODULECACHE_OVERRIDE"] ?? ProcessEnv.vars["SWIFTPM_TESTS_MODULECACHE"]
112124
if let moduleCachePath = moduleCachePath {
113125
command += ["-module-cache-path", moduleCachePath]
114126
}
115127

116128
// Parse the plugin as a library so that `@main` is supported even though there might be only a single source file.
117129
command += ["-parse-as-library"]
118130

131+
// Add options to create a .dia file containing any diagnostics emitted by the compiler.
132+
let diagnosticsFile = cacheDir.appending(component: "diagnostics.dia")
133+
command += ["-Xfrontend", "-serialize-diagnostics-path", "-Xfrontend", diagnosticsFile.pathString]
134+
135+
// Add all the source files that comprise the plugin scripts.
119136
command += sources.paths.map { $0.pathString }
120-
let compiledExec = cacheDir.appending(component: "compiled-plugin")
121-
command += ["-o", compiledExec.pathString]
122-
123-
let result = try Process.popen(arguments: command, environment: toolchain.swiftCompilerEnvironment)
124-
let output = try (result.utf8Output() + result.utf8stderrOutput()).spm_chuzzle() ?? ""
125-
if result.exitStatus != .terminated(code: 0) {
126-
// TODO: Make this a proper error.
127-
throw StringError("failed to compile package plugin:\n\(command)\n\n\(output)")
128-
}
137+
138+
// Add the path of the compiled executable.
139+
let executableFile = cacheDir.appending(component: "compiled-plugin")
140+
command += ["-o", executableFile.pathString]
141+
142+
// Invoke the compiler and get back the result.
143+
let compilerResult = try Process.popen(arguments: command, environment: toolchain.swiftCompilerEnvironment)
129144

130-
return compiledExec
145+
// Finally return the result. We return the path of the compiled executable only if the compilation succeeded.
146+
let compiledExecutable = (compilerResult.exitStatus == .terminated(code: 0)) ? executableFile : nil
147+
return PluginCompilationResult(compiledExecutable: executableFile, diagnosticsFile: diagnosticsFile, compilerResult: compilerResult)
131148
}
132149

133150
/// Returns path to the sdk, if possible.
@@ -210,9 +227,24 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
210227
}
211228
}
212229

230+
/// 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.
231+
public struct PluginCompilationResult {
232+
/// Path of the compiled executable, or .none if compilation failed.
233+
public var compiledExecutable: AbsolutePath?
234+
235+
/// Path of the libClang diagnostics file emitted by the compiler (even if compilation succeded, it might contain warnings).
236+
public var diagnosticsFile: AbsolutePath
237+
238+
/// Process result of invoking the Swift compiler to produce the executable (contains command line, environment, exit status, and any output).
239+
public var compilerResult: ProcessResult
240+
}
241+
213242

214243
/// An error encountered by the default plugin runner.
215244
public enum DefaultPluginScriptRunnerError: Error {
245+
/// Failed to compile the plugin script, so it cannot be run.
246+
case compilationFailed(PluginCompilationResult)
247+
216248
/// Failed to start running the compiled plugin script as a subprocess. The message describes the error, and the
217249
/// command is the full command line that the runner tried to launch.
218250
case subprocessDidNotStart(_ message: String, command: [String])
@@ -226,10 +258,11 @@ public enum DefaultPluginScriptRunnerError: Error {
226258
/// line, and the output contains any emitted stdout and stderr.
227259
case missingPluginJSON(_ message: String, command: [String], output: String)
228260
}
229-
230261
extension DefaultPluginScriptRunnerError: CustomStringConvertible {
231262
public var description: String {
232263
switch self {
264+
case .compilationFailed(let result):
265+
return "could not compile plugin script: \(result)"
233266
case .subprocessDidNotStart(let message, _):
234267
return "could not run plugin script: \(message)"
235268
case .subprocessFailed(let message, _, let output):

0 commit comments

Comments
 (0)