@@ -18,8 +18,10 @@ import PackageGraph
18
18
import PackageModel
19
19
import SourceControl
20
20
import SPMBuildCore
21
+ import TSCBasic
21
22
import TSCUtility
22
23
import _Concurrency
24
+ import Workspace
23
25
24
26
struct DeprecatedAPIDiff : ParsableCommand {
25
27
static let configuration = CommandConfiguration ( commandName: " experimental-api-diff " ,
@@ -57,7 +59,7 @@ struct APIDiff: AsyncSwiftCommand {
57
59
Each ignored breaking change in the file should appear on its own line and contain the exact message \
58
60
to be ignored (e.g. 'API breakage: func foo() has been removed').
59
61
""" )
60
- var breakageAllowlistPath : AbsolutePath ?
62
+ var breakageAllowlistPath : Basics . AbsolutePath ?
61
63
62
64
@Argument ( help: " The baseline treeish to compare to (for example, a commit hash, branch name, tag, and so on). " )
63
65
var treeish : String
@@ -75,32 +77,51 @@ struct APIDiff: AsyncSwiftCommand {
75
77
76
78
@Option ( name: . customLong( " baseline-dir " ) ,
77
79
help: " The path to a directory used to store API baseline files. If unspecified, a temporary directory will be used. " )
78
- var overrideBaselineDir : AbsolutePath ?
80
+ var overrideBaselineDir : Basics . AbsolutePath ?
79
81
80
82
@Flag ( help: " Regenerate the API baseline, even if an existing one is available. " )
81
83
var regenerateBaseline : Bool = false
82
84
83
85
func run( _ swiftCommandState: SwiftCommandState ) async throws {
84
- let apiDigesterPath = try swiftCommandState. getTargetToolchain ( ) . getSwiftAPIDigester ( )
85
- let apiDigesterTool = SwiftAPIDigester ( fileSystem: swiftCommandState. fileSystem, tool: apiDigesterPath)
86
-
87
86
let packageRoot = try globalOptions. locations. packageDirectory ?? swiftCommandState. getPackageRoot ( )
88
87
let repository = GitRepository ( path: packageRoot)
89
88
let baselineRevision = try repository. resolveRevision ( identifier: treeish)
90
89
91
- // We turn build manifest caching off because we need the build plan.
92
- let buildSystem = try await swiftCommandState. createBuildSystem (
93
- explicitBuildSystem: . native,
94
- traitConfiguration: . init( traitOptions: self . traits) ,
95
- cacheBuildManifest: false
96
- )
97
-
98
- let packageGraph = try await buildSystem. getPackageGraph ( )
99
- let modulesToDiff = try determineModulesToDiff (
90
+ let baselineDir = try overrideBaselineDir? . appending ( component: baselineRevision. identifier) ?? swiftCommandState. productsBuildParameters. apiDiff. appending ( component: " \( baselineRevision. identifier) -baselines " )
91
+ let packageGraph = try await swiftCommandState. loadPackageGraph ( )
92
+ let modulesToDiff = try Self . determineModulesToDiff (
100
93
packageGraph: packageGraph,
101
- observabilityScope: swiftCommandState. observabilityScope
94
+ productNames: products,
95
+ targetNames: targets,
96
+ observabilityScope: swiftCommandState. observabilityScope,
97
+ diagnoseMissingNames: true ,
102
98
)
103
99
100
+ if swiftCommandState. options. build. buildSystem == . swiftbuild {
101
+ try await runWithIntegratedAPIDigesterSupport (
102
+ swiftCommandState,
103
+ baselineRevision: baselineRevision,
104
+ baselineDir: baselineDir,
105
+ modulesToDiff: modulesToDiff
106
+ )
107
+ } else {
108
+ let buildSystem = try await swiftCommandState. createBuildSystem (
109
+ traitConfiguration: . init( traitOptions: self . traits) ,
110
+ cacheBuildManifest: false ,
111
+ )
112
+ try await runWithSwiftPMCoordinatedDiffing (
113
+ swiftCommandState,
114
+ buildSystem: buildSystem,
115
+ baselineRevision: baselineRevision,
116
+ modulesToDiff: modulesToDiff
117
+ )
118
+ }
119
+ }
120
+
121
+ private func runWithSwiftPMCoordinatedDiffing( _ swiftCommandState: SwiftCommandState , buildSystem: any BuildSystem , baselineRevision: Revision , modulesToDiff: Set < String > ) async throws {
122
+ let apiDigesterPath = try swiftCommandState. getTargetToolchain ( ) . getSwiftAPIDigester ( )
123
+ let apiDigesterTool = SwiftAPIDigester ( fileSystem: swiftCommandState. fileSystem, tool: apiDigesterPath)
124
+
104
125
// Build the current package.
105
126
try await buildSystem. build ( )
106
127
@@ -173,39 +194,180 @@ struct APIDiff: AsyncSwiftCommand {
173
194
}
174
195
}
175
196
176
- private func determineModulesToDiff( packageGraph: ModulesGraph , observabilityScope: ObservabilityScope ) throws -> Set < String > {
197
+ private func runWithIntegratedAPIDigesterSupport( _ swiftCommandState: SwiftCommandState , baselineRevision: Revision , baselineDir: Basics . AbsolutePath , modulesToDiff: Set < String > ) async throws {
198
+ // Build the baseline revision to generate baseline files.
199
+ let modulesWithBaselines = try await generateAPIBaselineUsingIntegratedAPIDigesterSupport ( swiftCommandState, baselineRevision: baselineRevision, baselineDir: baselineDir, modulesNeedingBaselines: modulesToDiff)
200
+
201
+ // Build the package and run a comparison agains the baselines.
202
+ var productsBuildParameters = try swiftCommandState. productsBuildParameters
203
+ productsBuildParameters. apiDigesterMode = . compareToBaselines(
204
+ baselinesDirectory: baselineDir,
205
+ modulesToCompare: modulesWithBaselines,
206
+ breakageAllowListPath: breakageAllowlistPath
207
+ )
208
+ let delegate = DiagnosticsCapturingBuildSystemDelegate ( )
209
+ let buildSystem = try await swiftCommandState. createBuildSystem (
210
+ traitConfiguration: . init( traitOptions: self . traits) ,
211
+ cacheBuildManifest: false ,
212
+ productsBuildParameters: productsBuildParameters,
213
+ delegate: delegate
214
+ )
215
+ try await buildSystem. build ( )
216
+
217
+ // Report the results of the comparison.
218
+ var comparisonResults : [ SwiftAPIDigester . ComparisonResult ] = [ ]
219
+ for (targetName, diagnosticPaths) in delegate. serializedDiagnosticsPathsByTarget {
220
+ guard let targetName, !diagnosticPaths. isEmpty else {
221
+ continue
222
+ }
223
+ var apiBreakingChanges : [ SerializedDiagnostics . Diagnostic ] = [ ]
224
+ var otherDiagnostics : [ SerializedDiagnostics . Diagnostic ] = [ ]
225
+ for path in diagnosticPaths {
226
+ let contents = try swiftCommandState. fileSystem. readFileContents ( path)
227
+ guard contents. count > 0 else {
228
+ continue
229
+ }
230
+ let serializedDiagnostics = try SerializedDiagnostics ( bytes: contents)
231
+ let apiDigesterCategory = " api-digester-breaking-change "
232
+ apiBreakingChanges. append ( contentsOf: serializedDiagnostics. diagnostics. filter { $0. category == apiDigesterCategory } )
233
+ otherDiagnostics. append ( contentsOf: serializedDiagnostics. diagnostics. filter { $0. category != apiDigesterCategory } )
234
+ }
235
+ let result = SwiftAPIDigester . ComparisonResult (
236
+ moduleName: targetName,
237
+ apiBreakingChanges: apiBreakingChanges,
238
+ otherDiagnostics: otherDiagnostics
239
+ )
240
+ comparisonResults. append ( result)
241
+ }
242
+
243
+ var detectedBreakingChange = false
244
+ for result in comparisonResults. sorted ( by: { $0. moduleName < $1. moduleName } ) {
245
+ if result. hasNoAPIBreakingChanges && !modulesToDiff. contains ( result. moduleName) {
246
+ continue
247
+ }
248
+ try printComparisonResult ( result, observabilityScope: swiftCommandState. observabilityScope)
249
+ detectedBreakingChange = detectedBreakingChange || !result. hasNoAPIBreakingChanges
250
+ }
251
+
252
+ for module in modulesToDiff. subtracting ( modulesWithBaselines) {
253
+ print ( " \n Skipping \( module) because it does not exist in the baseline " )
254
+ }
255
+
256
+ if detectedBreakingChange {
257
+ throw ExitCode ( 1 )
258
+ }
259
+ }
260
+
261
+ private func generateAPIBaselineUsingIntegratedAPIDigesterSupport( _ swiftCommandState: SwiftCommandState , baselineRevision: Revision , baselineDir: Basics . AbsolutePath , modulesNeedingBaselines: Set < String > ) async throws -> Set < String > {
262
+ // Setup a temporary directory where we can checkout and build the baseline treeish.
263
+ let baselinePackageRoot = try swiftCommandState. productsBuildParameters. apiDiff. appending ( " \( baselineRevision. identifier) -checkout " )
264
+ if swiftCommandState. fileSystem. exists ( baselinePackageRoot) {
265
+ try swiftCommandState. fileSystem. removeFileTree ( baselinePackageRoot)
266
+ }
267
+ if regenerateBaseline && swiftCommandState. fileSystem. exists ( baselineDir) {
268
+ try swiftCommandState. fileSystem. removeFileTree ( baselineDir)
269
+ }
270
+
271
+ // Clone the current package in a sandbox and checkout the baseline revision.
272
+ let repositoryProvider = GitRepositoryProvider ( )
273
+ let specifier = RepositorySpecifier ( path: baselinePackageRoot)
274
+ let workingCopy = try await repositoryProvider. createWorkingCopy (
275
+ repository: specifier,
276
+ sourcePath: swiftCommandState. getPackageRoot ( ) ,
277
+ at: baselinePackageRoot,
278
+ editable: false
279
+ )
280
+
281
+ try workingCopy. checkout ( revision: baselineRevision)
282
+
283
+ // Create the workspace for this package.
284
+ let workspace = try Workspace (
285
+ forRootPackage: baselinePackageRoot,
286
+ cancellator: swiftCommandState. cancellator
287
+ )
288
+
289
+ let graph = try await workspace. loadPackageGraph (
290
+ rootPath: baselinePackageRoot,
291
+ observabilityScope: swiftCommandState. observabilityScope
292
+ )
293
+
294
+ let baselineModules = try Self . determineModulesToDiff (
295
+ packageGraph: graph,
296
+ productNames: products,
297
+ targetNames: targets,
298
+ observabilityScope: swiftCommandState. observabilityScope,
299
+ diagnoseMissingNames: false
300
+ )
301
+
302
+ // Don't emit a baseline for a module that didn't exist yet in this revision.
303
+ var modulesNeedingBaselines = modulesNeedingBaselines
304
+ modulesNeedingBaselines. formIntersection ( graph. apiDigesterModules)
305
+
306
+ // Abort if we weren't able to load the package graph.
307
+ if swiftCommandState. observabilityScope. errorsReported {
308
+ throw Diagnostics . fatalError
309
+ }
310
+
311
+ // Update the data path input build parameters so it's built in the sandbox.
312
+ var productsBuildParameters = try swiftCommandState. productsBuildParameters
313
+ productsBuildParameters. dataPath = workspace. location. scratchDirectory
314
+ productsBuildParameters. apiDigesterMode = . generateBaselines( baselinesDirectory: baselineDir, modulesRequestingBaselines: modulesNeedingBaselines)
315
+
316
+ // Build the baseline module.
317
+ // FIXME: We need to implement the build tool invocation closure here so that build tool plugins work with the APIDigester. rdar://86112934
318
+ let buildSystem = try await swiftCommandState. createBuildSystem (
319
+ traitConfiguration: . init( ) ,
320
+ cacheBuildManifest: false ,
321
+ productsBuildParameters: productsBuildParameters,
322
+ packageGraphLoader: { graph }
323
+ )
324
+ try await buildSystem. build ( )
325
+ return baselineModules
326
+ }
327
+
328
+ private static func determineModulesToDiff( packageGraph: ModulesGraph , productNames: [ String ] , targetNames: [ String ] , observabilityScope: ObservabilityScope , diagnoseMissingNames: Bool ) throws -> Set < String > {
177
329
var modulesToDiff : Set < String > = [ ]
178
- if products . isEmpty && targets . isEmpty {
330
+ if productNames . isEmpty && targetNames . isEmpty {
179
331
modulesToDiff. formUnion ( packageGraph. apiDigesterModules)
180
332
} else {
181
- for productName in products {
333
+ for productName in productNames {
182
334
guard let product = packageGraph
183
335
. rootPackages
184
336
. flatMap ( \. products)
185
337
. first ( where: { $0. name == productName } ) else {
186
- observabilityScope. emit ( error: " no such product ' \( productName) ' " )
338
+ if diagnoseMissingNames {
339
+ observabilityScope. emit ( error: " no such product ' \( productName) ' " )
340
+ }
187
341
continue
188
342
}
189
343
guard product. type. isLibrary else {
190
- observabilityScope. emit ( error: " ' \( productName) ' is not a library product " )
344
+ if diagnoseMissingNames {
345
+ observabilityScope. emit ( error: " ' \( productName) ' is not a library product " )
346
+ }
191
347
continue
192
348
}
193
349
modulesToDiff. formUnion ( product. modules. filter { $0. underlying is SwiftModule } . map ( \. c99name) )
194
350
}
195
- for targetName in targets {
351
+ for targetName in targetNames {
196
352
guard let target = packageGraph
197
353
. rootPackages
198
354
. flatMap ( \. modules)
199
355
. first ( where: { $0. name == targetName } ) else {
200
- observabilityScope. emit ( error: " no such target ' \( targetName) ' " )
356
+ if diagnoseMissingNames {
357
+ observabilityScope. emit ( error: " no such target ' \( targetName) ' " )
358
+ }
201
359
continue
202
360
}
203
361
guard target. type == . library else {
204
- observabilityScope. emit ( error: " ' \( targetName) ' is not a library target " )
362
+ if diagnoseMissingNames {
363
+ observabilityScope. emit ( error: " ' \( targetName) ' is not a library target " )
364
+ }
205
365
continue
206
366
}
207
367
guard target. underlying is SwiftModule else {
208
- observabilityScope. emit ( error: " ' \( targetName) ' is not a Swift language target " )
368
+ if diagnoseMissingNames {
369
+ observabilityScope. emit ( error: " ' \( targetName) ' is not a Swift language target " )
370
+ }
209
371
continue
210
372
}
211
373
modulesToDiff. insert ( target. c99name)
0 commit comments