Skip to content

Commit c4c4e4b

Browse files
committed
Extend the previous logic for sending information to and from the plugin to allow multiple messages, and rework some of the API for calling the plugin to use this. This sets up for allowing the plugin to call back to the host to invoke actions or return information.
1 parent 22b65b2 commit c4c4e4b

File tree

6 files changed

+284
-200
lines changed

6 files changed

+284
-200
lines changed

Sources/PackagePlugin/Plugin.swift

Lines changed: 131 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -94,73 +94,144 @@ extension Plugin {
9494
// Open input and output handles for read from and writing to the host.
9595
let inputHandle = FileHandle(fileDescriptor: inputFD)
9696
let outputHandle = FileHandle(fileDescriptor: outputFD)
97-
98-
// Read the input data (a JSON-encoded struct) from the host. It has
99-
// all the input context for the plugin invocation.
100-
guard let inputData = try inputHandle.readToEnd() else {
101-
internalError("Couldn’t read input JSON.")
102-
}
103-
let inputStruct: PluginInput
104-
do {
105-
inputStruct = try PluginInput(from: inputData)
106-
} catch {
107-
internalError("Couldn’t decode input JSON: \(error).")
108-
}
109-
110-
// Construct a PluginContext from the deserialized input.
111-
let context = PluginContext(
112-
package: inputStruct.package,
113-
pluginWorkDirectory: inputStruct.pluginWorkDirectory,
114-
builtProductsDirectory: inputStruct.builtProductsDirectory,
115-
toolNamesToPaths: inputStruct.toolNamesToPaths)
116-
117-
// Instantiate the plugin. For now there are no parameters, but this is
118-
// where we would set them up, most likely as properties of the plugin
119-
// instance (in a manner similar to SwiftArgumentParser). This would
120-
// use property wrappers to mark up properties in the plugin.
121-
let plugin = self.init()
12297

123-
// Invoke the appropriate protocol method, based on the plugin action
124-
// that SwiftPM specified.
125-
let generatedCommands: [Command]
126-
switch inputStruct.pluginAction {
98+
// Read and process messages from the plugin host. At present there is
99+
// only one action message per plugin invocation followed by a message
100+
// to exit, but this can be extended in the future.
101+
while let message = try inputHandle.readPluginMessage() {
102+
switch message {
103+
// Invokes an action defined in the input JSON. This is an interim
104+
// message to bridge to the old logic; this will be separateed out
105+
// into different messages for different plugin capabilities, etc.
106+
// This will let us avoid the double encoded JSON.
107+
case .performAction(let wireInput):
108+
// Decode the plugin input structure. We'll resolve this doubly
109+
// encoded JSON in an upcoming change.
110+
let inputStruct: PluginInput
111+
do {
112+
inputStruct = try PluginInput(from: wireInput)
113+
} catch {
114+
internalError("Couldn’t decode input JSON: \(error).")
115+
}
116+
117+
// Construct a PluginContext from the deserialized input.
118+
let context = PluginContext(
119+
package: inputStruct.package,
120+
pluginWorkDirectory: inputStruct.pluginWorkDirectory,
121+
builtProductsDirectory: inputStruct.builtProductsDirectory,
122+
toolNamesToPaths: inputStruct.toolNamesToPaths)
123+
124+
// Instantiate the plugin. For now there are no parameters, but
125+
// this is where we would set them up, most likely as properties
126+
// of the plugin instance (similar to how SwiftArgumentParser
127+
// allows commands to annotate arguments). It could use property
128+
// wrappers to mark up properties in the plugin.
129+
let plugin = self.init()
130+
131+
// Invoke the appropriate protocol method, based on the plugin
132+
// action that SwiftPM specified.
133+
let generatedCommands: [Command]
134+
switch inputStruct.pluginAction {
135+
136+
case .createBuildToolCommands(let target):
137+
// Check that the plugin implements the appropriate protocol
138+
// for its declared capability.
139+
guard let plugin = plugin as? BuildToolPlugin else {
140+
throw PluginDeserializationError.malformedInputJSON("Plugin declared with `buildTool` capability but doesn't conform to `BuildToolPlugin` protocol")
141+
}
142+
143+
// Ask the plugin to create build commands for the target.
144+
generatedCommands = try plugin.createBuildCommands(context: context, target: target)
145+
146+
case .performCommand(let targets, let arguments):
147+
// Check that the plugin implements the appropriate protocol
148+
// for its declared capability.
149+
guard let plugin = plugin as? CommandPlugin else {
150+
throw PluginDeserializationError.malformedInputJSON("Plugin declared with `command` capability but doesn't conform to `CommandPlugin` protocol")
151+
}
152+
153+
// Invoke the plugin.
154+
try plugin.performCommand(context: context, targets: targets, arguments: arguments)
155+
156+
// For command plugin there are currently no return commands
157+
// (any commands invoked by the plugin are invoked directly).
158+
generatedCommands = []
159+
}
160+
161+
// Send back the output data (a JSON-encoded struct) to the plugin host.
162+
let outputStruct: PluginOutput
163+
do {
164+
outputStruct = try PluginOutput(commands: generatedCommands, diagnostics: Diagnostics.emittedDiagnostics)
165+
} catch {
166+
internalError("Couldn’t encode output JSON: \(error).")
167+
}
168+
try outputHandle.writePluginMessage(.provideResult(output: outputStruct.output))
127169

128-
case .createBuildToolCommands(let target):
129-
// Check that the plugin implements the appropriate protocol for its
130-
// declared capability.
131-
guard let plugin = plugin as? BuildToolPlugin else {
132-
throw PluginDeserializationError.malformedInputJSON("Plugin declared with `buildTool` capability but doesn't conform to `BuildToolPlugin` protocol")
133-
}
134-
135-
// Ask the plugin to create build commands for the input target.
136-
generatedCommands = try plugin.createBuildCommands(context: context, target: target)
137-
138-
case .performCommand(let targets, let arguments):
139-
// Check that the plugin implements the appropriate protocol for its
140-
// declared capability.
141-
guard let plugin = plugin as? CommandPlugin else {
142-
throw PluginDeserializationError.malformedInputJSON("Plugin declared with `command` capability but doesn't conform to `CommandPlugin` protocol")
143-
}
144-
145-
// Invoke the plugin.
146-
try plugin.performCommand(context: context, targets: targets, arguments: arguments)
170+
// Exits the plugin logic.
171+
case .quit:
172+
exit(0)
147173

148-
// For command plugin there are currently no return commands (any
149-
// commands invoked by the plugin are invoked directly).
150-
generatedCommands = []
151-
}
152-
153-
// Send back the output data (a JSON-encoded struct) to the plugin host.
154-
let outputStruct: PluginOutput
155-
do {
156-
outputStruct = try PluginOutput(commands: generatedCommands, diagnostics: Diagnostics.emittedDiagnostics)
157-
} catch {
158-
internalError("Couldn’t encode output JSON: \(error).")
174+
// Ignore other messages
175+
default:
176+
continue
177+
}
159178
}
160-
try outputHandle.write(contentsOf: outputStruct.outputData)
161179
}
162180

163181
public static func main() throws {
164182
try self.main(CommandLine.arguments)
165183
}
166184
}
185+
186+
187+
/// A message that the host can send to the plugin.
188+
enum HostToPluginMessage: Decodable {
189+
case performAction(input: WireInput)
190+
case quit
191+
}
192+
193+
/// A message that the plugin can send to the host.
194+
enum PluginToHostMessage: Encodable {
195+
case provideResult(output: WireOutput)
196+
}
197+
198+
fileprivate extension FileHandle {
199+
200+
func writePluginMessage(_ message: PluginToHostMessage) throws {
201+
// Encode the message as JSON.
202+
let payload = try JSONEncoder().encode(message)
203+
204+
// Form the header (a 12-digit length field in base-ten ASCII).
205+
try self.write(contentsOf: Data(String(format: "%012u", payload.count).utf8))
206+
207+
// The data is the header followed by the payload.
208+
try self.write(contentsOf: payload)
209+
}
210+
211+
func readPluginMessage() throws -> HostToPluginMessage? {
212+
// Read the header (a 12-digit length field in base-ten ASCII).
213+
guard let header = try self.read(upToCount: 12) else { return nil }
214+
guard header.count == 12 else {
215+
throw PluginMessageError.truncatedHeader
216+
}
217+
218+
// Decode the count.
219+
guard let count = Int(String(decoding: header, as: UTF8.self)), count >= 2 else {
220+
throw PluginMessageError.invalidPayloadSize
221+
}
222+
223+
// Read the JSON payload.
224+
guard let payload = try self.read(upToCount: count), payload.count == count else {
225+
throw PluginMessageError.truncatedPayload
226+
}
227+
228+
// Decode and return the message.
229+
return try JSONDecoder().decode(HostToPluginMessage.self, from: payload)
230+
}
231+
232+
enum PluginMessageError: Swift.Error {
233+
case truncatedHeader
234+
case invalidPayloadSize
235+
case truncatedPayload
236+
}
237+
}

