Skip to content

[SE-0301] Add swift package add-product command and supporting library #7477

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 1 commit into from
Apr 23, 2024
Merged
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 @@ -8,6 +8,7 @@

add_library(Commands
PackageCommands/AddDependency.swift
PackageCommands/AddProduct.swift
PackageCommands/AddTarget.swift
PackageCommands/APIDiff.swift
PackageCommands/ArchiveSource.swift
Expand Down
118 changes: 118 additions & 0 deletions Sources/Commands/PackageCommands/AddProduct.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2024 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

extension SwiftPackageCommand {
struct AddProduct: SwiftCommand {
/// The package product type used for the command-line. This is a
/// subset of `ProductType` that expands out the library types.
enum CommandProductType: String, Codable, ExpressibleByArgument {
case executable
case library
case staticLibrary = "static-library"
case dynamicLibrary = "dynamic-library"
case plugin
}

package static let configuration = CommandConfiguration(
abstract: "Add a new product to the manifest")

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

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

@Option(help: "The type of target to add, which can be one of 'executable', 'library', 'static-library', 'dynamic-library', or 'plugin'")
var type: CommandProductType = .library

@Option(
parsing: .upToNextOption,
help: "A list of targets that are part of this product"
)
var targets: [String] = []

@Option(help: "The URL for a remote binary target")
var url: String?

@Option(help: "The path to a local binary target")
var path: String?

@Option(help: "The checksum for a remote binary target")
var checksum: 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)
}
}
}

// Map the product type.
let type: ProductType = switch self.type {
case .executable: .executable
case .library: .library(.automatic)
case .dynamicLibrary: .library(.dynamic)
case .staticLibrary: .library(.static)
case .plugin: .plugin
}

let product = try ProductDescription(
name: name,
type: type,
targets: targets
)

let editResult = try PackageModelSyntax.AddProduct.addProduct(
product,
to: manifestSyntax
)

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

6 changes: 1 addition & 5 deletions Sources/Commands/PackageCommands/AddTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ extension SwiftPackageCommand {
case library
case executable
case test
case binary
case plugin
case macro
}

