Skip to content

Commit 9564daf

Browse files
authored
Support qualifiers for ambiguous plugin commands (#6920)
* Support qualifiers for ambiguous plugin commands This adds a `--package` option to the plugin command which will limit the set of available plugins to those inside a package with the given identity. The identity is not validated in any way, it will just be applied as a filter on all relevant packages. The option does apply to `--list` as well which seems a bit odd but a consequence of making listing an option rather than a command. rdar://99887952 * Test
1 parent c6c5cd8 commit 9564daf

File tree

9 files changed

+112
-8
lines changed

9 files changed

+112
-8
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// swift-tools-version: 5.9
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "A",
7+
products: [
8+
.plugin(
9+
name: "A",
10+
targets: ["A"]),
11+
],
12+
targets: [
13+
.plugin(
14+
name: "A",
15+
capability: .command(intent: .custom(
16+
verb: "A",
17+
description: "prints hello"
18+
))
19+
),
20+
]
21+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import PackagePlugin
2+
3+
@main
4+
struct A: CommandPlugin {
5+
func performCommand(context: PluginContext, arguments: [String]) async throws {
6+
print("Hello A!")
7+
}
8+
}
9+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// swift-tools-version: 5.9
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "B",
7+
products: [
8+
.plugin(
9+
name: "B",
10+
targets: ["B"]),
11+
],
12+
targets: [
13+
.plugin(
14+
name: "B",
15+
capability: .command(intent: .custom(
16+
verb: "A",
17+
description: "prints hello"
18+
))
19+
),
20+
]
21+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import PackagePlugin
2+
3+
@main
4+
struct B: CommandPlugin {
5+
func performCommand(context: PluginContext, arguments: [String]) async throws {
6+
print("Hello B!")
7+
}
8+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// swift-tools-version: 5.9
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "AmbiguousCommands",
7+
dependencies: [
8+
.package(path: "Dependencies/A"),
9+
.package(path: "Dependencies/B"),
10+
],
11+
targets: [
12+
.executableTarget(
13+
name: "AmbiguousCommands"),
14+
]
15+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("Hello, world!")

Sources/Commands/PackageTools/PluginCommand.swift

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ struct PluginCommand: SwiftCommand {
5757

5858
@Option(name: .customLong("allow-network-connections"))
5959
var allowNetworkConnections: NetworkPermission = .none
60+
61+
@Option(
62+
name: .customLong("package"),
63+
help: "Limit available plugins to a single package with the given identity"
64+
)
65+
var packageIdentity: String? = nil
6066
}
6167

6268
@OptionGroup()
@@ -80,7 +86,7 @@ struct PluginCommand: SwiftCommand {
8086
// List the available plugins, if asked to.
8187
if self.listCommands {
8288
let packageGraph = try swiftTool.loadPackageGraph()
83-
let allPlugins = PluginCommand.availableCommandPlugins(in: packageGraph)
89+
let allPlugins = PluginCommand.availableCommandPlugins(in: packageGraph, limitedTo: self.pluginOptions.packageIdentity)
8490
for plugin in allPlugins.sorted(by: { $0.name < $1.name }) {
8591
guard case .command(let intent, _) = plugin.capability else { continue }
8692
var line = "\(intent.invocationVerb)’ (plugin ‘\(plugin.name)"
@@ -113,7 +119,7 @@ struct PluginCommand: SwiftCommand {
113119
let packageGraph = try swiftTool.loadPackageGraph()
114120

115121
swiftTool.observabilityScope.emit(info: "Finding plugin for command ‘\(command)")
116-
let matchingPlugins = PluginCommand.findPlugins(matching: command, in: packageGraph)
122+
let matchingPlugins = PluginCommand.findPlugins(matching: command, in: packageGraph, limitedTo: options.packageIdentity)
117123

118124
// Complain if we didn't find exactly one.
119125
if matchingPlugins.isEmpty {
@@ -298,12 +304,12 @@ struct PluginCommand: SwiftCommand {
298304
// TODO: We should also emit a final line of output regarding the result.
299305
}
300306

301-
static func availableCommandPlugins(in graph: PackageGraph) -> [PluginTarget] {
307+
static func availableCommandPlugins(in graph: PackageGraph, limitedTo packageIdentity: String?) -> [PluginTarget] {
302308
// All targets from plugin products of direct dependencies are "available".
303-
let directDependencyPackages = graph.rootPackages.flatMap { $0.dependencies }
309+
let directDependencyPackages = graph.rootPackages.flatMap { $0.dependencies }.filter { $0.matching(identity: packageIdentity) }
304310
let directDependencyPluginTargets = directDependencyPackages.flatMap { $0.products.filter { $0.type == .plugin } }.flatMap { $0.targets }
305311
// As well as any plugin targets in root packages.
306-
let rootPackageTargets = graph.rootPackages.flatMap { $0.targets }
312+
let rootPackageTargets = graph.rootPackages.filter { $0.matching(identity: packageIdentity) }.flatMap { $0.targets }
307313
return (directDependencyPluginTargets + rootPackageTargets).compactMap { $0.underlyingTarget as? PluginTarget }.filter {
308314
switch $0.capability {
309315
case .buildTool: return false
@@ -312,9 +318,9 @@ struct PluginCommand: SwiftCommand {
312318
}
313319
}
314320

315-
static func findPlugins(matching verb: String, in graph: PackageGraph) -> [PluginTarget] {
321+
static func findPlugins(matching verb: String, in graph: PackageGraph, limitedTo packageIdentity: String?) -> [PluginTarget] {
316322
// Find and return the command plugins that match the command.
317-
Self.availableCommandPlugins(in: graph).filter {
323+
Self.availableCommandPlugins(in: graph, limitedTo: packageIdentity).filter {
318324
// Filter out any non-command plugins and any whose verb is different.
319325
guard case .command(let intent, _) = $0.capability else { return false }
320326
return verb == intent.invocationVerb
@@ -386,3 +392,13 @@ extension SandboxNetworkPermission {
386392
}
387393
}
388394
}
395+
396+
extension ResolvedPackage {
397+
fileprivate func matching(identity: String?) -> Bool {
398+
if let identity {
399+
return self.identity == .plain(identity)
400+
} else {
401+
return true
402+
}
403+
}
404+
}

Sources/Commands/PackageTools/SwiftPackageTool.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ extension PluginCommand.PluginOptions {
126126
func merged(with other: Self) -> Self {
127127
// validate against developer mistake
128128
assert(
129-
Mirror(reflecting: self).children.count == 3,
129+
Mirror(reflecting: self).children.count == 4,
130130
"Property added to PluginOptions without updating merged(with:)!"
131131
)
132132
// actual merge
@@ -137,6 +137,9 @@ extension PluginCommand.PluginOptions {
137137
if other.allowNetworkConnections != .none {
138138
merged.allowNetworkConnections = other.allowNetworkConnections
139139
}
140+
if other.packageIdentity != nil {
141+
merged.packageIdentity = other.packageIdentity
142+
}
140143
return merged
141144
}
142145
}

Tests/CommandsTests/PackageToolTests.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,6 +1842,16 @@ final class PackageToolTests: CommandsTestCase {
18421842
}
18431843
}
18441844

1845+
func testAmbiguousCommandPlugin() throws {
1846+
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
1847+
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")
1848+
1849+
try fixture(name: "Miscellaneous/Plugins/AmbiguousCommands") { fixturePath in
1850+
let (stdout, _) = try SwiftPM.Package.execute(["plugin", "--package", "A", "A"], packagePath: fixturePath)
1851+
XCTAssertMatch(stdout, .contains("Hello A!"))
1852+
}
1853+
}
1854+
18451855
func testCommandPluginNetworkingPermissions(permissionsManifestFragment: String, permissionError: String, reason: String, remedy: [String]) throws {
18461856
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
18471857
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")

0 commit comments

Comments
 (0)