@@ -18,6 +18,144 @@ import TSCUtility
18
18
19
19
public typealias Diagnostic = Basics . Diagnostic
20
20
21
+ public enum PluginAction {
22
+ case createBuildToolCommands( target: ResolvedTarget )
23
+ case performCommand( targets: [ ResolvedTarget ] , arguments: [ String ] )
24
+ }
25
+
26
+ extension PluginTarget {
27
+ /// Invokes the plugin by compiling its source code (if needed) and then running it as a subprocess. The specified
28
+ /// plugin action determines which entry point is called in the subprocess, and the package and the tool mapping
29
+ /// determine the context that is available to the plugin.
30
+ ///
31
+ /// The working directory should be a path in the file system into which the plugin is allowed to write information
32
+ /// that persists between all invocations of a plugin for the same purpose. The exact meaning of "same" means here
33
+ /// depends on the particular plugin; for a build tool plugin, it might be the combination of the plugin and target
34
+ /// for which it is being invoked.
35
+ ///
36
+ /// Note that errors thrown by this function relate to problems actually invoking the plugin. Any diagnostics that
37
+ /// are emitted by the plugin are contained in the returned result structure.
38
+ ///
39
+ /// - Parameters:
40
+ /// - action: The plugin action (i.e. entry point) to invoke, possibly containing parameters.
41
+ /// - package: The root of the package graph to pass down to the plugin.
42
+ /// - scriptRunner: Entity responsible for actually running the code of the plugin.
43
+ /// - outputDirectory: A directory under which the plugin can write anything it wants to.
44
+ /// - toolNamesToPaths: A mapping from name of tools available to the plugin to the corresponding absolute paths.
45
+ /// - fileSystem: The file system to which all of the paths refers.
46
+ ///
47
+ /// - Returns: A PluginInvocationResult that contains the results of invoking the plugin.
48
+ public func invoke(
49
+ action: PluginAction ,
50
+ package : ResolvedPackage ,
51
+ buildEnvironment: BuildEnvironment ,
52
+ scriptRunner: PluginScriptRunner ,
53
+ outputDirectory: AbsolutePath ,
54
+ toolNamesToPaths: [ String : AbsolutePath ] ,
55
+ observabilityScope: ObservabilityScope ,
56
+ fileSystem: FileSystem
57
+ ) throws -> PluginInvocationResult {
58
+ // Create the plugin working directory if needed (but don't do anything with it if it already exists).
59
+ do {
60
+ try fileSystem. createDirectory ( outputDirectory, recursive: true )
61
+ }
62
+ catch {
63
+ throw PluginEvaluationError . outputDirectoryCouldNotBeCreated ( path: outputDirectory, underlyingError: error)
64
+ }
65
+
66
+ // Create the input context to send to the plugin.
67
+ // TODO: Some of this could probably be cached.
68
+ var serializer = PluginScriptRunnerInputSerializer ( buildEnvironment: buildEnvironment)
69
+ let inputStruct = try serializer. makePluginScriptRunnerInput (
70
+ rootPackage: package ,
71
+ pluginWorkDir: outputDirectory,
72
+ builtProductsDir: outputDirectory, // FIXME — what is this parameter needed for?
73
+ toolNamesToPaths: toolNamesToPaths,
74
+ pluginAction: action)
75
+
76
+ // Serialize the PluginEvaluationInput to JSON.
77
+ let encoder = JSONEncoder ( )
78
+ encoder. outputFormatting = [ . prettyPrinted, . sortedKeys, . withoutEscapingSlashes]
79
+ let inputJSON = try encoder. encode ( inputStruct)
80
+
81
+ // Call the plugin script runner to actually invoke the plugin.
82
+ // TODO: This should be asynchronous.
83
+ let ( outputJSON, outputText) = try scriptRunner. runPluginScript (
84
+ sources: sources,
85
+ inputJSON: inputJSON,
86
+ pluginArguments: [ ] ,
87
+ toolsVersion: self . apiVersion,
88
+ writableDirectories: [ outputDirectory] ,
89
+ observabilityScope: observabilityScope,
90
+ fileSystem: fileSystem)
91
+
92
+ // Deserialize the JSON to an PluginScriptRunnerOutput.
93
+ let outputStruct : PluginScriptRunnerOutput
94
+ do {
95
+ let decoder = JSONDecoder ( )
96
+ outputStruct = try decoder. decode ( PluginScriptRunnerOutput . self, from: outputJSON)
97
+ }
98
+ catch {
99
+ throw PluginEvaluationError . decodingPluginOutputFailed ( json: outputJSON, underlyingError: error)
100
+ }
101
+
102
+ // Generate emittable Diagnostics from the plugin output.
103
+ let diagnostics : [ Diagnostic ] = try outputStruct. diagnostics. map { diag in
104
+ let metadata : ObservabilityMetadata ? = try diag. file. map {
105
+ var metadata = ObservabilityMetadata ( )
106
+ metadata. fileLocation = try . init( . init( validating: $0) , line: diag. line)
107
+ return metadata
108
+ }
109
+
110
+ switch diag. severity {
111
+ case . error:
112
+ return . error( diag. message, metadata: metadata)
113
+ case . warning:
114
+ return . warning( diag. message, metadata: metadata)
115
+ case . remark:
116
+ return . info( diag. message, metadata: metadata)
117
+ }
118
+ }
119
+
120
+ // FIXME: Validate the plugin output structure here, e.g. paths, etc.
121
+
122
+ // Generate commands from the plugin output. This is where we translate from the transport JSON to our
123
+ // internal form. We deal with BuildCommands and PrebuildCommands separately.
124
+ // FIXME: This feels a bit too specific to have here.
125
+ // FIXME: Also there is too much repetition here, need to unify it.
126
+ let buildCommands = outputStruct. buildCommands. map { cmd in
127
+ PluginInvocationResult . BuildCommand (
128
+ configuration: . init(
129
+ displayName: cmd. displayName,
130
+ executable: cmd. executable,
131
+ arguments: cmd. arguments,
132
+ environment: cmd. environment,
133
+ workingDirectory: cmd. workingDirectory. map { AbsolutePath ( $0) } ) ,
134
+ inputFiles: cmd. inputFiles. map { AbsolutePath ( $0) } ,
135
+ outputFiles: cmd. outputFiles. map { AbsolutePath ( $0) } )
136
+ }
137
+ let prebuildCommands = outputStruct. prebuildCommands. map { cmd in
138
+ PluginInvocationResult . PrebuildCommand (
139
+ configuration: . init(
140
+ displayName: cmd. displayName,
141
+ executable: cmd. executable,
142
+ arguments: cmd. arguments,
143
+ environment: cmd. environment,
144
+ workingDirectory: cmd. workingDirectory. map { AbsolutePath ( $0) } ) ,
145
+ outputFilesDirectory: AbsolutePath ( cmd. outputFilesDirectory) )
146
+ }
147
+
148
+ // Create and return an evaluation result for the invocation.
149
+ return PluginInvocationResult (
150
+ plugin: self ,
151
+ diagnostics: diagnostics,
152
+ textOutput: String ( decoding: outputText, as: UTF8 . self) ,
153
+ buildCommands: buildCommands,
154
+ prebuildCommands: prebuildCommands)
155
+ }
156
+ }
157
+
158
+
21
159
extension PackageGraph {
22
160
23
161
/// Traverses the graph of reachable targets in a package graph, and applies plugins to targets as needed. Each
@@ -93,85 +231,20 @@ extension PackageGraph {
93
231
}
94
232
} )
95
233
96
- // Give each invocation of a plugin a separate output directory .
234
+ // Assign a plugin working directory based on the package, target, and plugin .
97
235
let pluginOutputDir = outputDir. appending ( components: package . identity. description, target. name, pluginTarget. name)
98
- do {
99
- try fileSystem. createDirectory ( pluginOutputDir, recursive: true )
100
- }
101
- catch {
102
- throw PluginEvaluationError . outputDirectoryCouldNotBeCreated ( path: pluginOutputDir, underlyingError: error)
103
- }
104
236
105
- // Create the input context to pass when applying the plugin to the target.
106
- var serializer = PluginScriptRunnerInputSerializer ( buildEnvironment: buildEnvironment)
107
- let pluginInput = try serializer. makePluginScriptRunnerInput (
108
- rootPackage: package ,
109
- pluginWorkDir: pluginOutputDir,
110
- builtProductsDir: builtToolsDir,
237
+ // Invoke the plugin.
238
+ let result = try pluginTarget. invoke (
239
+ action: . createBuildToolCommands( target: target) ,
240
+ package : package ,
241
+ buildEnvironment: buildEnvironment,
242
+ scriptRunner: pluginScriptRunner,
243
+ outputDirectory: pluginOutputDir,
111
244
toolNamesToPaths: toolNamesToPaths,
112
- pluginAction: . createBuildToolCommands( target: target) )
113
-
114
- // Run the plugin in the context of the target. The details of this are left to the plugin runner.
115
- // TODO: This should be asynchronous.
116
- let ( pluginOutput, emittedText) = try runPluginScript (
117
- sources: pluginTarget. sources,
118
- input: pluginInput,
119
- toolsVersion: package . manifest. toolsVersion,
120
- writableDirectories: [ pluginOutputDir] ,
121
- pluginScriptRunner: pluginScriptRunner,
122
245
observabilityScope: observabilityScope,
123
- fileSystem: fileSystem
124
- )
125
-
126
- // Generate emittable Diagnostics from the plugin output.
127
- let diagnostics : [ Diagnostic ] = try pluginOutput. diagnostics. map { diag in
128
- let metadata : ObservabilityMetadata ? = try diag. file. map {
129
- var metadata = ObservabilityMetadata ( )
130
- metadata. fileLocation = try . init( . init( validating: $0) , line: diag. line)
131
- return metadata
132
- }
133
-
134
- switch diag. severity {
135
- case . error:
136
- return . error( diag. message, metadata: metadata)
137
- case . warning:
138
- return . warning( diag. message, metadata: metadata)
139
- case . remark:
140
- return . info( diag. message, metadata: metadata)
141
- }
142
- }
143
-
144
- // Extract any emitted text output (received from the stdout/stderr of the plugin invocation).
145
- let textOutput = String ( decoding: emittedText, as: UTF8 . self)
146
-
147
- // FIXME: Validate the plugin output structure here, e.g. paths, etc.
148
-
149
- // Generate commands from the plugin output. This is where we translate from the transport JSON to our
150
- // internal form. We deal with BuildCommands and PrebuildCommands separately.
151
- let buildCommands = pluginOutput. buildCommands. map { cmd in
152
- PluginInvocationResult . BuildCommand (
153
- configuration: . init(
154
- displayName: cmd. displayName,
155
- executable: cmd. executable,
156
- arguments: cmd. arguments,
157
- environment: cmd. environment,
158
- workingDirectory: cmd. workingDirectory. map { AbsolutePath ( $0) } ) ,
159
- inputFiles: cmd. inputFiles. map { AbsolutePath ( $0) } ,
160
- outputFiles: cmd. outputFiles. map { AbsolutePath ( $0) } )
161
- }
162
- let prebuildCommands = pluginOutput. prebuildCommands. map { cmd in
163
- PluginInvocationResult . PrebuildCommand (
164
- configuration: . init(
165
- displayName: cmd. displayName,
166
- executable: cmd. executable,
167
- arguments: cmd. arguments,
168
- environment: cmd. environment,
169
- workingDirectory: cmd. workingDirectory. map { AbsolutePath ( $0) } ) ,
170
- outputFilesDirectory: AbsolutePath ( cmd. outputFilesDirectory) )
171
- }
172
-
173
- // Create an evaluation result from the usage of the plugin by the target.
174
- pluginResults. append ( PluginInvocationResult ( plugin: pluginTarget, diagnostics: diagnostics, textOutput: textOutput, buildCommands: buildCommands, prebuildCommands: prebuildCommands) )
246
+ fileSystem: fileSystem)
247
+ pluginResults. append ( result)
175
248
}
176
249
177
250
// Associate the list of results with the target. The list will have one entry for each plugin used by the target.
@@ -200,6 +273,7 @@ extension PackageGraph {
200
273
let ( outputJSON, stdoutText) = try pluginScriptRunner. runPluginScript (
201
274
sources: sources,
202
275
inputJSON: inputJSON,
276
+ pluginArguments: [ ] ,
203
277
toolsVersion: toolsVersion,
204
278
writableDirectories: writableDirectories,
205
279
observabilityScope: observabilityScope,
@@ -341,6 +415,7 @@ public protocol PluginScriptRunner {
341
415
func runPluginScript(
342
416
sources: Sources ,
343
417
inputJSON: Data ,
418
+ pluginArguments: [ String ] ,
344
419
toolsVersion: ToolsVersion ,
345
420
writableDirectories: [ AbsolutePath ] ,
346
421
observabilityScope: ObservabilityScope ,
@@ -380,6 +455,7 @@ struct PluginScriptRunnerInput: Codable {
380
455
/// the capabilities declared for the plugin.
381
456
enum PluginAction : Codable {
382
457
case createBuildToolCommands( targetId: Target . Id )
458
+ case performCommand( targetIds: [ Target . Id ] , arguments: [ String ] )
383
459
}
384
460
385
461
/// A single absolute path in the wire structure, represented as a tuple
@@ -551,13 +627,6 @@ struct PluginScriptRunnerInputSerializer {
551
627
var packages : [ PluginScriptRunnerInput . Package ] = [ ]
552
628
var packagesToIds : [ ResolvedPackage : PluginScriptRunnerInput . Package . Id ] = [ : ]
553
629
554
- /// The action that SwiftPM wants to invoke on the plugin.
555
- enum PluginAction {
556
- case createBuildToolCommands( target: ResolvedTarget )
557
- }
558
-
559
- /// Constructs the codable, serialized input to the plugin, based on a
560
- /// plugin action, a package subgraph, and other parameters.
561
630
mutating func makePluginScriptRunnerInput(
562
631
rootPackage: ResolvedPackage ,
563
632
pluginWorkDir: AbsolutePath ,
@@ -573,8 +642,10 @@ struct PluginScriptRunnerInputSerializer {
573
642
switch pluginAction {
574
643
case . createBuildToolCommands( let target) :
575
644
serializedPluginAction = . createBuildToolCommands( targetId: try serialize ( target: target) !)
645
+ case . performCommand( let targets, let arguments) :
646
+ serializedPluginAction = . performCommand( targetIds: try targets. compactMap { try serialize ( target: $0) } , arguments: arguments)
576
647
}
577
- let input = PluginScriptRunnerInput (
648
+ return PluginScriptRunnerInput (
578
649
paths: paths,
579
650
targets: targets,
580
651
products: products,
@@ -584,7 +655,6 @@ struct PluginScriptRunnerInputSerializer {
584
655
builtProductsDirId: builtProductsDirId,
585
656
toolNamesToPathIds: toolNamesToPathIds,
586
657
pluginAction: serializedPluginAction)
587
- return input
588
658
}
589
659
590
660
/// Adds a path to the serialized structure, if it isn't already there.
0 commit comments