Skip to content

Add swift package add-target-plugin command #8432

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions Sources/Commands/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ add_library(Commands
PackageCommands/AddTarget.swift
PackageCommands/AddTargetDependency.swift
PackageCommands/AddSetting.swift
PackageCommands/AddTargetPlugin.swift
PackageCommands/APIDiff.swift
PackageCommands/ArchiveSource.swift
PackageCommands/AuditBinaryArtifact.swift
Expand Down
87 changes: 87 additions & 0 deletions Sources/Commands/PackageCommands/AddTargetPlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import ArgumentParser
import Basics
import CoreCommands
import PackageModel
import PackageModelSyntax
import SwiftParser
import SwiftSyntax
import TSCBasic
import TSCUtility
import Workspace
import PackageGraph
import Foundation

extension SwiftPackageCommand {
struct AddTargetPlugin: SwiftCommand {
package static let configuration = CommandConfiguration(
abstract: "Add a new target plugin to the manifest"
)

@OptionGroup(visibility: .hidden)
var globalOptions: GlobalOptions

@Argument(help: "The name of the new plugin")
var pluginName: String

@Argument(help: "The name of the target to update")
var targetName: String

@Option(help: "The package in which the plugin resides")
var package: String?

func run(_ swiftCommandState: SwiftCommandState) throws {
let workspace = try swiftCommandState.getActiveWorkspace()

guard let packagePath = try swiftCommandState.getWorkspaceRoot().packages.first else {
throw StringError("unknown package")
}

// Load the manifest file
let fileSystem = workspace.fileSystem
let manifestPath = packagePath.appending("Package.swift")
let manifestContents: ByteString
do {
manifestContents = try fileSystem.readFileContents(manifestPath)
} catch {
throw StringError("cannot find package manifest in \(manifestPath)")
}

// Parse the manifest.
let manifestSyntax = manifestContents.withData { data in
data.withUnsafeBytes { buffer in
buffer.withMemoryRebound(to: UInt8.self) { buffer in
Parser.parse(source: buffer)
}
}
}

let plugin: TargetDescription.PluginUsage = .plugin(name: pluginName, package: package)

let editResult = try PackageModelSyntax.AddTargetPlugin.addTargetPlugin(
plugin,
targetName: targetName,
to: manifestSyntax
)

try editResult.applyEdits(
to: fileSystem,
manifest: manifestSyntax,
manifestPath: manifestPath,
verbose: !globalOptions.logging.quiet
)
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public struct SwiftPackageCommand: AsyncParsableCommand {
AddTargetDependency.self,
AddSetting.self,
AuditBinaryArtifact.self,
AddTargetPlugin.self,
Clean.self,
PurgeCache.self,
Reset.self,
Expand Down
2 changes: 1 addition & 1 deletion Sources/PackageModelSyntax/AddPackageDependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,4 @@ fileprivate extension MappablePackageDependency.Kind {
return id
}
}
}
}
165 changes: 165 additions & 0 deletions Sources/PackageModelSyntax/AddTargetPlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Basics
import PackageLoading
import PackageModel
import SwiftParser
import SwiftSyntax
import SwiftSyntaxBuilder

/// Add a target plugin to a manifest's source code.
public enum AddTargetPlugin {
/// The set of argument labels that can occur after the "plugins"
/// argument in the various target initializers.
///
/// TODO: Could we generate this from the the PackageDescription module, so
/// we don't have keep it up-to-date manually?
private static let argumentLabelsAfterDependencies: Set<String> = []

/// Produce the set of source edits needed to add the given target
/// plugin to the given manifest file.
public static func addTargetPlugin(
_ plugin: TargetDescription.PluginUsage,
targetName: String,
to manifest: SourceFileSyntax
) throws -> PackageEditResult {
// Make sure we have a suitable tools version in the manifest.
try manifest.checkEditManifestToolsVersion()

guard let packageCall = manifest.findCall(calleeName: "Package") else {
throw ManifestEditError.cannotFindPackage
}

// Dig out the array of targets.
guard let targetsArgument = packageCall.findArgument(labeled: "targets"),
let targetArray = targetsArgument.expression.findArrayArgument() else {
throw ManifestEditError.cannotFindTargets
}

// Look for a call whose name is a string literal matching the
// requested target name.
func matchesTargetCall(call: FunctionCallExprSyntax) -> Bool {
guard let nameArgument = call.findArgument(labeled: "name") else {
return false
}

guard let stringLiteral = nameArgument.expression.as(StringLiteralExprSyntax.self),
let literalValue = stringLiteral.representedLiteralValue
else {
return false
}

return literalValue == targetName
}

guard let targetCall = FunctionCallExprSyntax.findFirst(
in: targetArray,
matching: matchesTargetCall
) else {
throw ManifestEditError.cannotFindTarget(targetName: targetName)
}

guard try !self.pluginAlreadyAdded(
plugin,
to: targetName,
in: targetCall
)
else {
return PackageEditResult(manifestEdits: [])
}

let newTargetCall = try addTargetPluginLocal(
plugin,
to: targetCall
)

return PackageEditResult(
manifestEdits: [
.replace(targetCall, with: newTargetCall.description)
]
)
}

private static func pluginAlreadyAdded(
_ plugin: TargetDescription.PluginUsage,
to targetName: String,
in packageCall: FunctionCallExprSyntax
) throws -> Bool {
let pluginSyntax = plugin.asSyntax()
guard let pluginFnSyntax = pluginSyntax.as(FunctionCallExprSyntax.self)
else {
throw ManifestEditError.cannotFindPackage
}

guard let id = pluginFnSyntax.arguments.first(where: {
$0.label?.text == "name"
})
else {
throw InternalError("Missing 'name' argument in plugin syntax")
}

if let existingPlugins = packageCall.findArgument(labeled: "plugins") {
// If we have an existing plugins array, we need to check if
if let expr = existingPlugins.expression.as(ArrayExprSyntax.self) {
// Iterate through existing plugins and look for an argument that matches
// the `name` argument of the new plugin.
let existingArgument = expr.elements.first { elem in
if let funcExpr = elem.expression.as(
FunctionCallExprSyntax.self
) {
return funcExpr.arguments.contains {
$0.with(\.trailingComma, nil).trimmedDescription ==
id.with(\.trailingComma, nil).trimmedDescription
}
}
return true
}

if let existingArgument {
let normalizedExistingArgument = existingArgument.detached.with(\.trailingComma, nil)
// This exact plugin already exists, return false to indicate we should do nothing.
if normalizedExistingArgument.trimmedDescription == pluginSyntax.trimmedDescription {
return true
}
throw ManifestEditError.existingPlugin(
pluginName: plugin.identifier,
taget: targetName
)
}
}
}

return false
}

/// Implementation of adding a target plugin to an existing call.
static func addTargetPluginLocal(
_ plugin: TargetDescription.PluginUsage,
to targetCall: FunctionCallExprSyntax
) throws -> FunctionCallExprSyntax {
try targetCall.appendingToArrayArgument(
label: "plugins",
trailingLabels: self.argumentLabelsAfterDependencies,
newElement: plugin.asSyntax()
)
}
}

extension TargetDescription.PluginUsage {
fileprivate var identifier: String {
switch self {
case .plugin(let name, _):
name
}
}
}
2 changes: 2 additions & 0 deletions Sources/PackageModelSyntax/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ add_library(PackageModelSyntax
AddSwiftSetting.swift
AddTarget.swift
AddTargetDependency.swift
AddTargetPlugin.swift
ManifestEditError.swift
ManifestSyntaxRepresentable.swift
PackageDependency+Syntax.swift
PluginUsage+Syntax.swift
PackageEditResult.swift
ProductDescription+Syntax.swift
SyntaxEditUtils.swift
Expand Down
3 changes: 3 additions & 0 deletions Sources/PackageModelSyntax/ManifestEditError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ package enum ManifestEditError: Error {
case oldManifest(ToolsVersion, expected: ToolsVersion)
case cannotAddSettingsToPluginTarget
case existingDependency(dependencyName: String)
case existingPlugin(pluginName: String, taget: String)
}

extension ToolsVersion {
Expand All @@ -49,6 +50,8 @@ extension ManifestEditError: CustomStringConvertible {
"plugin targets do not support settings"
case .existingDependency(let name):
"unable to add dependency '\(name)' because it already exists in the list of dependencies"
case .existingPlugin(let name, let taget):
"unable to add plugin '\(name)' to taget '\(taget)' because it already exists in the list of plugins"
}
}
}
Expand Down
28 changes: 28 additions & 0 deletions Sources/PackageModelSyntax/PluginUsage+Syntax.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import PackageModel
import SwiftSyntax
import SwiftSyntaxBuilder

extension TargetDescription.PluginUsage: ManifestSyntaxRepresentable {
func asSyntax() -> ExprSyntax {
switch self {
case let .plugin(name: name, package: package):
if let package {
return ".plugin(name: \(literal: name.description), package: \(literal: package.description))"
} else {
return ".plugin(name: \(literal: name.description))"
}
}
}
}
Loading