Skip to content

Commit 3af7f89

Browse files
authored
Initial implementation of Command Plugins (#3855)
Initial implementation of support for SwiftPM Command Plugins. https://forums.swift.org/t/pitch-package-manager-command-plugins/53172 This feature is experimental until approved, so declaring command plugins requires a tools version of `999.0` in the package manifest, and invoking them requires `SWIFTPM_ENABLE_COMMAND_PLUGINS` to be set to `1` in the environment of `swift` `package`. This implementation temporarily uses a `swift package plugin <verb>` form for invoking command plugins. Per the pitch and proposal, plugin-defined commands are intended to be invocable using the shortcut `swift package <verb>` when the feature is complete (there is discussion of even supporting `swift <verb>` but that is not part of the current proposal). This feature is being developed incrementally across this commit and follow-on commits. This commit includes all the proposed changes to `PackageDescription` and `PackagePlugin`. Remaining items of the full feature will then be landed in future PRs. The `SWIFTPM_ENABLE_COMMAND_PLUGINS` guard flag will only be removed once the evolution proposal has been approved and the feature has been fully implemented including any amendments. Specific modifications in this commit: - add the new enum cases for the command plugin capability to PackageDescription - add serialization of those cases in PackageDescriptionSerializer - deserialize them in the ManifestJSONLoader and add to the package model - add manifest source generation for the command capability - add ability to describe packages that have command plugins - add a unit test to check loading a manifest with a command capability - add protocol declarations for command plugin entry point in the plugin API - ability to invoke the command plugin in libSwiftPM and unit tests - add a unit test to check round trip calling into the plugin - add ability to invoke the command plugin from CLI - made the host-to-plugin communication be bidirectional and interactive - ability for plugin to call back into SwiftPM for services - ability to create symbol graphs for a target when a plugin requests it - ability to run a build when the plugin requests it Remaining to implement: - request approval to run command plugins that need special permissions - implement the option to list the available command plugins - support qualifiers for ambiguous plugin commands - ability to run a test when the plugin requests it - build any executables needed by command plugins before invoking the plugin - provide access to toolchain executables in the map sent to plugins - add async to all the plugin APIs (in separate PR, depends on back deployment) Some of these are not specific to command plugins but rather apply to all plugins in general.
1 parent 7ef3ce1 commit 3af7f89

28 files changed

+2257
-507
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/Describe.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,55 @@ struct DescribedPackage: Encodable {
144144
/// Represents a plugin capability for the sole purpose of generating a description.
145145
struct DescribedPluginCapability: Encodable {
146146
let type: String
147+
let intent: CommandIntent?
148+
let permissions: [Permission]?
147149

148150
init(from capability: PluginCapability, in package: Package) {
149151
switch capability {
150152
case .buildTool:
151153
self.type = "buildTool"
154+
self.intent = nil
155+
self.permissions = nil
156+
case .command(let intent, let permissions):
157+
self.type = "command"
158+
self.intent = .init(from: intent)
159+
self.permissions = permissions.map{ .init(from: $0) }
160+
}
161+
}
162+
163+
struct CommandIntent: Encodable {
164+
let type: String
165+
let verb: String?
166+
let description: String?
167+
168+
init(from intent: PackageModel.PluginCommandIntent) {
169+
switch intent {
170+
case .documentationGeneration:
171+
self.type = "documentationGeneration"
172+
self.verb = nil
173+
self.description = nil
174+
case .sourceCodeFormatting:
175+
self.type = "sourceCodeFormatting"
176+
self.verb = nil
177+
self.description = nil
178+
case .custom(let verb, let description):
179+
self.type = "custom"
180+
self.verb = verb
181+
self.description = description
182+
}
183+
}
184+
}
185+
186+
struct Permission: Encodable {
187+
let type: String
188+
let reason: String
189+
190+
init(from permission: PackageModel.PluginPermission) {
191+
switch permission {
192+
case .writeToPackageDirectory(let reason):
193+
self.type = "writeToPackageDirectory"
194+
self.reason = reason
195+
}
152196
}
153197
}
154198
}

0 commit comments

Comments
 (0)