Skip to content

Initial implementation of Command Plugins #3855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Nov 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
55e7cbd
Add the declaration of the `command` capability for plugins (this is …
abertelrud Nov 10, 2021
1a51580
Add the plugin-side declarations for command plugins, per the pitch, …
abertelrud Nov 11, 2021
7f2e793
Add back the "intent" enum for command plugins, as described in the r…
abertelrud Nov 12, 2021
26f02b8
Make the communication between the plugin host and the plugin slightl…
abertelrud Nov 15, 2021
b3ef9f6
Add a way to invoke command plugins from `swift package`. Temporaril…
abertelrud Nov 16, 2021
e6f7cad
Mark the PackageDescription manifest API as being available only with…
abertelrud Nov 16, 2021
2856ac6
Adjustments based on PR review feedback
abertelrud Nov 18, 2021
bcd3c3b
Clean up the communication between the plugin host and plugin to use …
abertelrud Nov 18, 2021
22b65b2
Implement PR review feedback
abertelrud Nov 19, 2021
c9d6b6c
Extend the previous logic for sending information to and from the plu…
abertelrud Nov 19, 2021
0d94e59
Make plugin compilation and invocation calls asynchronous
abertelrud Nov 19, 2021
302194d
Changes based on review feedback:
abertelrud Nov 20, 2021
9abe430
Add in PackageManager with the callbacks from the plugin to the host
abertelrud Nov 19, 2021
fa48908
Make the host-side communication with the plugin use FileHandle callb…
abertelrud Nov 23, 2021
95e3f75
Pass through a plugin invocation delegate instead of just a text outp…
abertelrud Nov 24, 2021
e056381
Update the comments that describe how the plugin host and the plugin …
abertelrud Nov 26, 2021
a69313b
Use plugin messaging rather than a custom return struct to send back …
abertelrud Nov 26, 2021
c1a187c
Add the ability for a command plugin to ask for symbol graph informat…
abertelrud Nov 27, 2021
d8f83d5
Make plugins send diagnostics to the host as they are emitted, and no…
abertelrud Nov 27, 2021
92b3e4a
Add in a missing source file to CMakeLists.txt
abertelrud Nov 27, 2021
ccff9db
Align terminology and declarations with those in the Package Manager …
abertelrud Nov 27, 2021
d0b2dc5
Pass the correct package to the plugin based on the targets passed
abertelrud Nov 29, 2021
fc44eec
Move the code coverage path method to a place in the struct where it …
abertelrud Nov 29, 2021
8ceac9a
Add support for running builds on-request from the plugin, and add a …
abertelrud Nov 29, 2021
06549d8
Use the tool's output stream, not print, for emitting plugin output
abertelrud Nov 29, 2021
7723030
Remove a delegate queue reference that was only used for checking pre…
abertelrud Nov 29, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Sources/Build/BuildOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
let packageGraphLoader: () throws -> PackageGraph

/// The closure for invoking plugins in the package graph.
let pluginInvoker: (PackageGraph) throws -> [ResolvedTarget: [PluginInvocationResult]]
let pluginInvoker: (PackageGraph) throws -> [ResolvedTarget: [BuildToolPluginInvocationResult]]

/// The llbuild build delegate reference.
private var buildSystemDelegate: BuildOperationBuildSystemDelegateHandler?
Expand Down Expand Up @@ -70,7 +70,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
buildParameters: BuildParameters,
cacheBuildManifest: Bool,
packageGraphLoader: @escaping () throws -> PackageGraph,
pluginInvoker: @escaping (PackageGraph) throws -> [ResolvedTarget: [PluginInvocationResult]],
pluginInvoker: @escaping (PackageGraph) throws -> [ResolvedTarget: [BuildToolPluginInvocationResult]],
outputStream: OutputByteStream,
logLevel: Basics.Diagnostic.Severity,
fileSystem: TSCBasic.FileSystem,
Expand All @@ -92,7 +92,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
}
}

