Skip to content

Commit 939c635

Browse files
committed
Add AddTargetPlugin command
1 parent c8d439a commit 939c635

File tree

6 files changed

+269
-0
lines changed

6 files changed

+269
-0
lines changed

Sources/Commands/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ add_library(Commands
1111
PackageCommands/AddProduct.swift
1212
PackageCommands/AddTarget.swift
1313
PackageCommands/AddTargetDependency.swift
14+
PackageCommands/AddTargetPlugin.swift
1415
PackageCommands/APIDiff.swift
1516
PackageCommands/ArchiveSource.swift
1617
PackageCommands/CompletionCommand.swift
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2014-2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import ArgumentParser
14+
import Basics
15+
import CoreCommands
16+
import PackageModel
17+
import PackageModelSyntax
18+
import SwiftParser
19+
import SwiftSyntax
20+
import TSCBasic
21+
import TSCUtility
22+
import Workspace
23+
24+
extension SwiftPackageCommand {
25+
struct AddTargetPlugin: SwiftCommand {
26+
package static let configuration = CommandConfiguration(
27+
abstract: "Add a new target plugin to the manifest"
28+
)
29+
30+
@OptionGroup(visibility: .hidden)
31+
var globalOptions: GlobalOptions
32+
33+
@Argument(help: "The name of the new plugin")
34+
var pluginName: String
35+
36+
@Argument(help: "The name of the target to update")
37+
var targetName: String
38+
39+
@Option(help: "The package in which the plugin resides")
40+
var package: String?
41+
42+
func run(_ swiftCommandState: SwiftCommandState) throws {
43+
let workspace = try swiftCommandState.getActiveWorkspace()
44+
45+
guard let packagePath = try swiftCommandState.getWorkspaceRoot().packages.first else {
46+
throw StringError("unknown package")
47+
}
48+
49+
// Load the manifest file
50+
let fileSystem = workspace.fileSystem
51+
let manifestPath = packagePath.appending("Package.swift")
52+
let manifestContents: ByteString
53+
do {
54+
manifestContents = try fileSystem.readFileContents(manifestPath)
55+
} catch {
56+
throw StringError("cannot find package manifest in \(manifestPath)")
57+
}
58+
59+
// Parse the manifest.
60+
let manifestSyntax = manifestContents.withData { data in
61+
data.withUnsafeBytes { buffer in
62+
buffer.withMemoryRebound(to: UInt8.self) { buffer in
63+
Parser.parse(source: buffer)
64+
}
65+
}
66+
}
67+
68+
let plugin: TargetDescription.PluginUsage = .plugin(name: pluginName, package: package)
69+
70+
let editResult = try PackageModelSyntax.AddPluginDependency.addTargetPlugin(
71+
plugin,
72+
targetName: targetName,
73+
to: manifestSyntax
74+
)
75+
76+
try editResult.applyEdits(
77+
to: fileSystem,
78+
manifest: manifestSyntax,
79+
manifestPath: manifestPath,
80+
verbose: !globalOptions.logging.quiet
81+
)
82+
}
83+
}
84+
}
85+

Sources/Commands/PackageCommands/SwiftPackageCommand.swift

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public struct SwiftPackageCommand: AsyncParsableCommand {
3737
AddProduct.self,
3838
AddTarget.self,
3939
AddTargetDependency.self,
40+
AddTargetPlugin.self,
4041
Clean.self,
4142
PurgeCache.self,
4243
Reset.self,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Basics
14+
import PackageLoading
15+
import PackageModel
16+
import SwiftParser
17+
import SwiftSyntax
18+
import SwiftSyntaxBuilder
19+
20+
/// Add a target plugin to a manifest's source code.
21+
public struct AddPluginDependency {
22+
/// The set of argument labels that can occur after the "plugins"
23+
/// argument in the various target initializers.
24+
///
25+
/// TODO: Could we generate this from the the PackageDescription module, so
26+
/// we don't have keep it up-to-date manually?
27+
private static let argumentLabelsAfterDependencies: Set<String> = []
28+
29+
/// Produce the set of source edits needed to add the given target
30+
/// plugin to the given manifest file.
31+
public static func addTargetPlugin(
32+
_ plugin: TargetDescription.PluginUsage,
33+
targetName: String,
34+
to manifest: SourceFileSyntax
35+
) throws -> PackageEditResult {
36+
// Make sure we have a suitable tools version in the manifest.
37+
try manifest.checkEditManifestToolsVersion()
38+
39+
guard let packageCall = manifest.findCall(calleeName: "Package") else {
40+
throw ManifestEditError.cannotFindPackage
41+
}
42+
43+
// Dig out the array of targets.
44+
guard let targetsArgument = packageCall.findArgument(labeled: "targets"),
45+
let targetArray = targetsArgument.expression.findArrayArgument() else {
46+
throw ManifestEditError.cannotFindTargets
47+
}
48+
49+
// Look for a call whose name is a string literal matching the
50+
// requested target name.
51+
func matchesTargetCall(call: FunctionCallExprSyntax) -> Bool {
52+
guard let nameArgument = call.findArgument(labeled: "name") else {
53+
return false
54+
}
55+
56+
guard let stringLiteral = nameArgument.expression.as(StringLiteralExprSyntax.self),
57+
let literalValue = stringLiteral.representedLiteralValue else {
58+
return false
59+
}
60+
61+
return literalValue == targetName
62+
}
63+
64+
guard let targetCall = FunctionCallExprSyntax.findFirst(in: targetArray, matching: matchesTargetCall) else {
65+
throw ManifestEditError.cannotFindTarget(targetName: targetName)
66+
}
67+
68+
let newTargetCall = try addTargetPluginLocal(
69+
plugin, to: targetCall
70+
)
71+
72+
return PackageEditResult(
73+
manifestEdits: [
74+
.replace(targetCall, with: newTargetCall.description)
75+
]
76+
)
77+
}
78+
79+
/// Implementation of adding a target dependency to an existing call.
80+
static func addTargetPluginLocal(
81+
_ plugin: TargetDescription.PluginUsage,
82+
to targetCall: FunctionCallExprSyntax
83+
) throws -> FunctionCallExprSyntax {
84+
try targetCall.appendingToArrayArgument(
85+
label: "plugins",
86+
trailingLabels: Self.argumentLabelsAfterDependencies,
87+
newElement: plugin.asSyntax()
88+
)
89+
}
90+
}
91+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2014-2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import PackageModel
14+
import SwiftSyntax
15+
16+
extension TargetDescription.PluginUsage: ManifestSyntaxRepresentable {
17+
func asSyntax() -> ExprSyntax {
18+
switch self {
19+
case let .plugin(name: name, package: package):
20+
if let package {
21+
return ".plugin(name: \(literal: name.description), package: \(literal: package.description))"
22+
} else {
23+
return ".plugin(name: \(literal: name.description))"
24+
}
25+
}
26+
}
27+
}

Tests/CommandsTests/PackageCommandTests.swift

+64
Original file line numberDiff line numberDiff line change
@@ -1235,6 +1235,70 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase {
12351235
}
12361236
}
12371237

1238+
func testPackageAddPluginDependencyExternalPackage() async throws {
1239+
try await testWithTemporaryDirectory { tmpPath in
1240+
let fs = localFileSystem
1241+
let path = tmpPath.appending("PackageB")
1242+
try fs.createDirectory(path)
1243+
1244+
try fs.writeFileContents(path.appending("Package.swift"), string:
1245+
"""
1246+
// swift-tools-version: 5.9
1247+
import PackageDescription
1248+
let package = Package(
1249+
name: "client",
1250+
targets: [ .target(name: "library") ]
1251+
)
1252+
"""
1253+
)
1254+
try localFileSystem.writeFileContents(path.appending(components: "Sources", "library", "library.swift"), string:
1255+
"""
1256+
public func Foo() { }
1257+
"""
1258+
)
1259+
1260+
_ = try await execute(["add-target-plugin", "--package", "other-package", "other-product", "library"], packagePath: path)
1261+
1262+
let manifest = path.appending("Package.swift")
1263+
XCTAssertFileExists(manifest)
1264+
let contents: String = try fs.readFileContents(manifest)
1265+
1266+
XCTAssertMatch(contents, .contains(#".plugin(name: "other-product", package: "other-package"#))
1267+
}
1268+
}
1269+
1270+
func testPackageAddPluginDependencyInternalPackage() async throws {
1271+
try await testWithTemporaryDirectory { tmpPath in
1272+
let fs = localFileSystem
1273+
let path = tmpPath.appending("PackageB")
1274+
try fs.createDirectory(path)
1275+
1276+
try fs.writeFileContents(path.appending("Package.swift"), string:
1277+
"""
1278+
// swift-tools-version: 5.9
1279+
import PackageDescription
1280+
let package = Package(
1281+
name: "client",
1282+
targets: [ .target(name: "library") ]
1283+
)
1284+
"""
1285+
)
1286+
try localFileSystem.writeFileContents(path.appending(components: "Sources", "library", "library.swift"), string:
1287+
"""
1288+
public func Foo() { }
1289+
"""
1290+
)
1291+
1292+
_ = try await execute(["add-target-plugin", "other-product", "library"], packagePath: path)
1293+
1294+
let manifest = path.appending("Package.swift")
1295+
XCTAssertFileExists(manifest)
1296+
let contents: String = try fs.readFileContents(manifest)
1297+
1298+
XCTAssertMatch(contents, .contains(#".plugin(name: "other-product"#))
1299+
}
1300+
}
1301+
12381302
func testPackageAddProduct() async throws {
12391303
try await testWithTemporaryDirectory { tmpPath in
12401304
let fs = localFileSystem

0 commit comments

Comments
 (0)