16
16
17
17
@_implementationOnly import Foundation
18
18
#if os(Windows)
19
- @_implementationOnly import ucrt // for stdio functions
19
+ @_implementationOnly import ucrt
20
20
#endif
21
21
22
- // The way in which SwiftPM communicates with the plugin is an implementation
23
- // detail, but the way it currently works is that the plugin is compiled (in
24
- // a very similar way to the package manifest) and then run in a sandbox.
22
+ // The specifics of how SwiftPM communicates with the plugin are implementation
23
+ // details, but the way it currently works is that the plugin is compiled as an
24
+ // executable and then run in a sandbox that blocks network access and prevents
25
+ // changes to all except a few file system locations.
25
26
//
26
- // Currently the plugin input is provided in the form of a JSON-encoded input
27
- // structure passed as the last command line argument; however, this will very
28
- // likely change so that it is instead passed on `stdin` of the process that
29
- // runs the plugin, since that avoids any command line length limitations.
27
+ // The "plugin host" (SwiftPM or an IDE using libSwiftPM) sends a JSON-encoded
28
+ // context struct to the plugin process on its original standard-input pipe, and
29
+ ///when finished, the plugin sends a JSON-encoded result struct back to the host
30
+ // on its original standard-output pipe. The plugin host treats output on the
31
+ // standard-error pipe as free-form output text from the plugin (for debugging
32
+ // purposes, etc).
33
+
34
+ // Within the plugin process, `stdout` is redirected to `stderr` so that print
35
+ // statements from the plugin are treated as plain-text output, and `stdin` is
36
+ // closed so that attemps by the plugin logic to read from console input return
37
+ // errors instead of blocking. The original `stdin` and `stdout` are duplicated
38
+ // for use as messaging pipes, and are not directly used by the plugin logic.
30
39
//
31
- // An output structure containing any generated commands and diagnostics is
32
- // passed back to SwiftPM on `stdout`. All freeform output from the plugin
33
- // is redirected to `stderr`, which SwiftPM shows to the user without inter-
34
- // preting it in any way.
40
+ // Using the standard input and output streams avoids having to make allowances
41
+ // in the sandbox for other channels of communication, and seems a more portable
42
+ // approach than many of the alternatives.
35
43
//
36
- // The exit code of the compiled plugin determines success or failure (though
37
- // failure to decode the output is also considered a failure to run the ex-
38
- // tension) .
44
+ // The exit code of the plugin process determines whether the plugin invocation
45
+ // is considered successful. A failure result should also be accompanied by an
46
+ // emitted error diagnostic, so that errors are understandable by the user .
39
47
40
48
extension Plugin {
41
49
42
50
public static func main( _ arguments: [ String ] ) throws {
43
-
44
- // Use the initial `stdout` for returning JSON, and redirect `stdout`
45
- // to `stderr` for capturing freeform text.
46
- let jsonOut = fdopen ( dup ( fileno ( stdout) ) , " w " )
47
- dup2 ( fileno ( stderr) , fileno ( stdout) )
48
-
49
- // Close `stdin` to avoid blocking if the plugin tries to read input.
50
- close ( fileno ( stdin) )
51
-
52
- // Private function for reporting internal errors and halting execution.
51
+ // Private function to report internal errors and then exit.
53
52
func internalError( _ message: String ) -> Never {
54
53
Diagnostics . error ( " Internal Error: \( message) " )
55
54
fputs ( " Internal Error: \( message) " , stderr)
56
55
exit ( 1 )
57
56
}
58
57
59
- // Look for the input JSON as the last argument of the invocation.
60
- guard let inputData = ProcessInfo . processInfo. arguments. last? . data ( using: . utf8) else {
61
- internalError ( " Expected last argument to contain JSON input data in UTF-8 encoding, but didn't find it. " )
58
+ // Private function to construct an error message from an `errno` code.
59
+ func describe( errno: Int32 ) -> String {
60
+ if let cStr = strerror ( errno) { return String ( cString: cStr) }
61
+ return String ( describing: errno)
62
62
}
63
63
64
- // Deserialize the input JSON.
65
- let input : PluginInput
64
+ // Duplicate the `stdin` file descriptor, which we will then use as an
65
+ // input stream from which we receive messages from the plugin host.
66
+ let inputFD = dup ( fileno ( stdin) )
67
+ guard inputFD >= 0 else {
68
+ internalError ( " Could not duplicate `stdin`: \( describe ( errno: errno) ) . " )
69
+ }
70
+
71
+ // Having duplicated the original standard-input descriptor, we close
72
+ // `stdin` so that attempts by the plugin to read console input (which
73
+ // are usually a mistake) return errors instead of blocking.
74
+ guard close ( fileno ( stdin) ) >= 0 else {
75
+ internalError ( " Could not close `stdin`: \( describe ( errno: errno) ) . " )
76
+ }
77
+
78
+ // Duplicate the `stdout` file descriptor, which we will then use as a
79
+ // message stream to which we send output to the plugin host.
80
+ let outputFD = dup ( fileno ( stdout) )
81
+ guard outputFD >= 0 else {
82
+ internalError ( " Could not dup `stdout`: \( describe ( errno: errno) ) . " )
83
+ }
84
+
85
+ // Having duplicated the original standard-output descriptor, redirect
86
+ // `stdout` to `stderr` so that all free-form text output goes there.
87
+ guard dup2 ( fileno ( stderr) , fileno ( stdout) ) >= 0 else {
88
+ internalError ( " Could not dup2 `stdout` to `stderr`: \( describe ( errno: errno) ) . " )
89
+ }
90
+
91
+ // Turn off full buffering so printed text appears as soon as possible.
92
+ setlinebuf ( stdout)
93
+
94
+ // Open input and output handles for read from and writing to the host.
95
+ let inputHandle = FileHandle ( fileDescriptor: inputFD)
96
+ 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
66
104
do {
67
- input = try PluginInput ( from: inputData)
105
+ inputStruct = try PluginInput ( from: inputData)
68
106
} catch {
69
107
internalError ( " Couldn’t decode input JSON: \( error) . " )
70
108
}
71
109
72
110
// Construct a PluginContext from the deserialized input.
73
111
let context = PluginContext (
74
- package : input . package ,
75
- pluginWorkDirectory: input . pluginWorkDirectory,
76
- builtProductsDirectory: input . builtProductsDirectory,
77
- toolNamesToPaths: input . toolNamesToPaths)
112
+ package : inputStruct . package ,
113
+ pluginWorkDirectory: inputStruct . pluginWorkDirectory,
114
+ builtProductsDirectory: inputStruct . builtProductsDirectory,
115
+ toolNamesToPaths: inputStruct . toolNamesToPaths)
78
116
79
117
// Instantiate the plugin. For now there are no parameters, but this is
80
118
// where we would set them up, most likely as properties of the plugin
@@ -85,7 +123,7 @@ extension Plugin {
85
123
// Invoke the appropriate protocol method, based on the plugin action
86
124
// that SwiftPM specified.
87
125
let generatedCommands : [ Command ]
88
- switch input . pluginAction {
126
+ switch inputStruct . pluginAction {
89
127
90
128
case . createBuildToolCommands( let target) :
91
129
// Check that the plugin implements the appropriate protocol for its
@@ -112,18 +150,14 @@ extension Plugin {
112
150
generatedCommands = [ ]
113
151
}
114
152
115
- // Construct the output structure to send back to SwiftPM .
116
- let output : PluginOutput
153
+ // Send back the output data (a JSON-encoded struct) to the plugin host .
154
+ let outputStruct : PluginOutput
117
155
do {
118
- output = try PluginOutput ( commands: generatedCommands, diagnostics: Diagnostics . emittedDiagnostics)
156
+ outputStruct = try PluginOutput ( commands: generatedCommands, diagnostics: Diagnostics . emittedDiagnostics)
119
157
} catch {
120
158
internalError ( " Couldn’t encode output JSON: \( error) . " )
121
159
}
122
-
123
- // On stdout, write a zero byte followed by the JSON data — this is what libSwiftPM expects to see. Anything before the last zero byte is treated as freeform output from the plugin (such as debug output from `print` statements). Since `FileHandle.write()` doesn't obey buffering we first have to flush any existing output.
124
- if fwrite ( [ UInt8] ( output. outputData) , 1 , output. outputData. count, jsonOut) != output. outputData. count {
125
- internalError ( " Couldn’t write output JSON: \( strerror ( errno) . map { String ( cString: $0) } ?? String ( describing: errno) ) . " )
126
- }
160
+ try outputHandle. write ( contentsOf: outputStruct. outputData)
127
161
}
128
162
129
163
public static func main( ) throws {
0 commit comments