public func getPluginInvocationResults(for graph: PackageGraph) throws -> [ResolvedTarget: [PluginInvocationResult]] {
public func getPluginInvocationResults(for graph: PackageGraph) throws -> [ResolvedTarget: [BuildToolPluginInvocationResult]] {
return try self.pluginInvoker(graph)
}

Expand Down Expand Up @@ -295,20 +295,20 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS

/// Runs any prebuild commands associated with the given list of plugin invocation results, in order, and returns the
/// results of running those prebuild commands.
private func runPrebuildCommands(for pluginResults: [PluginInvocationResult]) throws -> [PrebuildCommandResult] {
private func runPrebuildCommands(for pluginResults: [BuildToolPluginInvocationResult]) throws -> [PrebuildCommandResult] {
// Run through all the commands from all the plugin usages in the target.
return try pluginResults.map { pluginResult in
// As we go we will collect a list of prebuild output directories whose contents should be input to the build,
// and a list of the files in those directories after running the commands.
var derivedSourceFiles: [AbsolutePath] = []
var prebuildOutputDirs: [AbsolutePath] = []
for command in pluginResult.prebuildCommands {
self.observabilityScope.emit(info: "Running" + command.configuration.displayName)
self.observabilityScope.emit(info: "Running" + (command.configuration.displayName ?? command.configuration.executable.basename))

// Run the command configuration as a subshell. This doesn't return until it is done.
// TODO: We need to also use any working directory, but that support isn't yet available on all platforms at a lower level.
// TODO: Invoke it in a sandbox that allows writing to only the temporary location.
let commandLine = [command.configuration.executable] + command.configuration.arguments
let commandLine = [command.configuration.executable.pathString] + command.configuration.arguments
let processResult = try Process.popen(arguments: commandLine, environment: command.configuration.environment)
let output = try processResult.utf8Output() + processResult.utf8stderrOutput()
if processResult.exitStatus != .terminated(code: 0) {
Expand Down
10 changes: 5 additions & 5 deletions Sources/Build/BuildPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ public final class SwiftTargetBuildDescription {
private(set) var moduleMap: AbsolutePath?

/// The results of applying any plugins to this target.
public let pluginInvocationResults: [PluginInvocationResult]
public let pluginInvocationResults: [BuildToolPluginInvocationResult]

/// The results of running any prebuild commands for this target.
public let prebuildCommandResults: [PrebuildCommandResult]
Expand All @@ -612,7 +612,7 @@ public final class SwiftTargetBuildDescription {
target: ResolvedTarget,
toolsVersion: ToolsVersion,
buildParameters: BuildParameters,
pluginInvocationResults: [PluginInvocationResult] = [],
pluginInvocationResults: [BuildToolPluginInvocationResult] = [],
prebuildCommandResults: [PrebuildCommandResult] = [],
isTestTarget: Bool? = nil,
testDiscoveryTarget: Bool = false,
Expand Down Expand Up @@ -1424,7 +1424,7 @@ public class BuildPlan {
}

/// The results of invoking any plugins used by targets in this build.
public let pluginInvocationResults: [ResolvedTarget: [PluginInvocationResult]]
public let pluginInvocationResults: [ResolvedTarget: [BuildToolPluginInvocationResult]]

/// The results of running any prebuild commands for the targets in this build. This includes any derived
/// source files as well as directories to which any changes should cause us to reevaluate the build plan.
Expand Down Expand Up @@ -1523,7 +1523,7 @@ public class BuildPlan {
public convenience init(
buildParameters: BuildParameters,
graph: PackageGraph,
pluginInvocationResults: [ResolvedTarget: [PluginInvocationResult]] = [:],
pluginInvocationResults: [ResolvedTarget: [BuildToolPluginInvocationResult]] = [:],
prebuildCommandResults: [ResolvedTarget: [PrebuildCommandResult]] = [:],
diagnostics: DiagnosticsEngine,
fileSystem: FileSystem
Expand All @@ -1541,7 +1541,7 @@ public class BuildPlan {
public init(
buildParameters: BuildParameters,
graph: PackageGraph,
pluginInvocationResults: [ResolvedTarget: [PluginInvocationResult]] = [:],
pluginInvocationResults: [ResolvedTarget: [BuildToolPluginInvocationResult]] = [:],
prebuildCommandResults: [ResolvedTarget: [PrebuildCommandResult]] = [:],
fileSystem: FileSystem,
observabilityScope: ObservabilityScope
Expand Down
7 changes: 4 additions & 3 deletions Sources/Build/LLBuildManifestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -607,11 +607,12 @@ extension LLBuildManifestBuilder {
// Add any regular build commands created by plugins for the target (prebuild commands are handled separately).
for command in target.pluginInvocationResults.reduce([], { $0 + $1.buildCommands }) {
// 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.
let execPath = AbsolutePath(command.configuration.executable, relativeTo: buildParameters.buildPath)
let execPath = command.configuration.executable
let uniquedName = ([execPath.pathString] + command.configuration.arguments).joined(separator: "|")
let displayName = command.configuration.displayName ?? execPath.basename
manifest.addShellCmd(
name: command.configuration.displayName + "-" + ByteString(encodingAsUTF8: uniquedName).sha256Checksum,
description: command.configuration.displayName,
name: displayName + "-" + ByteString(encodingAsUTF8: uniquedName).sha256Checksum,
description: displayName,
inputs: [.file(execPath)] + command.inputFiles.map{ .file($0) },
outputs: command.outputFiles.map{ .file($0) },
arguments: [execPath.pathString] + command.configuration.arguments,
Expand Down
44 changes: 44 additions & 0 deletions Sources/Commands/Describe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,55 @@ struct DescribedPackage: Encodable {
/// Represents a plugin capability for the sole purpose of generating a description.
struct DescribedPluginCapability: Encodable {
let type: String
let intent: CommandIntent?
let permissions: [Permission]?

init(from capability: PluginCapability, in package: Package) {
switch capability {
case .buildTool:
self.type = "buildTool"
self.intent = nil
self.permissions = nil
case .command(let intent, let permissions):
self.type = "command"
self.intent = .init(from: intent)
self.permissions = permissions.map{ .init(from: $0) }
}
}

struct CommandIntent: Encodable {
let type: String
let verb: String?
let description: String?

init(from intent: PackageModel.PluginCommandIntent) {
switch intent {
case .documentationGeneration:
self.type = "documentationGeneration"
self.verb = nil
self.description = nil
case .sourceCodeFormatting:
self.type = "sourceCodeFormatting"
self.verb = nil
self.description = nil
case .custom(let verb, let description):
self.type = "custom"
self.verb = verb
self.description = description
}
}
}

struct Permission: Encodable {
let type: String
let reason: String

init(from permission: PackageModel.PluginPermission) {
switch permission {
case .writeToPackageDirectory(let reason):
self.type = "writeToPackageDirectory"
self.reason = reason
}
}
}
}
Expand Down
Loading