Skip to content

Commit 52f50e0

Browse files
committed
Add the declaration of the command capability for plugins (this is the first part of implementing command plugins, just laying the foundation)
- add the new enum case to PackageDescription - add serialization of it in PackageDescriptionSerializer - deserialize it in the ManifestJSONLoader - add manifest source generation for it - add ability to describe packages with command plugins - adjust other parts of the code accordingly - add a unit test to check loading a manifest with a command capability
1 parent f02d201 commit 52f50e0

File tree

9 files changed

+207
-18
lines changed

9 files changed

+207
-18
lines changed

Sources/Commands/Describe.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,34 @@ struct DescribedPackage: Encodable {
142142
/// Represents a plugin capability for the sole purpose of generating a description.
143143
struct DescribedPluginCapability: Encodable {
144144
let type: String
145+
let verb: String?
146+
let description: String?
147+
let permissions: [PluginPermission]?
145148

146149
init(from capability: PluginCapability, in package: Package) {
147150
switch capability {
148151
case .buildTool:
149152
self.type = "buildTool"
153+
self.verb = nil
154+
self.description = nil
155+
self.permissions = nil
156+
case .command(let verb, let description, let permissions):
157+
self.type = "command"
158+
self.verb = verb
159+
self.description = description
160+
self.permissions = permissions.map{
161+
switch $0 {
162+
case .packageWritability(let reason):
163+
return PluginPermission(type: "packageWritability", reason: reason)
164+
}
165+
}
150166
}
151167
}
168+
169+
struct PluginPermission: Encodable {
170+
let type: String
171+
let reason: String
172+
}
152173
}
153174

154175
/// Represents a target for the sole purpose of generating a description.

Sources/PackageDescription/PackageDescriptionSerialization.swift

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,18 +224,42 @@ extension SystemPackageProvider: Encodable {
224224

225225
extension Target.PluginCapability: Encodable {
226226
private enum CodingKeys: CodingKey {
227-
case type
227+
case type, verb, description, permissions
228228
}
229229

230230
private enum Capability: String, Encodable {
231-
case buildTool
231+
case buildTool, command
232232
}
233233

234234
public func encode(to encoder: Encoder) throws {
235235
var container = encoder.container(keyedBy: CodingKeys.self)
236236
switch self {
237237
case ._buildTool:
238238
try container.encode(Capability.buildTool, forKey: .type)
239+
case ._command(let verb, let description, let permissions):
240+
try container.encode(Capability.command, forKey: .type)
241+
try container.encode(verb, forKey: .verb)
242+
try container.encode(description, forKey: .description)
243+
try container.encode(permissions, forKey: .permissions)
244+
}
245+
}
246+
}
247+
248+
extension PluginPermission: Encodable {
249+
private enum CodingKeys: CodingKey {
250+
case type, reason
251+
}
252+
253+
private enum PermissionType: String, Encodable {
254+
case packageWritability
255+
}
256+
257+
public func encode(to encoder: Encoder) throws {
258+
var container = encoder.container(keyedBy: CodingKeys.self)
259+
switch self {
260+
case .packageWritability(let reason):
261+
try container.encode(PermissionType.packageWritability, forKey: .type)
262+
try container.encode(reason, forKey: .reason)
239263
}
240264
}
241265
}

Sources/PackageDescription/Target.swift

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,11 @@ public final class Target {
127127
private var _pluginCapability: PluginCapability?
128128

129129
/// The different types of capability that a plugin can provide. In this
130-
/// version of SwiftPM, only build tool plugins are supported; this enum
131-
/// will be extended as new plugin capabilities are added.
130+
/// version of SwiftPM, only build tool and command plugins are supported;
131+
/// this enum will be extended as new plugin capabilities are added.
132132
public enum PluginCapability {
133133
case _buildTool
134+
case _command(verb: String, description: String, permissions: [PluginPermission])
134135
}
135136

136137
/// The target's C build settings.
@@ -1010,6 +1011,38 @@ extension Target.PluginCapability {
10101011
public static func buildTool() -> Target.PluginCapability {
10111012
return ._buildTool
10121013
}
1014+
1015+
/// Specifies that the plugin provides a user command capability. It will
1016+
/// be available to invoke manually on one or more targets in a package.
1017+
/// The package can specify the verb that is used to invoke the command.
1018+
@available(_PackageDescription, introduced: 5.6)
1019+
/// Plugins that specify a `command` capability define commands that can be run
1020+
/// using the SwiftPM CLI (`swift package <verb>`), or in an IDE that supports
1021+
/// Swift Packages.
1022+
public static func command(
1023+
/// The `swift package` CLI verb through which the plugin can be invoked.
1024+
verb: String,
1025+
1026+
/// A description of the functionality of the custom command, suitable for
1027+
/// showing in help text output.
1028+
description: String,
1029+
1030+
/// Any permissions needed by the command plugin. This affects what the
1031+
/// sandbox in which the plugin is run allows. Some permissions may require
1032+
/// approval by the user.
1033+
permissions: [PluginPermission] = []
1034+
) -> Target.PluginCapability {
1035+
return ._command(verb: verb, description: description, permissions: permissions)
1036+
}
1037+
}
1038+
1039+
public enum PluginPermission {
1040+
/// The custom command plugin requests permission to modify the files inside the
1041+
/// package directory. The `reason` string is shown to the user at the time of
1042+
/// request for approval, explaining why the plugin is requesting this access.
1043+
case packageWritability(reason: String)
1044+
1045+
/// Any future enum cases should use @available()
10131046
}
10141047

10151048
extension Target.PluginUsage {

Sources/PackageLoading/ManifestJSONParser.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,24 @@ extension TargetDescription.PluginCapability {
596596
switch type {
597597
case "buildTool":
598598
self = .buildTool
599+
case "command":
600+
let verb = try json.get(String.self, forKey: "verb")
601+
let description = try json.get(String.self, forKey: "description")
602+
let permissions = try json.getArray("permissions").map(TargetDescription.PluginPermission.init(v4:))
603+
self = .command(verb: verb, description: description, permissions: permissions)
604+
default:
605+
throw InternalError("invalid type \(type)")
606+
}
607+
}
608+
}
609+
610+
extension TargetDescription.PluginPermission {
611+
fileprivate init(v4 json: JSON) throws {
612+
let type = try json.get(String.self, forKey: "type")
613+
switch type {
614+
case "packageWritability":
615+
let reason = try json.get(String.self, forKey: "reason")
616+
self = .packageWritability(reason: reason)
599617
default:
600618
throw InternalError("invalid type \(type)")
601619
}
@@ -610,7 +628,6 @@ extension TargetDescription.PluginUsage {
610628
let name = try json.get(String.self, forKey: "name")
611629
let package = try? json.get(String.self, forKey: "package")
612630
self = .plugin(name: name, package: package)
613-
614631
default:
615632
throw InternalError("invalid type \(type)")
616633
}

Sources/PackageLoading/PackageBuilder.swift

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -828,24 +828,18 @@ public final class PackageBuilder {
828828

829829
// Deal with package plugin targets.
830830
if potentialModule.type == .plugin {
831+
// Check that the target has a declared capability; we should not have come this far if not.
831832
guard let declaredCapability = manifestTarget.pluginCapability else {
832833
throw ModuleError.pluginCapabilityNotDeclared(target: manifestTarget.name)
833834
}
834835

835-
// Translate the capability from the target description form coming in from the manifest
836-
// to the package model form.
837-
let capability: PluginCapability
838-
switch declaredCapability {
839-
case .buildTool:
840-
capability = .buildTool
841-
}
842-
843-
// Crate and return an PluginTarget configured with the information from the manifest.
836+
// Create and return an PluginTarget configured with the information from the manifest.
844837
return PluginTarget(
845838
name: potentialModule.name,
846839
platforms: self.platforms(), // FIXME: this should be host platform
847840
sources: sources,
848-
pluginCapability: capability,
841+
apiVersion: self.manifest.toolsVersion,
842+
pluginCapability: PluginCapability(from: declaredCapability),
849843
dependencies: dependencies)
850844
}
851845

Sources/PackageModel/Manifest/TargetDescription.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ public struct TargetDescription: Equatable, Codable {
110110
/// Represents the declared capability of a package plugin.
111111
public enum PluginCapability: Equatable {
112112
case buildTool
113+
case command(verb: String, description: String, permissions: [PluginPermission])
114+
}
115+
116+
public enum PluginPermission: Equatable, Codable {
117+
case packageWritability(reason: String)
113118
}
114119

115120
/// The target-specific build settings declared in this target.
@@ -259,14 +264,19 @@ extension TargetDescription.Dependency: ExpressibleByStringLiteral {
259264

260265
extension TargetDescription.PluginCapability: Codable {
261266
private enum CodingKeys: CodingKey {
262-
case buildTool
267+
case buildTool, command
263268
}
264269

265270
public func encode(to encoder: Encoder) throws {
266271
var container = encoder.container(keyedBy: CodingKeys.self)
267272
switch self {
268273
case .buildTool:
269274
try container.encodeNil(forKey: .buildTool)
275+
case .command(let a1, let a2, let a3):
276+
var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .command)
277+
try unkeyedContainer.encode(a1)
278+
try unkeyedContainer.encode(a2)
279+
try unkeyedContainer.encode(a3)
270280
}
271281
}
272282

@@ -278,6 +288,12 @@ extension TargetDescription.PluginCapability: Codable {
278288
switch key {
279289
case .buildTool:
280290
self = .buildTool
291+
case .command:
292+
var unkeyedValues = try values.nestedUnkeyedContainer(forKey: key)
293+
let a1 = try unkeyedValues.decode(String.self)
294+
let a2 = try unkeyedValues.decode(String.self)
295+
let a3 = try unkeyedValues.decode([TargetDescription.PluginPermission].self)
296+
self = .command(verb: a1, description: a2, permissions: a3)
281297
}
282298
}
283299
}

Sources/PackageModel/ManifestSourceGeneration.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,23 @@ fileprivate extension SourceCodeFragment {
415415
switch capability {
416416
case .buildTool:
417417
self.init(enum: "buildTool", subnodes: [])
418+
case .command(let verb, let description, let permissions):
419+
var params: [SourceCodeFragment] = []
420+
params.append(SourceCodeFragment(key: "verb", string: verb))
421+
params.append(SourceCodeFragment(key: "description", string: description))
422+
if !permissions.isEmpty {
423+
params.append(SourceCodeFragment(key: "permissions", subnodes: permissions.map{ .init(from: $0) }))
424+
}
425+
self.init(enum: "command", subnodes: params)
426+
}
427+
}
428+
429+
/// Instantiates a SourceCodeFragment to represent a single plugin permission.
430+
init(from permission: TargetDescription.PluginPermission) {
431+
switch permission {
432+
case .packageWritability(let reason):
433+
let param = SourceCodeFragment(key: "reason", string: reason)
434+
self.init(enum: "packageWritability", subnodes: [param])
418435
}
419436
}
420437

Sources/PackageModel/Target.swift

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -614,16 +614,22 @@ public final class BinaryTarget: Target {
614614

615615
public final class PluginTarget: Target {
616616

617+
/// Declared capability of the plugin.
617618
public let capability: PluginCapability
619+
620+
/// API version to use for PackagePlugin API availability.
621+
public let apiVersion: ToolsVersion
618622

619623
public init(
620624
name: String,
621625
platforms: [SupportedPlatform] = [],
622626
sources: Sources,
627+
apiVersion: ToolsVersion,
623628
pluginCapability: PluginCapability,
624629
dependencies: [Target.Dependency] = []
625630
) {
626631
self.capability = pluginCapability
632+
self.apiVersion = apiVersion
627633
super.init(
628634
name: name,
629635
defaultLocalization: nil,
@@ -638,33 +644,42 @@ public final class PluginTarget: Target {
638644

639645
private enum CodingKeys: String, CodingKey {
640646
case capability
647+
case apiVersion
641648
}
642649

643650
public override func encode(to encoder: Encoder) throws {
644651
var container = encoder.container(keyedBy: CodingKeys.self)
645652
try container.encode(self.capability, forKey: .capability)
653+
try container.encode(self.apiVersion, forKey: .apiVersion)
646654
try super.encode(to: encoder)
647655
}
648656

649657
required public init(from decoder: Decoder) throws {
650658
let container = try decoder.container(keyedBy: CodingKeys.self)
651659
self.capability = try container.decode(PluginCapability.self, forKey: .capability)
660+
self.apiVersion = try container.decode(ToolsVersion.self, forKey: .apiVersion)
652661
try super.init(from: decoder)
653662
}
654663
}
655664

656-
public enum PluginCapability: Equatable, Codable {
665+
public enum PluginCapability: Hashable, Codable {
657666
case buildTool
667+
case command(verb: String, description: String, permissions: [PluginPermission])
658668

659669
private enum CodingKeys: String, CodingKey {
660-
case buildTool
670+
case buildTool, command
661671
}
662672

663673
public func encode(to encoder: Encoder) throws {
664674
var container = encoder.container(keyedBy: CodingKeys.self)
665675
switch self {
666676
case .buildTool:
667677
try container.encodeNil(forKey: .buildTool)
678+
case .command(let a1, let a2, let a3):
679+
var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .command)
680+
try unkeyedContainer.encode(a1)
681+
try unkeyedContainer.encode(a2)
682+
try unkeyedContainer.encode(a3)
668683
}
669684
}
670685

@@ -676,6 +691,32 @@ public enum PluginCapability: Equatable, Codable {
676691
switch key {
677692
case .buildTool:
678693
self = .buildTool
694+
case .command:
695+
var unkeyedValues = try values.nestedUnkeyedContainer(forKey: key)
696+
let a1 = try unkeyedValues.decode(String.self)
697+
let a2 = try unkeyedValues.decode(String.self)
698+
let a3 = try unkeyedValues.decode([PluginPermission].self)
699+
self = .command(verb: a1, description: a2, permissions: a3)
700+
}
701+
}
702+
703+
public init(from desc: TargetDescription.PluginCapability) {
704+
switch desc {
705+
case .buildTool:
706+
self = .buildTool
707+
case .command(let verb, let description, let permissions):
708+
self = .command(verb: verb, description: description, permissions: permissions.map{ .init(from: $0) })
709+
}
710+
}
711+
}
712+
713+
public enum PluginPermission: Hashable, Codable {
714+
case packageWritability(reason: String)
715+
716+
public init(from desc: TargetDescription.PluginPermission) {
717+
switch desc {
718+
case .packageWritability(let reason):
719+
self = .packageWritability(reason: reason)
679720
}
680721
}
681722
}

Tests/PackageLoadingTests/PD_5_6_LoadingTests.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,32 @@ class PackageDescription5_6LoadingTests: PackageDescriptionLoadingTests {
9494
XCTAssertEqual(manifest.targets[0].pluginCapability, .buildTool)
9595
}
9696

97+
func testCommandPluginTarget() throws {
98+
let content = """
99+
import PackageDescription
100+
let package = Package(
101+
name: "Foo",
102+
targets: [
103+
.plugin(
104+
name: "Foo",
105+
capability: .command(
106+
verb: "mycmd",
107+
description: "helpful description of mycmd",
108+
permissions: [ .packageWritability(reason: "YOLO") ]
109+
)
110+
)
111+
]
112+
)
113+
"""
114+
115+
let observability = ObservabilitySystem.makeForTesting()
116+
let manifest = try loadManifest(content, observabilityScope: observability.topScope)
117+
XCTAssertNoDiagnostics(observability.diagnostics)
118+
119+
XCTAssertEqual(manifest.targets[0].type, .plugin)
120+
XCTAssertEqual(manifest.targets[0].pluginCapability, .command(verb: "mycmd", description: "helpful description of mycmd", permissions: [.packageWritability(reason: "YOLO")]))
121+
}
122+
97123
func testPluginTargetCustomization() throws {
98124
let content = """
99125
import PackageDescription

0 commit comments

Comments
 (0)