Skip to content

Commit 0fb7392

Browse files
committed
Implement proposed adjustments to SE-0303 to use @main for plugins
This is a WIP implementation of the proposed adjustments to SE-0303 in swiftlang/swift-evolution#1434. This changes the entry point to use `@main` rather than top-level code, and introduces protocols for the entry points corresponding to the various plugin capabilities. It gets rid of the globals in favour of plugin parameters and return values (except for diagnostics, which are still collected using a process-wide list).
1 parent 603d7ee commit 0fb7392

File tree

10 files changed

+253
-187
lines changed

10 files changed

+253
-187
lines changed
Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,38 @@
11
import PackagePlugin
22

3-
print("Hello from the Build Tool Plugin!")
4-
5-
for inputFile in targetBuildContext.inputFiles.filter({ $0.path.extension == "dat" }) {
6-
let inputPath = inputFile.path
7-
let outputName = inputPath.stem + ".swift"
8-
let outputPath = targetBuildContext.pluginWorkDirectory.appending(outputName)
9-
commandConstructor.addBuildCommand(
10-
displayName:
11-
"Generating \(outputName) from \(inputPath.lastComponent)",
12-
executable:
13-
try targetBuildContext.tool(named: "MySourceGenBuildTool").path,
14-
arguments: [
15-
"\(inputPath)",
16-
"\(outputPath)"
17-
],
18-
environment: [
19-
"VARIABLE_NAME_PREFIX": "PREFIX_"
20-
],
21-
inputFiles: [
22-
inputPath,
23-
],
24-
outputFiles: [
25-
outputPath
3+
@main struct MyPlugin: BuildToolPlugin {
4+
5+
func createBuildCommands(context: TargetBuildContext) throws -> [Command] {
6+
print("running plugin with context \(context)")
7+
return [
8+
.buildCommand(displayName: "Do something", executable: "echo", arguments: ["bla", "bla", "bla"])
269
]
27-
)
10+
print("Hello from the Build Tool Plugin!")
11+
12+
let inputFiles = targetBuildContext.inputFiles.filter({ $0.path.extension == "dat" })
13+
return inputFiles.map {
14+
let inputPath = inputFile.path
15+
let outputName = inputPath.stem + ".swift"
16+
let outputPath = targetBuildContext.pluginWorkDirectory.appending(outputName)
17+
return .buildCommand(
18+
displayName:
19+
"Generating \(outputName) from \(inputPath.lastComponent)",
20+
executable:
21+
try targetBuildContext.tool(named: "MySourceGenBuildTool").path,
22+
arguments: [
23+
"\(inputPath)",
24+
"\(outputPath)"
25+
],
26+
environment: [
27+
"VARIABLE_NAME_PREFIX": "PREFIX_"
28+
],
29+
inputFiles: [
30+
inputPath,
31+
],
32+
outputFiles: [
33+
outputPath
34+
]
35+
)
36+
}
37+
}
2838
}

Sources/PackagePlugin/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ add_library(PackagePlugin
1313
PublicAPI/Globals.swift
1414
PublicAPI/Path.swift
1515
PublicAPI/TargetBuildContext.swift
16-
ImplementationDetails.swift)
16+
Implementation.swift)
1717

1818
target_compile_options(PackagePlugin PUBLIC
1919
$<$<COMPILE_LANGUAGE:Swift>:-package-description-version$<SEMICOLON>999.0>)
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
@_implementationOnly import Foundation
12+
13+
// The way in which SwiftPM communicates with the package plugin is an im-
14+
// plementation detail, but the way it currently works is that the plugin
15+
// is compiled (in a very similar way to the package manifest) and then run in
16+
// a sandbox. Currently it is passed the JSON encoded input structure as the
17+
// last command line argument; however, it this will likely change to instead
18+
// read it from stdin, since that avoids any command line length limitation.
19+
// Any generated commands and diagnostics are emitted on stdout after a zero
20+
// byte; this allows regular output, such as print statements for debugging,
21+
// to be emitted to SwiftPM verbatim. SwiftPM tries to interpret any stdout
22+
// contents after the last zero byte as a JSON encoded output struct in UTF-8
23+
// encoding; any failure to decode it is considered a protocol failure. The
24+
// exit code of the compiled plugin determines success or failure (though
25+
// failure to decode the output is also considered a failure to run the ex-
26+
// tension).
27+
28+
extension Plugin {
29+
30+
public static func main() throws {
31+
// Look for the input JSON as the last argument of the invocation.
32+
guard let inputData = ProcessInfo.processInfo.arguments.last?.data(using: .utf8) else {
33+
fputs("Expected last argument to contain JSON input data in UTF-8 encoding, but didn't find it.", stderr)
34+
output.diagnostics.append(Diagnostic(severity: .error, message: "Expected last argument to contain JSON input data in UTF-8 encoding, but didn't find it.", file: nil, line: nil))
35+
exit(1)
36+
}
37+
38+
// Decode the input JSON into a plugin context.
39+
let context: TargetBuildContext
40+
do {
41+
let decoder = JSONDecoder()
42+
context = try decoder.decode(TargetBuildContext.self, from: inputData)
43+
}
44+
catch {
45+
fputs("Couldn't decode input JSON (reason: \(error)", stderr)
46+
output.diagnostics.append(Diagnostic(severity: .error, message: "\(error)", file: nil, line: nil))
47+
exit(1)
48+
}
49+
50+
// Instantiate the plugin. For now there are no parameters, but this is where we would set them up, most likely as properties of the plugin.
51+
let plugin = self.init()
52+
53+
// Invoke the appropriate protocol method based on the action.
54+
switch context.pluginAction {
55+
case .createBuildToolCommands:
56+
// Check that the plugin conforms to `BuildToolPlugin`, and get the commands.
57+
guard let plugin = plugin as? BuildToolPlugin else { throw PluginDeserializationError.malformedInputJSON("...") }
58+
let commands = try plugin.createBuildCommands(context: context)
59+
60+
// Convert the commands to the encodable output representation SwiftPM currently expects.
61+
output.buildCommands = commands.compactMap {
62+
guard case let ._buildCommand(displayName, executable, arguments, environment, workingDir, inputFiles, outputFiles) = $0 else { return .none }
63+
return BuildCommand(displayName: displayName, executable: executable, arguments: arguments, environment: environment, workingDirectory: workingDir, inputFiles: inputFiles, outputFiles: outputFiles)
64+
}
65+
output.prebuildCommands = commands.compactMap {
66+
guard case let ._prebuildCommand(displayName, executable, arguments, environment, workingDir, outputFilesDir) = $0 else { return .none }
67+
return PrebuildCommand(displayName: displayName, executable: executable, arguments: arguments, environment: environment, workingDirectory: workingDir, outputFilesDirectory: outputFilesDir)
68+
}
69+
}
70+
71+
// Emit the output from the plugin for SwiftPM to read.
72+
let encoder = JSONEncoder()
73+
let outputData = try! encoder.encode(output)
74+
fputc(0, stdout)
75+
fputs(String(data: outputData, encoding: .utf8)!, stdout)
76+
fflush(stdout)
77+
}
78+
}
79+
80+
/// Private structures containing the information to send back to SwiftPM.
81+
82+
struct BuildCommand: Encodable {
83+
let displayName: String?
84+
let executable: Path
85+
let arguments: [String]
86+
let environment: [String: String]
87+
let workingDirectory: Path?
88+
let inputFiles: [Path]
89+
let outputFiles: [Path]
90+
}
91+
92+
struct PrebuildCommand: Encodable {
93+
let displayName: String?
94+
let executable: Path
95+
let arguments: [String]
96+
let environment: [String: String]
97+
let workingDirectory: Path?
98+
let outputFilesDirectory: Path
99+
}
100+
101+
struct Diagnostic: Encodable {
102+
enum Severity: String, Encodable {
103+
case error, warning, remark
104+
}
105+
let severity: Severity
106+
let message: String
107+
let file: Path?
108+
let line: Int?
109+
}
110+
111+
struct OutputStruct: Encodable {
112+
let version: Int
113+
var diagnostics: [Diagnostic] = []
114+
var buildCommands: [BuildCommand] = []
115+
var prebuildCommands: [PrebuildCommand] = []
116+
}
117+
118+
var output = OutputStruct(version: 1)
119+
120+
public enum PluginDeserializationError: Error {
121+
/// The input JSON is malformed in some way; the message provides more details.
122+
case malformedInputJSON(_ message: String)
123+
}
124+
125+
extension PluginDeserializationError: CustomStringConvertible {
126+
public var description: String {
127+
switch self {
128+
case .malformedInputJSON(let message):
129+
return "Malformed input JSON: \(message)"
130+
}
131+
}
132+
}

Sources/PackagePlugin/ImplementationDetails.swift

Lines changed: 0 additions & 96 deletions
This file was deleted.

0 commit comments

Comments
 (0)