Sources/PackagePlugin/PluginInput.swift

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,8 @@ struct PluginInput {
2323
case performCommand(targets: [Target], arguments: [String])
2424
}
2525

26-
internal init(from data: Data) throws {
27-
// Decode the input JSON, which is expected to be the serialized form
28-
// of a `WireInput` structure.
29-
let decoder = JSONDecoder()
30-
let input = try decoder.decode(WireInput.self, from: data)
31-
32-
// Create a deserializer to unpack the decoded input structures.
26+
internal init(from input: WireInput) throws {
27+
// Create a deserializer to unpack the input structures.
3328
var deserializer = PluginInputDeserializer(with: input)
3429

3530
// Unpack the individual pieces from which we'll create the plugin context.
@@ -285,7 +280,7 @@ fileprivate struct PluginInputDeserializer {
285280
/// of flat structures for each kind of entity. All references to entities use
286281
/// ID numbers that correspond to the indices into these arrays. The directed
287282
/// acyclic graph is then deserialized from this structure.
288-
fileprivate struct WireInput: Decodable {
283+
internal struct WireInput: Decodable {
289284
let paths: [Path]
290285
let targets: [Target]
291286
let products: [Product]

Sources/PackagePlugin/PluginOutput.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
/// SwiftPM. The output structure is currently much simpler than the input
1515
/// (which is actually a directed acyclic graph), and can be directly encoded.
1616
struct PluginOutput {
17-
let outputData: Data
17+
let output: WireOutput
1818

1919
public init(commands: [Command], diagnostics: [Diagnostic]) throws {
2020
// Construct a `WireOutput` struture containing the information that
@@ -43,18 +43,16 @@ struct PluginOutput {
4343
}
4444
}
4545

46-
// Encode the output structure to JSON, and keep it around until asked.
47-
let encoder = JSONEncoder()
48-
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
49-
self.outputData = try encoder.encode(output)
46+
// Keep the output structure around until asked for it.
47+
self.output = output
5048
}
5149
}
5250

5351

5452

5553
/// The output structure sent as JSON to SwiftPM. This structure is currently
5654
/// much simpler than the input structure (which is a directed acyclic graph).
57-
fileprivate struct WireOutput: Encodable {
55+
internal struct WireOutput: Encodable {
5856
var buildCommands: [BuildCommand] = []
5957
var prebuildCommands: [PrebuildCommand] = []
6058
var diagnostics: [Diagnostic] = []

0 commit comments

Comments
 (0)