Skip to content

Commit 567979c

Browse files
authored
Commands: split SwiftPackageTool into multiple files (#5883)
`SwiftPackageTool.swift` is quite big, it would be great to split it into multiple files for easier maintenance. Moved `SwiftPackageTool` subcommands into separate files of new `PackageTools` subdirectory.
1 parent 474a472 commit 567979c

23 files changed

+1884
-1572
lines changed

Sources/Commands/CMakeLists.txt

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@
77
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors
88

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

0 commit comments

Comments
 (0)