Skip to content

Commit a69313b

Browse files
committed
Use plugin messaging rather than a custom return struct to send back diagnostics and build command definitions. Adjust call sites accordingly. This further separates out the things that are particular to build tool plugins from those that are common to all plugins.
1 parent e056381 commit a69313b

File tree

12 files changed

+301
-297
lines changed

12 files changed

+301
-297
lines changed

Sources/Build/BuildOperation.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
3030
let packageGraphLoader: () throws -> PackageGraph
3131

3232
/// The closure for invoking plugins in the package graph.
33-
let pluginInvoker: (PackageGraph) throws -> [ResolvedTarget: [PluginInvocationResult]]
33+
let pluginInvoker: (PackageGraph) throws -> [ResolvedTarget: [BuildToolPluginInvocationResult]]
3434

3535
/// The llbuild build delegate reference.
3636
private var buildSystemDelegate: BuildOperationBuildSystemDelegateHandler?
@@ -70,7 +70,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
7070
buildParameters: BuildParameters,
7171
cacheBuildManifest: Bool,
7272
packageGraphLoader: @escaping () throws -> PackageGraph,
73-
pluginInvoker: @escaping (PackageGraph) throws -> [ResolvedTarget: [PluginInvocationResult]],
73+
pluginInvoker: @escaping (PackageGraph) throws -> [ResolvedTarget: [BuildToolPluginInvocationResult]],
7474
outputStream: OutputByteStream,
7575
logLevel: Basics.Diagnostic.Severity,
7676
fileSystem: TSCBasic.FileSystem,
@@ -92,7 +92,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
9292
}
9393
}
9494