Expand All @@ -42,7 +40,7 @@ extension SwiftPackageCommand {
@Argument(help: "The name of the new target")
var name: String

@Option(help: "The type of target to add, which can be one of ")
@Option(help: "The type of target to add, which can be one of 'library', 'executable', 'test', or 'macro'")
var type: TargetType = .library

@Option(
Expand Down Expand Up @@ -91,8 +89,6 @@ extension SwiftPackageCommand {
case .library: .regular
case .executable: .executable
case .test: .test
case .binary: .binary
case .plugin: .plugin
case .macro: .macro
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ package struct SwiftPackageCommand: AsyncParsableCommand {
version: SwiftVersion.current.completeDisplayString,
subcommands: [
AddDependency.self,
AddProduct.self,
AddTarget.self,
Clean.self,
PurgeCache.self,
Expand Down
58 changes: 58 additions & 0 deletions Sources/PackageModelSyntax/AddProduct.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2024 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 SwiftParser
import SwiftSyntax
import SwiftSyntaxBuilder

/// Add a product to the manifest's source code.
public struct AddProduct {
/// The set of argument labels that can occur after the "products"
/// argument in the Package 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 argumentLabelsAfterProducts: Set<String> = [
"dependencies",
"targets",
"swiftLanguageVersions",
"cLanguageStandard",
"cxxLanguageStandard"
]

/// Produce the set of source edits needed to add the given package
/// dependency to the given manifest file.
public static func addProduct(
_ product: ProductDescription,
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
}

let newPackageCall = try packageCall.appendingToArrayArgument(
label: "products",
trailingLabels: argumentLabelsAfterProducts,
newElement: product.asSyntax()
)

return PackageEditResult(
manifestEdits: [
.replace(packageCall, with: newPackageCall.description)
]
)
}
}
2 changes: 2 additions & 0 deletions Sources/PackageModelSyntax/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@

add_library(PackageModelSyntax
AddPackageDependency.swift
AddProduct.swift
AddTarget.swift
ManifestEditError.swift
ManifestSyntaxRepresentable.swift
PackageDependency+Syntax.swift
PackageEditResult.swift
ProductDescription+Syntax.swift
SyntaxEditUtils.swift
TargetDescription+Syntax.swift
)
Expand Down
61 changes: 61 additions & 0 deletions Sources/PackageModelSyntax/ProductDescription+Syntax.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2024 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 PackageModel
import SwiftSyntax
import SwiftParser

extension ProductDescription: ManifestSyntaxRepresentable {
/// The function name in the package manifest.
///
/// Some of these are actually invalid, but it's up to the caller
/// to check the precondition.
private var functionName: String {
switch type {
case .executable: "executable"
case .library(_): "library"
case .macro: "macro"
case .plugin: "plugin"
case .snippet: "snippet"
case .test: "test"
}
}

func asSyntax() -> ExprSyntax {
var arguments: [LabeledExprSyntax] = []
arguments.append(label: "name", stringLiteral: name)

// Libraries have a type.
if case .library(let libraryType) = type {
switch libraryType {
case .automatic:
break

case .dynamic, .static:
arguments.append(
label: "type",
expression: ".\(raw: libraryType.rawValue)"
)
}
}

arguments.appendIfNonEmpty(
label: "targets",
arrayLiteral: targets
)

let separateParen: String = arguments.count > 1 ? "\n" : ""
let argumentsSyntax = LabeledExprListSyntax(arguments)
return ".\(raw: functionName)(\(argumentsSyntax)\(raw: separateParen))"
}
}
41 changes: 34 additions & 7 deletions Sources/PackageModelSyntax/SyntaxEditUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ extension Trivia {
var hasNewlines: Bool {
contains(where: \.isNewline)
}

/// Produce trivia from the last newline to the end, dropping anything
/// prior to that.
func onlyLastLine() -> Trivia {
guard let lastNewline = pieces.lastIndex(where: { $0.isNewline }) else {
return self
}

return Trivia(pieces: pieces[lastNewline...])
}
}

/// Syntax walker to find the first occurrence of a given node kind that
Expand Down Expand Up @@ -186,7 +196,7 @@ extension ArrayExprSyntax {
if let last = elements.last {
// The leading trivia of the new element should match that of the
// last element.
leadingTrivia = last.leadingTrivia
leadingTrivia = last.leadingTrivia.onlyLastLine()

// Add a trailing comma to the last element if it isn't already
// there.
Expand Down Expand Up @@ -324,14 +334,31 @@ extension Array<LabeledExprSyntax> {
elements.append(expression: element.asSyntax())
}

// When we have more than one element in the array literal, we add
// newlines at the beginning of each element. Do the same for the
// right square bracket.
let rightSquareLeadingTrivia: Trivia = elements.count > 0
? .newline
: Trivia()
// Figure out the trivia for the left and right square
let leftSquareTrailingTrivia: Trivia
let rightSquareLeadingTrivia: Trivia
switch elements.count {
case 0:
// Put a single space between the square brackets.
leftSquareTrailingTrivia = Trivia()
rightSquareLeadingTrivia = .space

case 1:
// Put spaces around the single element
leftSquareTrailingTrivia = .space
rightSquareLeadingTrivia = .space

default:
// Each of the elements will have a leading newline. Add a leading
// newline before the close bracket.
leftSquareTrailingTrivia = Trivia()
rightSquareLeadingTrivia = .newline
}

let array = ArrayExprSyntax(
leftSquare: .leftSquareToken(
trailingTrivia: leftSquareTrailingTrivia
),
elements: ArrayElementListSyntax(elements),
rightSquare: .rightSquareToken(
leadingTrivia: rightSquareLeadingTrivia
Expand Down
30 changes: 30 additions & 0 deletions Tests/CommandsTests/PackageCommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,36 @@ final class PackageCommandTests: CommandsTestCase {
}
}

func testPackageAddProduct() throws {
try testWithTemporaryDirectory { tmpPath in
let fs = localFileSystem
let path = tmpPath.appending("PackageB")
try fs.createDirectory(path)

try fs.writeFileContents(path.appending("Package.swift"), string:
"""
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "client"
)
"""
)

_ = try execute(["add-product", "MyLib", "--targets", "MyLib", "--type", "static-library"], packagePath: path)

let manifest = path.appending("Package.swift")
XCTAssertFileExists(manifest)
let contents: String = try fs.readFileContents(manifest)

XCTAssertMatch(contents, .contains(#"products:"#))
XCTAssertMatch(contents, .contains(#".library"#))
XCTAssertMatch(contents, .contains(#"name: "MyLib""#))
XCTAssertMatch(contents, .contains(#"type: .static"#))
XCTAssertMatch(contents, .contains(#"targets:"#))
XCTAssertMatch(contents, .contains(#""MyLib""#))
}
}
func testPackageEditAndUnedit() throws {
try fixture(name: "Miscellaneous/PackageEdit") { fixturePath in
let fooPath = fixturePath.appending("foo")
Expand Down
Loading