@@ -18,8 +18,10 @@ import PackageGraph
1818import PackageModel
1919import SourceControl
2020import SPMBuildCore
21+ import TSCBasic
2122import TSCUtility
2223import _Concurrency
24+ import Workspace
2325
2426struct DeprecatedAPIDiff : ParsableCommand {
2527 static let configuration = CommandConfiguration ( commandName: " experimental-api-diff " ,
@@ -57,7 +59,7 @@ struct APIDiff: AsyncSwiftCommand {
5759 Each ignored breaking change in the file should appear on its own line and contain the exact message \
5860 to be ignored (e.g. 'API breakage: func foo() has been removed').
5961 """ )
60- var breakageAllowlistPath : AbsolutePath ?
62+ var breakageAllowlistPath : Basics . AbsolutePath ?
6163
6264 @Argument ( help: " The baseline treeish to compare to (for example, a commit hash, branch name, tag, and so on). " )
6365 var treeish : String
@@ -75,32 +77,51 @@ struct APIDiff: AsyncSwiftCommand {
7577
7678 @Option ( name: . customLong( " baseline-dir " ) ,
7779 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 ?
7981
8082 @Flag ( help: " Regenerate the API baseline, even if an existing one is available. " )
8183 var regenerateBaseline : Bool = false
8284
8385 func run( _ swiftCommandState: SwiftCommandState ) async throws {
84- let apiDigesterPath = try swiftCommandState. getTargetToolchain ( ) . getSwiftAPIDigester ( )
85- let apiDigesterTool = SwiftAPIDigester ( fileSystem: swiftCommandState. fileSystem, tool: apiDigesterPath)
86-
8786 let packageRoot = try globalOptions. locations. packageDirectory ?? swiftCommandState. getPackageRoot ( )
8887 let repository = GitRepository ( path: packageRoot)
8988 let baselineRevision = try repository. resolveRevision ( identifier: treeish)
9089
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 (
10093 packageGraph: packageGraph,
101- observabilityScope: swiftCommandState. observabilityScope
94+ productNames: products,
95+ targetNames: targets,
96+ observabilityScope: swiftCommandState. observabilityScope,
97+ diagnoseMissingNames: true ,
10298 )
10399
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+
104125 // Build the current package.
105126 try await buildSystem. build ( )
106127
@@ -173,39 +194,180 @@ struct APIDiff: AsyncSwiftCommand {
173194 }
174195 }
175196
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 > {
177329 var modulesToDiff : Set < String > = [ ]
178- if products . isEmpty && targets . isEmpty {
330+ if productNames . isEmpty && targetNames . isEmpty {
179331 modulesToDiff. formUnion ( packageGraph. apiDigesterModules)
180332 } else {
181- for productName in products {
333+ for productName in productNames {
182334 guard let product = packageGraph
183335 . rootPackages
184336 . flatMap ( \. products)
185337 . 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+ }
187341 continue
188342 }
189343 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+ }
191347 continue
192348 }
193349 modulesToDiff. formUnion ( product. modules. filter { $0. underlying is SwiftModule } . map ( \. c99name) )
194350 }
195- for targetName in targets {
351+ for targetName in targetNames {
196352 guard let target = packageGraph
197353 . rootPackages
198354 . flatMap ( \. modules)
199355 . 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+ }
201359 continue
202360 }
203361 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+ }
205365 continue
206366 }
207367 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+ }
209371 continue
210372 }
211373 modulesToDiff. insert ( target. c99name)
0 commit comments