Skip to content

Commit 2f0db3b

Browse files
committed
[SE-0301] Introduce swift package add-target support to add targets to a package
Add a new package command to add a target with the given name, type, and dependencies to the package. This includes adding the target to the package manifest as well as creating stub source files for the target to guide the user. For example, this command: swift package add-target SocialGraphClientTests --dependencies SocialGraphClient --type test adds the following to the targets in the package manifest: .testTarget(name: "SocialGraphClientTests", dependencies: ["SocialGraphClient"]), as well as creating the file `Tests/SocialGraphClientTests/SocialGraphClientTests.swift`, which looks like this: import SocialGraphClient import XCTest class SocialGraphClientTests: XCTestCase { func testSocialGraphClientTests() { XCTAssertEqual(42, 17 + 25) } } There is, undoubtedly, some tuning to do to clean this up. Here's the command-line interface, which mostly aligns with SE-0301: OVERVIEW: Add a new target to the manifest USAGE: swift package add-target <name> [--type <type>] [--dependencies <dependencies> ...] [--url <url>] [--path <path>] [--checksum <checksum>] ARGUMENTS: <name> The name of the new target OPTIONS: --type <type> The type of target to add, which can be one of (default: library) --dependencies <dependencies> A list of target dependency names --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 6010ecc commit 2f0db3b

File tree

10 files changed

+399
-92
lines changed

10 files changed

+399
-92
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ let package = Package(
256256
"PackageModel",
257257
.product(name: "SwiftBasicFormat", package: "swift-syntax"),
258258
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
259+
.product(name: "SwiftIDEUtils", package: "swift-syntax"),
259260
.product(name: "SwiftParser", package: "swift-syntax"),
260261
.product(name: "SwiftSyntax", package: "swift-syntax"),
261262
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),

Sources/Commands/CMakeLists.txt

Lines changed: 0 additions & 74 deletions
This file was deleted.

Sources/Commands/PackageCommands/AddDependency.swift

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import Basics
1515
import CoreCommands
1616
import PackageModel
1717
import PackageModelSyntax
18-
@_spi(FixItApplier) import SwiftIDEUtils
1918
import SwiftParser
2019
import SwiftSyntax
2120
import TSCBasic
@@ -140,19 +139,16 @@ extension SwiftPackageCommand {
140139
productFilter: .everything
141140
)
142141

143-
let edits = try AddPackageDependency.addPackageDependency(
142+
let editResult = try AddPackageDependency.addPackageDependency(
144143
packageDependency,
145144
to: manifestSyntax
146145
)
147146

148-
if edits.isEmpty {
149-
throw StringError("Unable to add package to manifest file")
150-
}
151-
152-
let updatedManifestSource = FixItApplier.apply(edits: edits, to: manifestSyntax)
153-
try fileSystem.writeFileContents(
154-
manifestPath,
155-
string: updatedManifestSource
147+
try editResult.applyEdits(
148+
to: fileSystem,
149+
manifest: manifestSyntax,
150+
manifestPath: manifestPath,
151+
verbose: !globalOptions.logging.quiet
156152
)
157153
}
158154
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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 AddTarget: SwiftCommand {
26+
/// The type of target that can be specified on the command line.
27+
enum TargetType: String, Codable, ExpressibleByArgument {
28+
case library
29+
case executable
30+
case test
31+
case binary
32+
case plugin
33+
case macro
34+
}
35+
36+
package static let configuration = CommandConfiguration(
37+
abstract: "Add a new target to the manifest")
38+
39+
@OptionGroup(visibility: .hidden)
40+
var globalOptions: GlobalOptions
41+
42+
@Argument(help: "The name of the new target")
43+
var name: String
44+
45+
@Option(help: "The type of target to add, which can be one of ")
46+
var type: TargetType = .library
47+
48+
@Option(
49+
parsing: .upToNextOption,
50+
help: "A list of target dependency names"
51+
)
52+
var dependencies: [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 target type.
90+
let type: TargetDescription.TargetType = switch self.type {
91+
case .library: .regular
92+
case .executable: .executable
93+
case .test: .test
94+
case .binary: .binary
95+
case .plugin: .plugin
96+
case .macro: .macro
97+
}
98+
99+
// Map dependencies
100+
let dependencies: [TargetDescription.Dependency] =
101+
self.dependencies.map {
102+
.byName(name: $0, condition: nil)
103+
}
104+
105+
let target = try TargetDescription(
106+
name: name,
107+
dependencies: dependencies,
108+
path: path,
109+
url: url,
110+
type: type,
111+
checksum: checksum
112+
)
113+
114+
let editResult = try PackageModelSyntax.AddTarget.addTarget(
115+
target,
116+
to: manifestSyntax
117+
)
118+
119+
try editResult.applyEdits(
120+
to: fileSystem,
121+
manifest: manifestSyntax,
122+
manifestPath: manifestPath,
123+
verbose: !globalOptions.logging.quiet
124+
)
125+
}
126+
}
127+
}
128+

