Skip to content

Commit a408054

Browse files
authored
[SE-0301] Add swift package add-product command and supporting library (swiftlang#7477)
Introduce support for adding a new product to the package manifest, both programmatically (via PackageModelSyntax) and via the `swift package add-product` command. Help for this command is: OVERVIEW: Add a new product to the manifest USAGE: swift package add-product <name> [--type <type>] [--targets <targets> ...] [--url <url>] [--path <path>] [--checksum <checksum>] ARGUMENTS: <name> The name of the new product OPTIONS: --type <type> The type of target to add, which can be one of 'executable', 'library', 'static-library', 'dynamic-library', or 'plugin' (default: library) --targets <targets> A list of targets that are part of this product --url <url> The URL for a remote binary target --path <path> The path to a local binary target --checksum <checksum> The checksum for a remote binary target --version Show the version. -h, -help, --help Show help information.
1 parent d30647a commit a408054

File tree

10 files changed

+345
-12
lines changed

10 files changed

+345
-12
lines changed

Sources/Commands/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
add_library(Commands
1010
PackageCommands/AddDependency.swift
11+
PackageCommands/AddProduct.swift
1112
PackageCommands/AddTarget.swift
1213
PackageCommands/APIDiff.swift
1314
PackageCommands/ArchiveSource.swift
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2014-2024 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 AddProduct: SwiftCommand {
26+
/// The package product type used for the command-line. This is a
27+
/// subset of `ProductType` that expands out the library types.
28+
enum CommandProductType: String, Codable, ExpressibleByArgument {
29+
case executable
30+
case library
31+
case staticLibrary = "static-library"
32+
case dynamicLibrary = "dynamic-library"
33+
case plugin
34+
}
35+
36+
package static let configuration = CommandConfiguration(
37+
abstract: "Add a new product to the manifest")
38+
39+
@OptionGroup(visibility: .hidden)
40+
var globalOptions: GlobalOptions
41+
42+
@Argument(help: "The name of the new product")
43+
var name: String
44+
45+
@Option(help: "The type of target to add, which can be one of 'executable', 'library', 'static-library', 'dynamic-library', or 'plugin'")
46+
var type: CommandProductType = .library
47+
48+
@Option(
49+
parsing: .upToNextOption,
50+
help: "A list of targets that are part of this product"
51+
)
52+
var targets: [String] = []
53+
54+
@Option(help: "The URL for a remote binary target")
55+
var url: String?
56+
57+
@Option(help: "The path to a local binary target")
58+
var path: String?
59+
60+
@Option(help: "The checksum for a remote binary target")
61+
var checksum: String?
62+
63+
func run(_ swiftCommandState: SwiftCommandState) throws {
64+
let workspace = try swiftCommandState.getActiveWorkspace()
65+
66+
guard let packagePath = try swiftCommandState.getWorkspaceRoot().packages.first else {
67+
throw StringError("unknown package")
68+
}
69+
70+
// Load the manifest file
71+
let fileSystem = workspace.fileSystem
72+
let manifestPath = packagePath.appending("Package.swift")
73+
let manifestContents: ByteString
74+
do {
75+
manifestContents = try fileSystem.readFileContents(manifestPath)
76+
} catch {
77+
throw StringError("cannot find package manifest in \(manifestPath)")
78+
}
79+
80+
// Parse the manifest.
81+
let manifestSyntax = manifestContents.withData { data in
82+
data.withUnsafeBytes { buffer in
83+
buffer.withMemoryRebound(to: UInt8.self) { buffer in
84+
Parser.parse(source: buffer)
85+
}
86+
}
87+
}
88+
89+
// Map the product type.
90+
let type: ProductType = switch self.type {
91+
case .executable: .executable
92+
case .library: .library(.automatic)
93+
case .dynamicLibrary: .library(.dynamic)
94+
case .staticLibrary: .library(.static)
95+
case .plugin: .plugin
96+
}
97+
98+
let product = try ProductDescription(
99+
name: name,
100+
type: type,
101+
targets: targets
102+
)
103+
104+
let editResult = try PackageModelSyntax.AddProduct.addProduct(
105+
product,
106+
to: manifestSyntax
107+
)
108+
109+
try editResult.applyEdits(
110+
to: fileSystem,
111+
manifest: manifestSyntax,
112+
manifestPath: manifestPath,
113+
verbose: !globalOptions.logging.quiet
114+
)
115+
}
116+
}
117+
}
118+

Sources/Commands/PackageCommands/AddTarget.swift

+1-5
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ extension SwiftPackageCommand {
2828
case library
2929
case executable
3030
case test
31-
case binary
32-
case plugin
3331
case macro
3432
}
3533

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

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

4846
@Option(
@@ -91,8 +89,6 @@ extension SwiftPackageCommand {
9189
case .library: .regular
9290
case .executable: .executable
9391
case .test: .test
94-
case .binary: .binary
95-
case .plugin: .plugin
9692
case .macro: .macro
9793
}
9894

Sources/Commands/PackageCommands/SwiftPackageCommand.swift

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ package struct SwiftPackageCommand: AsyncParsableCommand {
3636
version: SwiftVersion.current.completeDisplayString,
3737
subcommands: [
3838
AddDependency.self,
39+
AddProduct.self,
3940
AddTarget.self,
4041
Clean.self,
4142
PurgeCache.self,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2024 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 SwiftParser
15+
import SwiftSyntax
16+
import SwiftSyntaxBuilder
17+
18+
/// Add a product to the manifest's source code.
19+
public struct AddProduct {
20+
/// The set of argument labels that can occur after the "products"
21+
/// argument in the Package initializers.
22+
///
23+
/// TODO: Could we generate this from the the PackageDescription module, so
24+
/// we don't have keep it up-to-date manually?
25+
private static let argumentLabelsAfterProducts: Set<String> = [
26+
"dependencies",
27+
"targets",
28+
"swiftLanguageVersions",
29+
"cLanguageStandard",
30+
"cxxLanguageStandard"
31+
]
32+
33+
/// Produce the set of source edits needed to add the given package
34+
/// dependency to the given manifest file.
35+
public static func addProduct(
36+
_ product: ProductDescription,
37+
to manifest: SourceFileSyntax
38+
) throws -> PackageEditResult {
39+
// Make sure we have a suitable tools version in the manifest.
40+
try manifest.checkEditManifestToolsVersion()
41+
42+
guard let packageCall = manifest.findCall(calleeName: "Package") else {
43+
throw ManifestEditError.cannotFindPackage
44+
}
45+
46+
let newPackageCall = try packageCall.appendingToArrayArgument(
47+
label: "products",
48+
trailingLabels: argumentLabelsAfterProducts,
49+
newElement: product.asSyntax()
50+
)
51+
52+
return PackageEditResult(
53+
manifestEdits: [
54+
.replace(packageCall, with: newPackageCall.description)
55+
]
56+
)
57+
}
58+
}

Sources/PackageModelSyntax/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88

99
add_library(PackageModelSyntax
1010
AddPackageDependency.swift
11+
AddProduct.swift
1112
AddTarget.swift
1213
ManifestEditError.swift
1314
ManifestSyntaxRepresentable.swift
1415
PackageDependency+Syntax.swift
1516
PackageEditResult.swift
17+
ProductDescription+Syntax.swift
1618
SyntaxEditUtils.swift
1719
TargetDescription+Syntax.swift
1820
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2014-2024 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 PackageModel
15+
import SwiftSyntax
16+
import SwiftParser
17+
18+
extension ProductDescription: ManifestSyntaxRepresentable {
19+
/// The function name in the package manifest.
20+
///
21+
/// Some of these are actually invalid, but it's up to the caller
22+
/// to check the precondition.
23+
private var functionName: String {
24+
switch type {
25+
case .executable: "executable"
26+
case .library(_): "library"
27+
case .macro: "macro"
28+
case .plugin: "plugin"
29+
case .snippet: "snippet"
30+
case .test: "test"
31+
}
32+
}
33+
34+
func asSyntax() -> ExprSyntax {
35+
var arguments: [LabeledExprSyntax] = []
36+
arguments.append(label: "name", stringLiteral: name)
37+
38+
// Libraries have a type.
39+
if case .library(let libraryType) = type {
40+
switch libraryType {
41+
case .automatic:
42+
break
43+
44+
case .dynamic, .static:
45+
arguments.append(
46+
label: "type",
47+
expression: ".\(raw: libraryType.rawValue)"
48+
)
49+
}
50+
}
51+
52+
arguments.appendIfNonEmpty(
53+
label: "targets",
54+
arrayLiteral: targets
55+
)
56+
57+
let separateParen: String = arguments.count > 1 ? "\n" : ""
58+
let argumentsSyntax = LabeledExprListSyntax(arguments)
59+
return ".\(raw: functionName)(\(argumentsSyntax)\(raw: separateParen))"
60+
}
61+
}

Sources/PackageModelSyntax/SyntaxEditUtils.swift

+34-7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ extension Trivia {
2525
var hasNewlines: Bool {
2626
contains(where: \.isNewline)
2727
}
28+
29+
/// Produce trivia from the last newline to the end, dropping anything
30+
/// prior to that.
31+
func onlyLastLine() -> Trivia {
32+
guard let lastNewline = pieces.lastIndex(where: { $0.isNewline }) else {
33+
return self
34+
}
35+
36+
return Trivia(pieces: pieces[lastNewline...])
37+
}
2838
}
2939

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

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

327-
// When we have more than one element in the array literal, we add
328-
// newlines at the beginning of each element. Do the same for the
329-
// right square bracket.
330-
let rightSquareLeadingTrivia: Trivia = elements.count > 0
331-
? .newline
332-
: Trivia()
337+
// Figure out the trivia for the left and right square
338+
let leftSquareTrailingTrivia: Trivia
339+
let rightSquareLeadingTrivia: Trivia
340+
switch elements.count {
341+
case 0:
342+
// Put a single space between the square brackets.
343+
leftSquareTrailingTrivia = Trivia()
344+
rightSquareLeadingTrivia = .space
345+
346+
case 1:
347+
// Put spaces around the single element
348+
leftSquareTrailingTrivia = .space
349+
rightSquareLeadingTrivia = .space
350+
351+
default:
352+
// Each of the elements will have a leading newline. Add a leading
353+
// newline before the close bracket.
354+
leftSquareTrailingTrivia = Trivia()
355+
rightSquareLeadingTrivia = .newline
356+
}
333357

334358
let array = ArrayExprSyntax(
359+
leftSquare: .leftSquareToken(
360+
trailingTrivia: leftSquareTrailingTrivia
361+
),
335362
elements: ArrayElementListSyntax(elements),
336363
rightSquare: .rightSquareToken(
337364
leadingTrivia: rightSquareLeadingTrivia

Tests/CommandsTests/PackageCommandTests.swift

+30
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,36 @@ final class PackageCommandTests: CommandsTestCase {
851851
}
852852
}
853853

854+
func testPackageAddProduct() throws {
855+
try testWithTemporaryDirectory { tmpPath in
856+
let fs = localFileSystem
857+
let path = tmpPath.appending("PackageB")
858+
try fs.createDirectory(path)
859+
860+
try fs.writeFileContents(path.appending("Package.swift"), string:
861+
"""
862+
// swift-tools-version: 5.9
863+
import PackageDescription
864+
let package = Package(
865+
name: "client"
866+
)
867+
"""
868+
)
869+
870+
_ = try execute(["add-product", "MyLib", "--targets", "MyLib", "--type", "static-library"], packagePath: path)
871+
872+
let manifest = path.appending("Package.swift")
873+
XCTAssertFileExists(manifest)
874+
let contents: String = try fs.readFileContents(manifest)
875+
876+
XCTAssertMatch(contents, .contains(#"products:"#))
877+
XCTAssertMatch(contents, .contains(#".library"#))
878+
XCTAssertMatch(contents, .contains(#"name: "MyLib""#))
879+
XCTAssertMatch(contents, .contains(#"type: .static"#))
880+
XCTAssertMatch(contents, .contains(#"targets:"#))
881+
XCTAssertMatch(contents, .contains(#""MyLib""#))
882+
}
883+
}
854884
func testPackageEditAndUnedit() throws {
855885
try fixture(name: "Miscellaneous/PackageEdit") { fixturePath in
856886
let fooPath = fixturePath.appending("foo")

0 commit comments

Comments
 (0)