Skip to content

Commit e7ebf3a

Browse files
committed
Commands: split SwiftPackageTool into multiple files
1 parent f28c338 commit e7ebf3a

24 files changed

+1880
-1910
lines changed

Package.swift

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -585,19 +585,19 @@ if ProcessInfo.processInfo.environment["SWIFTPM_LLBUILD_FWK"] == nil {
585585
package.targets.first(where: { $0.name == "SPMLLBuild" })!.dependencies += [.product(name: "llbuildSwift", package: "swift-llbuild")]
586586
}
587587

588-
if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
589-
package.dependencies += [
590-
.package(url: "https://github.com/apple/swift-tools-support-core.git", .branch(relatedDependenciesBranch)),
591-
// The 'swift-argument-parser' version declared here must match that
592-
// used by 'swift-driver' and 'sourcekit-lsp'. Please coordinate
593-
// dependency version changes here with those projects.
594-
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.0.3")),
595-
.package(url: "https://github.com/apple/swift-driver.git", .branch(relatedDependenciesBranch)),
596-
.package(url: "https://github.com/apple/swift-crypto.git", .upToNextMinor(from: minimumCryptoVersion)),
597-
.package(url: "https://github.com/apple/swift-system.git", .upToNextMinor(from: "1.1.1")),
598-
.package(url: "https://github.com/apple/swift-collections.git", .upToNextMinor(from: "1.0.1")),
599-
]
600-
} else {
588+
//if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
589+
// package.dependencies += [
590+
// .package(url: "https://github.com/apple/swift-tools-support-core.git", .branch(relatedDependenciesBranch)),
591+
// // The 'swift-argument-parser' version declared here must match that
592+
// // used by 'swift-driver' and 'sourcekit-lsp'. Please coordinate
593+
// // dependency version changes here with those projects.
594+
// .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.0.3")),
595+
// .package(url: "https://github.com/apple/swift-driver.git", .branch(relatedDependenciesBranch)),
596+
// .package(url: "https://github.com/apple/swift-crypto.git", .upToNextMinor(from: minimumCryptoVersion)),
597+
// .package(url: "https://github.com/apple/swift-system.git", .upToNextMinor(from: "1.1.1")),
598+
// .package(url: "https://github.com/apple/swift-collections.git", .upToNextMinor(from: "1.0.1")),
599+
// ]
600+
//} else {
601601
package.dependencies += [
602602
.package(path: "../swift-tools-support-core"),
603603
.package(path: "../swift-argument-parser"),
@@ -606,4 +606,4 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
606606
.package(path: "../swift-system"),
607607
.package(path: "../swift-collections"),
608608
]
609-
}
609+
//}

Sources/Commands/CMakeLists.txt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@
88