Sources/Commands/PackageCommands/SwiftPackageCommand.swift

Lines changed: 1 addition & 0 deletions
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+
AddTarget.self,
3940
Clean.self,
4041
PurgeCache.self,
4142
Reset.self,

Sources/PackageModelSyntax/AddPackageDependency.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,20 @@ public struct AddPackageDependency {
3636
public static func addPackageDependency(
3737
_ dependency: PackageDependency,
3838
to manifest: SourceFileSyntax
39-
) throws -> [SourceEdit] {
39+
) throws -> PackageEditResult {
4040
// Make sure we have a suitable tools version in the manifest.
4141
try manifest.checkEditManifestToolsVersion()
4242

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

47-
return try packageCall.appendingToArrayArgument(
47+
let edits = try packageCall.appendingToArrayArgument(
4848
label: "dependencies",
4949
trailingLabels: Self.argumentLabelsAfterDependencies,
5050
newElement: dependency.asSyntax()
5151
)
52+
53+
return PackageEditResult(manifestEdits: edits)
5254
}
5355
}

Sources/PackageModelSyntax/AddTarget.swift

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,105 @@ public struct AddTarget {
2929
"cxxLanguageStandard"
3030
]
3131

32+
/// Add the given target to the manifest, producing a set of edit results
33+
/// that updates the manifest and adds some source files to stub out the
34+
/// new target.
3235
public static func addTarget(
3336
_ target: TargetDescription,
3437
to manifest: SourceFileSyntax
35-
) throws -> [SourceEdit] {
38+
) throws -> PackageEditResult {
3639
// Make sure we have a suitable tools version in the manifest.
3740
try manifest.checkEditManifestToolsVersion()
3841

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

43-
return try packageCall.appendingToArrayArgument(
46+
let manifestEdits = try packageCall.appendingToArrayArgument(
4447
label: "targets",
4548
trailingLabels: Self.argumentLabelsAfterTargets,
4649
newElement: target.asSyntax()
4750
)
51+
52+
let outerDirectory: String? = switch target.type {
53+
case .binary, .macro, .plugin, .system: nil
54+
case .executable, .regular: "Sources"
55+
case .test: "Tests"
56+
}
57+
58+
guard let outerDirectory else {
59+
return PackageEditResult(manifestEdits: manifestEdits)
60+
}
61+
62+
let sourceFilePath = try RelativePath(validating: outerDirectory)
63+
.appending(components: [target.name, "\(target.name).swift"])
64+
65+
// Introduce imports for each of the dependencies that were specified.
66+
var importModuleNames = target.dependencies.map {
67+
$0.name
68+
}
69+
70+
// Add appropriate test module dependencies.
71+
if target.type == .test {
72+
importModuleNames.append("XCTest")
73+
}
74+
75+
let importDecls = importModuleNames.lazy.sorted().map { name in
76+
DeclSyntax("import \(raw: name)").with(\.trailingTrivia, .newline)
77+
}
78+
79+
let imports = CodeBlockItemListSyntax {
80+
for importDecl in importDecls {
81+
importDecl
82+
}
83+
}
84+
85+
let sourceFileText: SourceFileSyntax = switch target.type {
86+
case .binary, .macro, .plugin, .system:
87+
fatalError("should have exited above")
88+
89+
case .test:
90+
"""
91+
\(imports)
92+
class \(raw: target.name): XCTestCase {
93+
func test\(raw: target.name)() {
94+
XCTAssertEqual(42, 17 + 25)
95+
}
96+
}
97+
"""
98+
99+
case .regular:
100+
"""
101+
\(imports)
102+
"""
103+
104+
case .executable:
105+
"""
106+
\(imports)
107+
@main
108+
struct \(raw: target.name)Main {
109+
static func main() {
110+
print("Hello, world")
111+
}
112+
}
113+
"""
114+
}
115+
116+
return PackageEditResult(
117+
manifestEdits: manifestEdits,
118+
auxiliaryFiles: [(sourceFilePath, sourceFileText)]
119+
)
120+
}
121+
}
122+
123+
fileprivate extension TargetDescription.Dependency {
124+
/// Retrieve the name of the dependency
125+
var name: String {
126+
switch self {
127+
case .target(name: let name, condition: _),
128+
.byName(name: let name, condition: _),
129+
.product(name: let name, package: _, moduleAliases: _, condition: _):
130+
name
131+
}
48132
}
49133
}

0 commit comments

Comments
 (0)