95-
public func getPluginInvocationResults(for graph: PackageGraph) throws -> [ResolvedTarget: [PluginInvocationResult]] {
95+
public func getPluginInvocationResults(for graph: PackageGraph) throws -> [ResolvedTarget: [BuildToolPluginInvocationResult]] {
9696
return try self.pluginInvoker(graph)
9797
}
9898

@@ -295,20 +295,20 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
295295

296296
/// Runs any prebuild commands associated with the given list of plugin invocation results, in order, and returns the
297297
/// results of running those prebuild commands.
298-
private func runPrebuildCommands(for pluginResults: [PluginInvocationResult]) throws -> [PrebuildCommandResult] {
298+
private func runPrebuildCommands(for pluginResults: [BuildToolPluginInvocationResult]) throws -> [PrebuildCommandResult] {
299299
// Run through all the commands from all the plugin usages in the target.
300300
return try pluginResults.map { pluginResult in
301301
// As we go we will collect a list of prebuild output directories whose contents should be input to the build,
302302
// and a list of the files in those directories after running the commands.
303303
var derivedSourceFiles: [AbsolutePath] = []
304304
var prebuildOutputDirs: [AbsolutePath] = []
305305
for command in pluginResult.prebuildCommands {
306-
self.observabilityScope.emit(info: "Running" + command.configuration.displayName)
306+
self.observabilityScope.emit(info: "Running" + (command.configuration.displayName ?? command.configuration.executable.basename))
307307

308308
// Run the command configuration as a subshell. This doesn't return until it is done.
309309
// TODO: We need to also use any working directory, but that support isn't yet available on all platforms at a lower level.
310310
// TODO: Invoke it in a sandbox that allows writing to only the temporary location.
311-
let commandLine = [command.configuration.executable] + command.configuration.arguments
311+
let commandLine = [command.configuration.executable.pathString] + command.configuration.arguments
312312
let processResult = try Process.popen(arguments: commandLine, environment: command.configuration.environment)
313313
let output = try processResult.utf8Output() + processResult.utf8stderrOutput()
314314
if processResult.exitStatus != .terminated(code: 0) {

Sources/Build/BuildPlan.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ public final class SwiftTargetBuildDescription {
602602
private(set) var moduleMap: AbsolutePath?
603603

604604
/// The results of applying any plugins to this target.
605-
public let pluginInvocationResults: [PluginInvocationResult]
605+
public let pluginInvocationResults: [BuildToolPluginInvocationResult]
606606

607607
/// The results of running any prebuild commands for this target.
608608
public let prebuildCommandResults: [PrebuildCommandResult]
@@ -612,7 +612,7 @@ public final class SwiftTargetBuildDescription {
612612
target: ResolvedTarget,
613613
toolsVersion: ToolsVersion,
614614
buildParameters: BuildParameters,
615-
pluginInvocationResults: [PluginInvocationResult] = [],
615+
pluginInvocationResults: [BuildToolPluginInvocationResult] = [],
616616
prebuildCommandResults: [PrebuildCommandResult] = [],
617617
isTestTarget: Bool? = nil,
618618
testDiscoveryTarget: Bool = false,
@@ -1424,7 +1424,7 @@ public class BuildPlan {
14241424
}
14251425

14261426
/// The results of invoking any plugins used by targets in this build.
1427-
public let pluginInvocationResults: [ResolvedTarget: [PluginInvocationResult]]
1427+
public let pluginInvocationResults: [ResolvedTarget: [BuildToolPluginInvocationResult]]
14281428

14291429
/// The results of running any prebuild commands for the targets in this build. This includes any derived
14301430
/// source files as well as directories to which any changes should cause us to reevaluate the build plan.
@@ -1523,7 +1523,7 @@ public class BuildPlan {
15231523
public convenience init(
15241524
buildParameters: BuildParameters,
15251525
graph: PackageGraph,
1526-
pluginInvocationResults: [ResolvedTarget: [PluginInvocationResult]] = [:],
1526+
pluginInvocationResults: [ResolvedTarget: [BuildToolPluginInvocationResult]] = [:],
15271527
prebuildCommandResults: [ResolvedTarget: [PrebuildCommandResult]] = [:],
15281528
diagnostics: DiagnosticsEngine,
15291529
fileSystem: FileSystem
@@ -1541,7 +1541,7 @@ public class BuildPlan {
15411541
public init(
15421542
buildParameters: BuildParameters,
15431543
graph: PackageGraph,
1544-
pluginInvocationResults: [ResolvedTarget: [PluginInvocationResult]] = [:],
1544+
pluginInvocationResults: [ResolvedTarget: [BuildToolPluginInvocationResult]] = [:],
15451545
prebuildCommandResults: [ResolvedTarget: [PrebuildCommandResult]] = [:],
15461546
fileSystem: FileSystem,
15471547
observabilityScope: ObservabilityScope

Sources/Build/LLBuildManifestBuilder.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -607,11 +607,12 @@ extension LLBuildManifestBuilder {
607607
// Add any regular build commands created by plugins for the target (prebuild commands are handled separately).
608608
for command in target.pluginInvocationResults.reduce([], { $0 + $1.buildCommands }) {
609609
// Create a shell command to invoke the executable. We include the path of the executable as a dependency, and make sure the name is unique.
610-
let execPath = AbsolutePath(command.configuration.executable, relativeTo: buildParameters.buildPath)
610+
let execPath = command.configuration.executable
611611
let uniquedName = ([execPath.pathString] + command.configuration.arguments).joined(separator: "|")
612+
let displayName = command.configuration.displayName ?? execPath.basename
612613
manifest.addShellCmd(
613-
name: command.configuration.displayName + "-" + ByteString(encodingAsUTF8: uniquedName).sha256Checksum,
614-
description: command.configuration.displayName,
614+
name: displayName + "-" + ByteString(encodingAsUTF8: uniquedName).sha256Checksum,
615+
description: displayName,
615616
inputs: [.file(execPath)] + command.inputFiles.map{ .file($0) },
616617
outputs: command.outputFiles.map{ .file($0) },
617618
arguments: [execPath.pathString] + command.configuration.arguments,

Sources/Commands/SwiftPackageTool.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -995,12 +995,12 @@ extension SwiftPackageTool {
995995
let delegateQueue: DispatchQueue
996996
var outputEmitter = PluginOutputEmitter()
997997

998-
func pluginEmittedOutput(data: Data) {
998+
func pluginEmittedOutput(_ data: Data) {
999999
dispatchPrecondition(condition: .onQueue(delegateQueue))
10001000
outputEmitter.emit(data: data)
10011001
}
10021002

1003-
func pluginEmittedDiagnostic(severity: PluginInvocationDiagnosticSeverity, message: String, file: String?, line: Int?) {
1003+
func pluginEmittedDiagnostic(_ diagnostic: Basics.Diagnostic) {
10041004
}
10051005
}
10061006
let pluginDelegate = PluginDelegate(swiftTool: swiftTool, delegateQueue: delegateQueue)

Sources/Commands/SwiftTool.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ public class SwiftTool {
732732
}
733733

734734
/// Invoke plugins for any reachable targets in the graph, and return a mapping from targets to corresponding evaluation results.
735-
func invokePlugins(graph: PackageGraph) throws -> [ResolvedTarget: [PluginInvocationResult]] {
735+
func invokePlugins(graph: PackageGraph) throws -> [ResolvedTarget: [BuildToolPluginInvocationResult]] {
736736
do {
737737
// Configure the plugin invocation inputs.
738738

@@ -763,7 +763,7 @@ public class SwiftTool {
763763
try localFileSystem.createDirectory(cacheDir, recursive: true)
764764

765765
// Ask the graph to invoke plugins, and return the result.
766-
let result = try graph.invokePlugins(
766+
let result = try graph.invokeBuildToolPlugins(
767767
outputDir: outputDir,
768768
builtToolsDir: builtToolsDir,
769769
buildEnvironment: buildEnvironment,

Sources/PackagePlugin/CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ add_library(PackagePlugin
1515
Path.swift
1616
Plugin.swift
1717
PluginInput.swift
18-
PluginOutput.swift
1918
Protocols.swift)
2019

2120
target_compile_options(PackagePlugin PUBLIC

Sources/PackagePlugin/Plugin.swift

Lines changed: 91 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ extension Plugin {
5454
/// Main entry point of the plugin — sets up a communication channel with
5555
/// the plugin host and runs the main message loop.
5656
public static func main() throws {
57-
// Duplicate the `stdin` file descriptor, which we will then use as an
58-
// input stream from which we receive messages from the plugin host.
57+
// Duplicate the `stdin` file descriptor, which we will then use for
58+
// receiving messages from the plugin host.
5959
let inputFD = dup(fileno(stdin))
6060
guard inputFD >= 0 else {
6161
internalError("Could not duplicate `stdin`: \(describe(errno: errno)).")
@@ -68,8 +68,8 @@ extension Plugin {
6868
internalError("Could not close `stdin`: \(describe(errno: errno)).")
6969
}
7070

71-
// Duplicate the `stdout` file descriptor, which we will then use as an
72-
// output stream to which we send messages to the plugin host.
71+
// Duplicate the `stdout` file descriptor, which we will then use for
72+
// sending messages to the plugin host.
7373
let outputFD = dup(fileno(stdout))
7474
guard outputFD >= 0 else {
7575
internalError("Could not dup `stdout`: \(describe(errno: errno)).")
@@ -89,7 +89,7 @@ extension Plugin {
8989
inputStream: FileHandle(fileDescriptor: inputFD),
9090
outputStream: FileHandle(fileDescriptor: outputFD))
9191

92-
// Process messages from the host until the input stream is closed,
92+
// Handle messages from the host until the input stream is closed,
9393
// indicating that we're done.
9494
while let message = try pluginHostConnection.waitForNextMessage() {
9595
try handleMessage(message)
@@ -98,13 +98,11 @@ extension Plugin {
9898

9999
fileprivate static func handleMessage(_ message: HostToPluginMessage) throws {
100100
switch message {
101-
// Invokes an action defined in the input JSON. This is an interim
102-
// message to bridge to the old logic; this will be separateed out
103-
// into different messages for different plugin capabilities, etc.
104-
// This will let us avoid the double encoded JSON.
101+
105102
case .performAction(let wireInput):
106-
// Decode the plugin input structure. We'll resolve this doubly
107-
// encoded JSON in an upcoming change.
103+
// Invokes an action defined in the input JSON. This is an interim
104+
// bridge to the old logic; the intent is to separate each action
105+
// into its own message type with customized input payload.
108106
let inputStruct: PluginInput
109107
do {
110108
inputStruct = try PluginInput(from: wireInput)
@@ -123,12 +121,13 @@ extension Plugin {
123121
// this is where we would set them up, most likely as properties
124122
// of the plugin instance (similar to how SwiftArgumentParser
125123
// allows commands to annotate arguments). It could use property
126-
// wrappers to mark up properties in the plugin.
124+
// wrappers to mark up properties in the plugin, and a separate
125+
// message could be used to query the plugin for its parameter
126+
// definitions.
127127
let plugin = self.init()
128128

129129
// Invoke the appropriate protocol method, based on the plugin
130130
// action that SwiftPM specified.
131-
let generatedCommands: [Command]
132131
switch inputStruct.pluginAction {
133132

134133
case .createBuildToolCommands(let target):
@@ -138,8 +137,39 @@ extension Plugin {
138137
throw PluginDeserializationError.malformedInputJSON("Plugin declared with `buildTool` capability but doesn't conform to `BuildToolPlugin` protocol")
139138
}
140139

141-
// Ask the plugin to create build commands for the target.
142-
generatedCommands = try plugin.createBuildCommands(context: context, target: target)
140+
// Invoke the plugin to create build commands for the target.
141+
let generatedCommands = try plugin.createBuildCommands(context: context, target: target)
142+
143+
// Send each of the generated commands to the host.
144+
for command in generatedCommands {
145+
switch command {
146+
147+
case let ._buildCommand(name, exec, args, env, workdir, inputs, outputs):
148+
let command = PluginToHostMessage.CommandConfiguration(
149+
displayName: name,
150+
executable: exec.string,
151+
arguments: args,
152+
environment: env,
153+
workingDirectory: workdir?.string)
154+
let message = PluginToHostMessage.defineBuildCommand(
155+
configuration: command,
156+
inputFiles: inputs.map{ $0.string },
157+
outputFiles: outputs.map{ $0.string })
158+
try pluginHostConnection.sendMessage(message)
159+
160+
case let ._prebuildCommand(name, exec, args, env, workdir, outdir):
161+
let command = PluginToHostMessage.CommandConfiguration(
162+
displayName: name,
163+
executable: exec.string,
164+
arguments: args,
165+
environment: env,
166+
workingDirectory: workdir?.string)
167+
let message = PluginToHostMessage.definePrebuildCommand(
168+
configuration: command,
169+
outputFilesDirectory: outdir.string)
170+
try pluginHostConnection.sendMessage(message)
171+
}
172+
}
143173

144174
case .performCommand(let targets, let arguments):
145175
// Check that the plugin implements the appropriate protocol
@@ -148,22 +178,32 @@ extension Plugin {
148178
throw PluginDeserializationError.malformedInputJSON("Plugin declared with `command` capability but doesn't conform to `CommandPlugin` protocol")
149179
}
150180

151-
// Invoke the plugin.
181+
// Invoke the plugin to perform its custom logic.
152182
try plugin.performCommand(context: context, targets: targets, arguments: arguments)
153-
154-
// For command plugin there are currently no return commands
155-
// (any commands invoked by the plugin are invoked directly).
156-
generatedCommands = []
157183
}
158184

159-
// Send back the output data (a JSON-encoded struct) to the plugin host.
160-
let outputStruct: PluginOutput
161-
do {
162-
outputStruct = try PluginOutput(commands: generatedCommands, diagnostics: Diagnostics.emittedDiagnostics)
163-
} catch {
164-
internalError("Couldn’t encode output JSON: \(error).")
185+
// Send any emitted diagnostics to the host.
186+
// FIXME: We should really be doing while diagnostics are emitted.
187+
for diagnostic in Diagnostics.emittedDiagnostics {
188+
let severity: PluginToHostMessage.DiagnosticSeverity
189+
switch diagnostic.severity {
190+
case .error:
191+
severity = .error
192+
case .warning:
193+
severity = .warning
194+
case .remark:
195+
severity = .remark
196+
}
197+
let message = PluginToHostMessage.emitDiagnostic(
198+
severity: severity,
199+
message: diagnostic.message,
200+
file: diagnostic.file?.string,
201+
line: diagnostic.line)
202+
try pluginHostConnection.sendMessage(message)
165203
}
166-
try pluginHostConnection.sendMessage(.pluginFinished(result: outputStruct.output))
204+
205+
// Send back a message to the host indicating that we're done.
206+
try pluginHostConnection.sendMessage(.actionComplete(success: true))
167207

168208
default:
169209
internalError("unexpected top-level message \(message)")
@@ -186,7 +226,6 @@ extension Plugin {
186226
/// Message channel for communicating with the plugin host.
187227
internal fileprivate(set) var pluginHostConnection: PluginHostConnection!
188228

189-
190229
/// A message that the host can send to the plugin.
191230
enum HostToPluginMessage: Decodable {
192231
/// The host is requesting that the plugin perform one of its declared plugin actions.
@@ -198,14 +237,34 @@ enum HostToPluginMessage: Decodable {
198237

199238
/// A message that the plugin can send to the host.
200239
enum PluginToHostMessage: Encodable {
201-
/// The plugin has finished the requested action and is returning a result.
202-
case pluginFinished(result: WireOutput)
203-
}
240+
/// The plugin emits a diagnostic.
241+
case emitDiagnostic(severity: DiagnosticSeverity, message: String, file: String?, line: Int?)
242+
243+
enum DiagnosticSeverity: String, Encodable {
244+
case error, warning, remark
245+
}
246+
247+
/// The plugin defines a build command.
248+
case defineBuildCommand(configuration: CommandConfiguration, inputFiles: [String], outputFiles: [String])
204249

250+
/// The plugin defines a prebuild command.
251+
case definePrebuildCommand(configuration: CommandConfiguration, outputFilesDirectory: String)
252+
253+
struct CommandConfiguration: Encodable {
254+
var displayName: String?
255+
var executable: String
256+
var arguments: [String]
257+
var environment: [String: String]
258+
var workingDirectory: String?
259+
}
260+
261+
/// The plugin has finished the requested action.
262+
case actionComplete(success: Bool)
263+
}
205264

206265
typealias PluginHostConnection = MessageConnection<PluginToHostMessage, HostToPluginMessage>
207266

208-
struct MessageConnection<TX,RX> where TX: Encodable, RX: Decodable {
267+
internal struct MessageConnection<TX,RX> where TX: Encodable, RX: Decodable {
209268
let inputStream: FileHandle
210269
let outputStream: FileHandle
211270

0 commit comments

Comments
 (0)