99
add_library(Commands
1010
Options.swift
11+
PackageTools/APIDiff.swift
12+
PackageTools/ArchiveSource.swift
13+
PackageTools/CompletionTool.swift
14+
PackageTools/ComputeChecksum.swift
15+
PackageTools/Config.swift
16+
PackageTools/Describe.swift
17+
PackageTools/DumpCommands.swift
18+
PackageTools/EditCommands.swift
19+
PackageTools/Format.swift
20+
PackageTools/Init.swift
21+
PackageTools/Learn.swift
22+
PackageTools/PluginCommand.swift
23+
PackageTools/ResetCommands.swift
24+
PackageTools/Resolve.swift
25+
PackageTools/ShowDependencies.swift
26+
PackageTools/SwiftPackageCollectionsTool.swift
27+
PackageTools/SwiftPackageRegistryTool.swift
28+
PackageTools/SwiftPackageTool.swift
29+
PackageTools/ToolsVersionCommand.swift
30+
PackageTools/Update.swift
1131
Snippets/CardEvent.swift
1232
Snippets/Cards/SnippetCard.swift
1333
Snippets/Cards/SnippetGroupCard.swift
@@ -24,7 +44,7 @@ add_library(Commands
2444
SwiftTool.swift
2545
Utilities/APIDigester.swift
2646
Utilities/DependenciesSerializer.swift
27-
Utilities/Describe.swift
47+
Utilities/DescribedPackage.swift
2848
Utilities/DOTManifestSerializer.swift
2949
Utilities/GenerateLinuxMain.swift
3050
Utilities/MultiRootSupport.swift
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2014-2022 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 Dispatch
16+
import PackageGraph
17+
import PackageModel
18+
import SourceControl
19+
import TSCBasic
20+
21+
struct DeprecatedAPIDiff: ParsableCommand {
22+
static let configuration = CommandConfiguration(commandName: "experimental-api-diff",
23+
abstract: "Deprecated - use `swift package diagnose-api-breaking-changes` instead",
24+
shouldDisplay: false)
25+
26+
@Argument(parsing: .unconditionalRemaining)
27+
var args: [String] = []
28+
29+
func run() throws {
30+
print("`swift package experimental-api-diff` has been renamed to `swift package diagnose-api-breaking-changes`")
31+
throw ExitCode.failure
32+
}
33+
}
34+
35+
struct APIDiff: SwiftCommand {
36+
static let configuration = CommandConfiguration(
37+
commandName: "diagnose-api-breaking-changes",
38+
abstract: "Diagnose API-breaking changes to Swift modules in a package",
39+
discussion: """
40+
The diagnose-api-breaking-changes command can be used to compare the Swift API of \
41+
a package to a baseline revision, diagnosing any breaking changes which have \
42+
been introduced. By default, it compares every Swift module from the baseline \
43+
revision which is part of a library product. For packages with many targets, this \
44+
behavior may be undesirable as the comparison can be slow. \
45+
The `--products` and `--targets` options may be used to restrict the scope of \
46+
the comparison.
47+
""")
48+
49+
@OptionGroup(_hiddenFromHelp: true)
50+
var globalOptions: GlobalOptions
51+
52+
@Option(help: """
53+
The path to a text file containing breaking changes which should be ignored by the API comparison. \
54+
Each ignored breaking change in the file should appear on its own line and contain the exact message \
55+
to be ignored (e.g. 'API breakage: func foo() has been removed').
56+
""")
57+
var breakageAllowlistPath: AbsolutePath?
58+
59+
@Argument(help: "The baseline treeish to compare to (e.g. a commit hash, branch name, tag, etc.)")
60+
var treeish: String
61+
62+
@Option(parsing: .upToNextOption,
63+
help: "One or more products to include in the API comparison. If present, only the specified products (and any targets specified using `--targets`) will be compared.")
64+
var products: [String] = []
65+
66+
@Option(parsing: .upToNextOption,
67+
help: "One or more targets to include in the API comparison. If present, only the specified targets (and any products specified using `--products`) will be compared.")
68+
var targets: [String] = []
69+
70+
@Option(name: .customLong("baseline-dir"),
71+
help: "The path to a directory used to store API baseline files. If unspecified, a temporary directory will be used.")
72+
var overrideBaselineDir: AbsolutePath?
73+
74+
@Flag(help: "Regenerate the API baseline, even if an existing one is available.")
75+
var regenerateBaseline: Bool = false
76+
77+
func run(_ swiftTool: SwiftTool) throws {
78+
let apiDigesterPath = try swiftTool.getDestinationToolchain().getSwiftAPIDigester()
79+
let apiDigesterTool = SwiftAPIDigester(fileSystem: swiftTool.fileSystem, tool: apiDigesterPath)
80+
81+
let packageRoot = try globalOptions.locations.packageDirectory ?? swiftTool.getPackageRoot()
82+
let repository = GitRepository(path: packageRoot)
83+
let baselineRevision = try repository.resolveRevision(identifier: treeish)
84+
85+
// We turn build manifest caching off because we need the build plan.
86+
let buildSystem = try swiftTool.createBuildSystem(explicitBuildSystem: .native, cacheBuildManifest: false)
87+
88+
let packageGraph = try buildSystem.getPackageGraph()
89+
let modulesToDiff = try determineModulesToDiff(
90+
packageGraph: packageGraph,
91+
observabilityScope: swiftTool.observabilityScope
92+
)
93+
94+
// Build the current package.
95+
try buildSystem.build()
96+
97+
// Dump JSON for the baseline package.
98+
let baselineDumper = try APIDigesterBaselineDumper(
99+
baselineRevision: baselineRevision,
100+
packageRoot: swiftTool.getPackageRoot(),
101+
buildParameters: try buildSystem.buildPlan.buildParameters,
102+
apiDigesterTool: apiDigesterTool,
103+
observabilityScope: swiftTool.observabilityScope
104+
)
105+
106+
let baselineDir = try baselineDumper.emitAPIBaseline(
107+
for: modulesToDiff,
108+
at: overrideBaselineDir,
109+
force: regenerateBaseline,
110+
logLevel: swiftTool.logLevel,
111+
swiftTool: swiftTool
112+
)
113+
114+
let results = ThreadSafeArrayStore<SwiftAPIDigester.ComparisonResult>()
115+
let group = DispatchGroup()
116+
let semaphore = DispatchSemaphore(value: Int(try buildSystem.buildPlan.buildParameters.jobs))
117+
var skippedModules: Set<String> = []
118+
119+
for module in modulesToDiff {
120+
let moduleBaselinePath = baselineDir.appending(component: "\(module).json")
121+
guard swiftTool.fileSystem.exists(moduleBaselinePath) else {
122+
print("\nSkipping \(module) because it does not exist in the baseline")
123+
skippedModules.insert(module)
124+
continue
125+
}
126+
semaphore.wait()
127+
DispatchQueue.sharedConcurrent.async(group: group) {
128+
do {
129+
if let comparisonResult = try apiDigesterTool.compareAPIToBaseline(
130+
at: moduleBaselinePath,
131+
for: module,
132+
buildPlan: try buildSystem.buildPlan,
133+
except: breakageAllowlistPath
134+
) {
135+
results.append(comparisonResult)
136+
}
137+
} catch {
138+
swiftTool.observabilityScope.emit(error: "failed to compare API to baseline: \(error)")
139+
}
140+
semaphore.signal()
141+
}
142+
}
143+
144+
group.wait()
145+
146+
let failedModules = modulesToDiff
147+
.subtracting(skippedModules)
148+
.subtracting(results.map(\.moduleName))
149+
for failedModule in failedModules {
150+
swiftTool.observabilityScope.emit(error: "failed to read API digester output for \(failedModule)")
151+
}
152+
153+
for result in results.get() {
154+
try self.printComparisonResult(result, observabilityScope: swiftTool.observabilityScope)
155+
}
156+
157+
guard failedModules.isEmpty && results.get().allSatisfy(\.hasNoAPIBreakingChanges) else {
158+
throw ExitCode.failure
159+
}
160+
}
161+
162+
private func determineModulesToDiff(packageGraph: PackageGraph, observabilityScope: ObservabilityScope) throws -> Set<String> {
163+
var modulesToDiff: Set<String> = []
164+
if products.isEmpty && targets.isEmpty {
165+
modulesToDiff.formUnion(packageGraph.apiDigesterModules)
166+
} else {
167+
for productName in products {
168+
guard let product = packageGraph
169+
.rootPackages
170+
.flatMap(\.products)
171+
.first(where: { $0.name == productName }) else {
172+
observabilityScope.emit(error: "no such product '\(productName)'")
173+
continue
174+
}
175+
guard product.type.isLibrary else {
176+
observabilityScope.emit(error: "'\(productName)' is not a library product")
177+
continue
178+
}
179+
modulesToDiff.formUnion(product.targets.filter { $0.underlyingTarget is SwiftTarget }.map(\.c99name))
180+
}
181+
for targetName in targets {
182+
guard let target = packageGraph
183+
.rootPackages
184+
.flatMap(\.targets)
185+
.first(where: { $0.name == targetName }) else {
186+
observabilityScope.emit(error: "no such target '\(targetName)'")
187+
continue
188+
}
189+
guard target.type == .library else {
190+
observabilityScope.emit(error: "'\(targetName)' is not a library target")
191+
continue
192+
}
193+
guard target.underlyingTarget is SwiftTarget else {
194+
observabilityScope.emit(error: "'\(targetName)' is not a Swift language target")
195+
continue
196+
}
197+
modulesToDiff.insert(target.c99name)
198+
}
199+
guard !observabilityScope.errorsReported else {
200+
throw ExitCode.failure
201+
}
202+
}
203+
return modulesToDiff
204+
}
205+
206+
private func printComparisonResult(
207+
_ comparisonResult: SwiftAPIDigester.ComparisonResult,
208+
observabilityScope: ObservabilityScope
209+
) throws {
210+
for diagnostic in comparisonResult.otherDiagnostics {
211+
let metadata = try diagnostic.location.map { location -> ObservabilityMetadata in
212+
var metadata = ObservabilityMetadata()
213+
metadata.fileLocation = .init(
214+
try .init(validating: location.filename),
215+
line: location.line < Int.max ? Int(location.line) : .none
216+
)
217+
return metadata
218+
}
219+
220+
switch diagnostic.level {
221+
case .error, .fatal:
222+
observabilityScope.emit(error: diagnostic.text, metadata: metadata)
223+
case .warning:
224+
observabilityScope.emit(warning: diagnostic.text, metadata: metadata)
225+
case .note:
226+
observabilityScope.emit(info: diagnostic.text, metadata: metadata)
227+
case .remark:
228+
observabilityScope.emit(info: diagnostic.text, metadata: metadata)
229+
case .ignored:
230+
break
231+
}
232+
}
233+
234+
let moduleName = comparisonResult.moduleName
235+
if comparisonResult.apiBreakingChanges.isEmpty {
236+
print("\nNo breaking changes detected in \(moduleName)")
237+
} else {
238+
let count = comparisonResult.apiBreakingChanges.count
239+
print("\n\(count) breaking \(count > 1 ? "changes" : "change") detected in \(moduleName):")
240+
for change in comparisonResult.apiBreakingChanges {
241+
print(" 💔 \(change.text)")
242+
}
243+
}
244+
}
245+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2014-2022 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 SourceControl
15+
import TSCBasic
16+
17+
extension SwiftPackageTool {
18+
struct ArchiveSource: SwiftCommand {
19+
static let configuration = CommandConfiguration(
20+
commandName: "archive-source",
21+
abstract: "Create a source archive for the package"
22+
)
23+
24+
@OptionGroup(_hiddenFromHelp: true)
25+
var globalOptions: GlobalOptions
26+
27+
@Option(
28+
name: [.short, .long],
29+
help: "The absolute or relative path for the generated source archive"
30+
)
31+
var output: AbsolutePath?
32+
33+
func run(_ swiftTool: SwiftTool) throws {
34+
let packageRoot = try globalOptions.locations.packageDirectory ?? swiftTool.getPackageRoot()
35+
let repository = GitRepository(path: packageRoot)
36+
37+
let destination: AbsolutePath
38+
if let output = output {
39+
destination = output
40+
} else {
41+
let graph = try swiftTool.loadPackageGraph()
42+
let packageName = graph.rootPackages[0].manifest.displayName // TODO: use identity instead?
43+
destination = packageRoot.appending(component: "\(packageName).zip")
44+
}
45+
46+
try repository.archive(to: destination)
47+
48+
if destination.isDescendantOfOrEqual(to: packageRoot) {
49+
let relativePath = destination.relative(to: packageRoot)
50+
print("Created \(relativePath.pathString)")
51+
} else {
52+
print("Created \(destination.pathString)")
53+
}
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)