From 7807e68d56f6b9a78e6e8e8bcfff255841c43cbc Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Thu, 10 Jul 2025 11:01:59 -0400 Subject: [PATCH 1/8] Symbol graph support for swiftbuild build system Refactor the build system protocol to make symbol graphs and build plans optional build outputs --- Sources/Build/BuildOperation.swift | 23 ++- .../Commands/PackageCommands/APIDiff.swift | 11 +- .../PackageCommands/DumpCommands.swift | 96 +++++++----- .../Commands/PackageCommands/Install.swift | 4 +- .../Commands/PackageCommands/Migrate.swift | 11 +- .../PackageCommands/PluginCommand.swift | 7 +- .../Commands/Snippets/Cards/SnippetCard.swift | 2 +- Sources/Commands/SwiftBuildCommand.swift | 2 +- Sources/Commands/SwiftRunCommand.swift | 15 +- Sources/Commands/SwiftTestCommand.swift | 2 +- Sources/Commands/Utilities/APIDigester.swift | 8 +- .../Commands/Utilities/PluginDelegate.swift | 145 +++++++++--------- .../BuildSystem/BuildSystem.swift | 36 ++++- Sources/SwiftBuildSupport/PIFBuilder.swift | 2 +- .../PackagePIFProjectBuilder+Modules.swift | 2 + .../SwiftBuildSupport/SwiftBuildSystem.swift | 25 ++- Sources/XCBuildSupport/XcodeBuildSystem.swift | 8 +- Sources/swift-bootstrap/main.swift | 2 +- Tests/CommandsTests/PackageCommandTests.swift | 22 ++- Tests/FunctionalTests/TraitTests.swift | 4 +- 20 files changed, 264 insertions(+), 163 deletions(-) diff --git a/Sources/Build/BuildOperation.swift b/Sources/Build/BuildOperation.swift index 17b63ac880b..dac7dcaf71e 100644 --- a/Sources/Build/BuildOperation.swift +++ b/Sources/Build/BuildOperation.swift @@ -396,9 +396,9 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS } /// Perform a build using the given build description and subset. - public func build(subset: BuildSubset) async throws { + public func build(subset: BuildSubset, buildOutputs: [BuildOutput]) async throws -> BuildOutputResult { guard !self.config.shouldSkipBuilding(for: .target) else { - return + return BuildOutputResult() } let buildStartTime = DispatchTime.now() @@ -422,7 +422,11 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS // any errors up-front. Returns true if we should proceed with the build // or false if not. It will already have thrown any appropriate error. guard try await self.compilePlugins(in: subset) else { - return + if buildOutputs.contains(.buildPlan), let buildPlan = try? self.buildPlan { + return BuildOutputResult(buildPlan: buildPlan) + } else { + return BuildOutputResult() + } } let configuration = self.config.configuration(for: .target) @@ -463,7 +467,12 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS warning: "unable to delete \(oldBuildPath), skip creating symbolic link", underlyingError: error ) - return + + if buildOutputs.contains(.buildPlan), let buildPlan = try? self.buildPlan { + return BuildOutputResult(buildPlan: buildPlan) + } else { + return BuildOutputResult() + } } } @@ -479,6 +488,12 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS underlyingError: error ) } + + if buildOutputs.contains(.buildPlan), let buildPlan = try? self.buildPlan { + return BuildOutputResult(buildPlan: buildPlan) + } else { + return BuildOutputResult() + } } /// Compiles any plugins specified or implied by the build subset, returning diff --git a/Sources/Commands/PackageCommands/APIDiff.swift b/Sources/Commands/PackageCommands/APIDiff.swift index 9b380d90e9d..5bae87ba9fc 100644 --- a/Sources/Commands/PackageCommands/APIDiff.swift +++ b/Sources/Commands/PackageCommands/APIDiff.swift @@ -123,14 +123,17 @@ struct APIDiff: AsyncSwiftCommand { let apiDigesterTool = SwiftAPIDigester(fileSystem: swiftCommandState.fileSystem, tool: apiDigesterPath) // Build the current package. - try await buildSystem.build() + let buildOutputResult = try await buildSystem.build(subset: .allExcludingTests, buildOutputs: [.buildPlan]) + guard let buildPlan = buildOutputResult.buildPlan else { + throw ExitCode.failure + } // Dump JSON for the baseline package. let baselineDumper = try APIDigesterBaselineDumper( baselineRevision: baselineRevision, packageRoot: swiftCommandState.getPackageRoot(), - productsBuildParameters: try buildSystem.buildPlan.destinationBuildParameters, - toolsBuildParameters: try buildSystem.buildPlan.toolsBuildParameters, + productsBuildParameters: buildPlan.destinationBuildParameters, + toolsBuildParameters: buildPlan.toolsBuildParameters, apiDigesterTool: apiDigesterTool, observabilityScope: swiftCommandState.observabilityScope ) @@ -159,7 +162,7 @@ struct APIDiff: AsyncSwiftCommand { if let comparisonResult = try apiDigesterTool.compareAPIToBaseline( at: moduleBaselinePath, for: module, - buildPlan: try buildSystem.buildPlan, + buildPlan: buildPlan, except: breakageAllowlistPath ) { return comparisonResult diff --git a/Sources/Commands/PackageCommands/DumpCommands.swift b/Sources/Commands/PackageCommands/DumpCommands.swift index e5ccf3873fe..ac1e7cc7f82 100644 --- a/Sources/Commands/PackageCommands/DumpCommands.swift +++ b/Sources/Commands/PackageCommands/DumpCommands.swift @@ -52,54 +52,76 @@ struct DumpSymbolGraph: AsyncSwiftCommand { // // We turn build manifest caching off because we need the build plan. let buildSystem = try await swiftCommandState.createBuildSystem( - explicitBuildSystem: .native, // We are enabling all traits for dumping the symbol graph. traitConfiguration: .init(enableAllTraits: true), cacheBuildManifest: false ) - try await buildSystem.build() + let buildResult = try await buildSystem.build(subset: .allExcludingTests, buildOutputs: [.symbolGraph, .buildPlan]) - // Configure the symbol graph extractor. - let symbolGraphExtractor = try SymbolGraphExtract( - fileSystem: swiftCommandState.fileSystem, - tool: swiftCommandState.getTargetToolchain().getSymbolGraphExtract(), - observabilityScope: swiftCommandState.observabilityScope, - skipSynthesizedMembers: skipSynthesizedMembers, - minimumAccessLevel: minimumAccessLevel, - skipInheritedDocs: skipInheritedDocs, - includeSPISymbols: includeSPISymbols, - emitExtensionBlockSymbols: extensionBlockSymbolBehavior == .emitExtensionBlockSymbols, - outputFormat: .json(pretty: prettyPrint) - ) + let symbolGraphDirectory = try swiftCommandState.productsBuildParameters.dataPath.appending("symbolgraph") - // Run the tool once for every library and executable target in the root package. - let buildPlan = try buildSystem.buildPlan - let modulesGraph = try await buildSystem.getPackageGraph() - let symbolGraphDirectory = buildPlan.destinationBuildParameters.dataPath.appending("symbolgraph") - for description in buildPlan.buildModules { - guard description.module.type == .library, - modulesGraph.rootPackages[description.package.id] != nil - else { - continue - } + let fs = swiftCommandState.fileSystem + + try? fs.removeFileTree(symbolGraphDirectory) + try fs.createDirectory(symbolGraphDirectory, recursive: true) - print("-- Emitting symbol graph for", description.module.name) - let result = try symbolGraphExtractor.extractSymbolGraph( - for: description, - outputRedirection: .collect(redirectStderr: true), - outputDirectory: symbolGraphDirectory, - verboseOutput: swiftCommandState.logLevel <= .info + if buildResult.symbolGraph { + // The build system produced symbol graphs for us, one for each target. + let buildPath = try swiftCommandState.productsBuildParameters.buildPath + + // Copy the symbol graphs from the target-specific locations to the single output directory + for rootPackage in try await buildSystem.getPackageGraph().rootPackages { + for module in rootPackage.modules { + if case let sgDir = buildPath.appending(components: "\(try swiftCommandState.productsBuildParameters.triple.archName)", "\(module.name).symbolgraphs"), fs.exists(sgDir) { + for sgFile in try fs.getDirectoryContents(sgDir) { + try fs.copy(from: sgDir.appending(components: sgFile), to: symbolGraphDirectory.appending(sgFile)) + } + } + } + } + } else if let buildPlan = buildResult.buildPlan { + // Otherwise, with a build plan we can run the symbol graph extractor tool on the built targets. + let symbolGraphExtractor = try SymbolGraphExtract( + fileSystem: swiftCommandState.fileSystem, + tool: swiftCommandState.getTargetToolchain().getSymbolGraphExtract(), + observabilityScope: swiftCommandState.observabilityScope, + skipSynthesizedMembers: skipSynthesizedMembers, + minimumAccessLevel: minimumAccessLevel, + skipInheritedDocs: skipInheritedDocs, + includeSPISymbols: includeSPISymbols, + emitExtensionBlockSymbols: extensionBlockSymbolBehavior == .emitExtensionBlockSymbols, + outputFormat: .json(pretty: prettyPrint) ) - if result.exitStatus != .terminated(code: 0) { - let commandline = "\nUsing commandline: \(result.arguments)" - switch result.output { - case .success(let value): - swiftCommandState.observabilityScope.emit(error: "Failed to emit symbol graph for '\(description.module.c99name)': \(String(decoding: value, as: UTF8.self))\(commandline)") - case .failure(let error): - swiftCommandState.observabilityScope.emit(error: "Internal error while emitting symbol graph for '\(description.module.c99name)': \(error)\(commandline)") + // Run the tool once for every library and executable target in the root package. + let modulesGraph = try await buildSystem.getPackageGraph() + for description in buildPlan.buildModules { + guard description.module.type == .library, + modulesGraph.rootPackages[description.package.id] != nil + else { + continue + } + + print("-- Emitting symbol graph for", description.module.name) + let result = try symbolGraphExtractor.extractSymbolGraph( + for: description, + outputRedirection: .collect(redirectStderr: true), + outputDirectory: symbolGraphDirectory, + verboseOutput: swiftCommandState.logLevel <= .info + ) + + if result.exitStatus != .terminated(code: 0) { + let commandline = "\nUsing commandline: \(result.arguments)" + switch result.output { + case .success(let value): + swiftCommandState.observabilityScope.emit(error: "Failed to emit symbol graph for '\(description.module.c99name)': \(String(decoding: value, as: UTF8.self))\(commandline)") + case .failure(let error): + swiftCommandState.observabilityScope.emit(error: "Internal error while emitting symbol graph for '\(description.module.c99name)': \(error)\(commandline)") + } } } + } else { + throw InternalError("Build system \(buildSystem) cannot produce a symbol graph.") } print("Files written to", symbolGraphDirectory.pathString) diff --git a/Sources/Commands/PackageCommands/Install.swift b/Sources/Commands/PackageCommands/Install.swift index 9572c402559..06c9a66f41a 100644 --- a/Sources/Commands/PackageCommands/Install.swift +++ b/Sources/Commands/PackageCommands/Install.swift @@ -88,8 +88,8 @@ extension SwiftPackageCommand { commandState.preferredBuildConfiguration = .release } - try await commandState.createBuildSystem(explicitProduct: productToInstall.name, traitConfiguration: .init()) - .build(subset: .product(productToInstall.name)) + _ = try await commandState.createBuildSystem(explicitProduct: productToInstall.name, traitConfiguration: .init()) + .build(subset: .product(productToInstall.name), buildOutputs: []) let binPath = try commandState.productsBuildParameters.buildPath.appending(component: productToInstall.name) let finalBinPath = swiftpmBinDir.appending(component: binPath.basename) diff --git a/Sources/Commands/PackageCommands/Migrate.swift b/Sources/Commands/PackageCommands/Migrate.swift index 95098638934..2c3cc5d46cf 100644 --- a/Sources/Commands/PackageCommands/Migrate.swift +++ b/Sources/Commands/PackageCommands/Migrate.swift @@ -102,16 +102,21 @@ extension SwiftPackageCommand { // whole project to get diagnostic files. print("> Starting the build") + var buildResult: BuildOutputResult? if !targets.isEmpty { for target in targets { - try await buildSystem.build(subset: .target(target)) + buildResult = try await buildSystem.build(subset: .target(target), buildOutputs: [.buildPlan]) } } else { - try await buildSystem.build(subset: .allIncludingTests) + buildResult = try await buildSystem.build(subset: .allIncludingTests, buildOutputs: [.buildPlan]) } // Determine all of the targets we need up update. - let buildPlan = try buildSystem.buildPlan + let buildPlan = buildResult!.buildPlan + + guard let buildPlan else { + throw ExitCode.failure + } var modules: [any ModuleBuildDescription] = [] if !targets.isEmpty { diff --git a/Sources/Commands/PackageCommands/PluginCommand.swift b/Sources/Commands/PackageCommands/PluginCommand.swift index ec2243a6a71..40a923de710 100644 --- a/Sources/Commands/PackageCommands/PluginCommand.swift +++ b/Sources/Commands/PackageCommands/PluginCommand.swift @@ -351,11 +351,10 @@ struct PluginCommand: AsyncSwiftCommand { for: try pluginScriptRunner.hostTriple ) { name, path in // Build the product referenced by the tool, and add the executable to the tool map. Product dependencies are not supported within a package, so if the tool happens to be from the same package, we instead find the executable that corresponds to the product. There is always one, because of autogeneration of implicit executables with the same name as the target if there isn't an explicit one. - try await buildSystem.build(subset: .product(name, for: .host)) + let buildResult = try await buildSystem.build(subset: .product(name, for: .host), buildOutputs: [.buildPlan]) - // TODO determine if there is a common way to calculate the build tool binary path that doesn't depend on the build system. - if buildSystemKind == .native { - if let builtTool = try buildSystem.buildPlan.buildProducts.first(where: { + if let buildPlan = buildResult.buildPlan { + if let builtTool = buildPlan.buildProducts.first(where: { $0.product.name == name && $0.buildParameters.destination == .host }) { return try builtTool.binaryPath diff --git a/Sources/Commands/Snippets/Cards/SnippetCard.swift b/Sources/Commands/Snippets/Cards/SnippetCard.swift index d54b916ccf3..327df62ca39 100644 --- a/Sources/Commands/Snippets/Cards/SnippetCard.swift +++ b/Sources/Commands/Snippets/Cards/SnippetCard.swift @@ -113,7 +113,7 @@ struct SnippetCard: Card { func runExample() async throws { print("Building '\(snippet.path)'\n") let buildSystem = try await swiftCommandState.createBuildSystem(explicitProduct: snippet.name, traitConfiguration: .init()) - try await buildSystem.build(subset: .product(snippet.name)) + _ = try await buildSystem.build(subset: .product(snippet.name), buildOutputs: []) let executablePath = try swiftCommandState.productsBuildParameters.buildPath.appending(component: snippet.name) if let exampleTarget = try await buildSystem.getPackageGraph().module(for: snippet.name) { try ProcessEnv.chdir(exampleTarget.sources.paths[0].parentDirectory) diff --git a/Sources/Commands/SwiftBuildCommand.swift b/Sources/Commands/SwiftBuildCommand.swift index 8bc8e1aaccb..7de511f5d92 100644 --- a/Sources/Commands/SwiftBuildCommand.swift +++ b/Sources/Commands/SwiftBuildCommand.swift @@ -203,7 +203,7 @@ public struct SwiftBuildCommand: AsyncSwiftCommand { outputStream: TSCBasic.stdoutStream ) do { - try await buildSystem.build(subset: subset) + _ = try await buildSystem.build(subset: subset, buildOutputs: []) } catch _ as Diagnostics { throw ExitCode.failure } diff --git a/Sources/Commands/SwiftRunCommand.swift b/Sources/Commands/SwiftRunCommand.swift index ac08161af22..77bdf23f11c 100644 --- a/Sources/Commands/SwiftRunCommand.swift +++ b/Sources/Commands/SwiftRunCommand.swift @@ -145,10 +145,13 @@ public struct SwiftRunCommand: AsyncSwiftCommand { ) // Perform build. - try await buildSystem.build() + let buildResult = try await buildSystem.build(subset: .allExcludingTests, buildOutputs: [.buildPlan]) + guard let buildPlan = buildResult.buildPlan else { + throw ExitCode.failure + } // Execute the REPL. - let arguments = try buildSystem.buildPlan.createREPLArguments() + let arguments = try buildPlan.createREPLArguments() print("Launching Swift REPL with arguments: \(arguments.joined(separator: " "))") try self.run( fileSystem: swiftCommandState.fileSystem, @@ -165,9 +168,9 @@ public struct SwiftRunCommand: AsyncSwiftCommand { ) let productName = try await findProductName(in: buildSystem.getPackageGraph()) if options.shouldBuildTests { - try await buildSystem.build(subset: .allIncludingTests) + _ = try await buildSystem.build(subset: .allIncludingTests, buildOutputs: []) } else if options.shouldBuild { - try await buildSystem.build(subset: .product(productName)) + _ = try await buildSystem.build(subset: .product(productName), buildOutputs: []) } let productRelativePath = try swiftCommandState.productsBuildParameters.executablePath(for: productName) @@ -221,9 +224,9 @@ public struct SwiftRunCommand: AsyncSwiftCommand { ) let productName = try await findProductName(in: buildSystem.getPackageGraph()) if options.shouldBuildTests { - try await buildSystem.build(subset: .allIncludingTests) + _ = try await buildSystem.build(subset: .allIncludingTests, buildOutputs: []) } else if options.shouldBuild { - try await buildSystem.build(subset: .product(productName)) + _ = try await buildSystem.build(subset: .product(productName), buildOutputs: []) } let executablePath = try swiftCommandState.productsBuildParameters.buildPath.appending(component: productName) diff --git a/Sources/Commands/SwiftTestCommand.swift b/Sources/Commands/SwiftTestCommand.swift index a5b1e2cd919..e498fa5aa30 100644 --- a/Sources/Commands/SwiftTestCommand.swift +++ b/Sources/Commands/SwiftTestCommand.swift @@ -1572,7 +1572,7 @@ private func buildTestsIfNeeded( .allIncludingTests } - try await buildSystem.build(subset: subset) + _ = try await buildSystem.build(subset: subset, buildOutputs: []) // Find the test product. let testProducts = await buildSystem.builtTestProducts diff --git a/Sources/Commands/Utilities/APIDigester.swift b/Sources/Commands/Utilities/APIDigester.swift index be402306492..ef5bb50bdce 100644 --- a/Sources/Commands/Utilities/APIDigester.swift +++ b/Sources/Commands/Utilities/APIDigester.swift @@ -146,7 +146,11 @@ struct APIDigesterBaselineDumper { toolsBuildParameters: toolsBuildParameters, packageGraphLoader: { graph } ) - try await buildSystem.build() + let buildResult = try await buildSystem.build(subset: .allExcludingTests, buildOutputs: [.buildPlan]) + + guard let buildPlan = buildResult.buildPlan else { + throw Diagnostics.fatalError + } // Dump the SDK JSON. try swiftCommandState.fileSystem.createDirectory(baselineDir, recursive: true) @@ -158,7 +162,7 @@ struct APIDigesterBaselineDumper { try apiDigesterTool.emitAPIBaseline( to: baselinePath(module), for: module, - buildPlan: buildSystem.buildPlan + buildPlan: buildPlan ) return nil } catch { diff --git a/Sources/Commands/Utilities/PluginDelegate.swift b/Sources/Commands/Utilities/PluginDelegate.swift index 98464b43227..06778015cda 100644 --- a/Sources/Commands/Utilities/PluginDelegate.swift +++ b/Sources/Commands/Utilities/PluginDelegate.swift @@ -249,7 +249,7 @@ final class PluginDelegate: PluginInvocationDelegate { traitConfiguration: .init(), toolsBuildParameters: toolsBuildParameters ) - try await buildSystem.build(subset: .allIncludingTests) + _ = try await buildSystem.build(subset: .allIncludingTests, buildOutputs: []) // Clean out the code coverage directory that may contain stale `profraw` files from a previous run of // the code coverage tool. @@ -414,85 +414,92 @@ final class PluginDelegate: PluginInvocationDelegate { cacheBuildManifest: false ) - func lookupDescription( - for moduleName: String, - destination: BuildParameters.Destination - ) throws -> ModuleBuildDescription? { - try buildSystem.buildPlan.buildModules.first { - $0.module.name == moduleName && $0.buildParameters.destination == destination + // Build the target, if needed. We are interested in symbol graph (ideally) or a build plan. + let buildResult = try await buildSystem.build(subset: .target(targetName), buildOutputs: [.symbolGraph, .buildPlan]) + + if buildResult.symbolGraph { + let path = (try swiftCommandState.productsBuildParameters.buildPath) + return PluginInvocationSymbolGraphResult(directoryPath: "\(path)/\(try swiftCommandState.productsBuildParameters.triple.archName)/\(targetName).symbolgraphs") + } else if let buildPlan = buildResult.buildPlan { + func lookupDescription( + for moduleName: String, + destination: BuildParameters.Destination + ) throws -> ModuleBuildDescription? { + try buildPlan.buildModules.first { + $0.module.name == moduleName && $0.buildParameters.destination == destination + } } - } - // Build the target, if needed. This would also create a build plan. - try await buildSystem.build(subset: .target(targetName)) - - // FIXME: The name alone doesn't give us enough information to figure out what - // the destination is, this logic prefers "target" over "host" because that's - // historically how this was setup. Ideally we should be building for both "host" - // and "target" if module is configured for them but that would require changing - // `PluginInvocationSymbolGraphResult` to carry multiple directories. - let description = if let targetDescription = try lookupDescription(for: targetName, destination: .target) { - targetDescription - } else if let hostDescription = try lookupDescription(for: targetName, destination: .host) { - hostDescription - } else { - throw InternalError("could not find a target named: \(targetName)") - } + // FIXME: The name alone doesn't give us enough information to figure out what + // the destination is, this logic prefers "target" over "host" because that's + // historically how this was setup. Ideally we should be building for both "host" + // and "target" if module is configured for them but that would require changing + // `PluginInvocationSymbolGraphResult` to carry multiple directories. + let description = if let targetDescription = try lookupDescription(for: targetName, destination: .target) { + targetDescription + } else if let hostDescription = try lookupDescription(for: targetName, destination: .host) { + hostDescription + } else { + throw InternalError("could not find a target named: \(targetName)") + } - // Configure the symbol graph extractor. - var symbolGraphExtractor = try SymbolGraphExtract( - fileSystem: swiftCommandState.fileSystem, - tool: swiftCommandState.getTargetToolchain().getSymbolGraphExtract(), - observabilityScope: swiftCommandState.observabilityScope - ) - symbolGraphExtractor.skipSynthesizedMembers = !options.includeSynthesized - switch options.minimumAccessLevel { - case .private: - symbolGraphExtractor.minimumAccessLevel = .private - case .fileprivate: - symbolGraphExtractor.minimumAccessLevel = .fileprivate - case .internal: - symbolGraphExtractor.minimumAccessLevel = .internal - case .package: - symbolGraphExtractor.minimumAccessLevel = .package - case .public: - symbolGraphExtractor.minimumAccessLevel = .public - case .open: - symbolGraphExtractor.minimumAccessLevel = .open - } - symbolGraphExtractor.skipInheritedDocs = true - symbolGraphExtractor.includeSPISymbols = options.includeSPI - symbolGraphExtractor.emitExtensionBlockSymbols = options.emitExtensionBlocks - - // Determine the output directory, and remove any old version if it already exists. - let outputDir = description.buildParameters.dataPath.appending( - components: "extracted-symbols", - description.package.identity.description, - targetName - ) - try swiftCommandState.fileSystem.removeFileTree(outputDir) - - // Run the symbol graph extractor on the target. - let result = try symbolGraphExtractor.extractSymbolGraph( - for: description, - outputRedirection: .collect, - outputDirectory: outputDir, - verboseOutput: self.swiftCommandState.logLevel <= .info - ) + // Configure the symbol graph extractor. + var symbolGraphExtractor = try SymbolGraphExtract( + fileSystem: swiftCommandState.fileSystem, + tool: swiftCommandState.getTargetToolchain().getSymbolGraphExtract(), + observabilityScope: swiftCommandState.observabilityScope + ) + symbolGraphExtractor.skipSynthesizedMembers = !options.includeSynthesized + switch options.minimumAccessLevel { + case .private: + symbolGraphExtractor.minimumAccessLevel = .private + case .fileprivate: + symbolGraphExtractor.minimumAccessLevel = .fileprivate + case .internal: + symbolGraphExtractor.minimumAccessLevel = .internal + case .package: + symbolGraphExtractor.minimumAccessLevel = .package + case .public: + symbolGraphExtractor.minimumAccessLevel = .public + case .open: + symbolGraphExtractor.minimumAccessLevel = .open + } + symbolGraphExtractor.skipInheritedDocs = true + symbolGraphExtractor.includeSPISymbols = options.includeSPI + symbolGraphExtractor.emitExtensionBlockSymbols = options.emitExtensionBlocks + + // Determine the output directory, and remove any old version if it already exists. + let outputDir = description.buildParameters.dataPath.appending( + components: "extracted-symbols", + description.package.identity.description, + targetName + ) + try swiftCommandState.fileSystem.removeFileTree(outputDir) + + // Run the symbol graph extractor on the target. + let result = try symbolGraphExtractor.extractSymbolGraph( + for: description, + outputRedirection: .collect, + outputDirectory: outputDir, + verboseOutput: self.swiftCommandState.logLevel <= .info + ) - guard result.exitStatus == .terminated(code: 0) else { - throw AsyncProcessResult.Error.nonZeroExit(result) - } + guard result.exitStatus == .terminated(code: 0) else { + throw AsyncProcessResult.Error.nonZeroExit(result) + } - // Return the results to the plugin. - return PluginInvocationSymbolGraphResult(directoryPath: outputDir.pathString) + // Return the results to the plugin. + return PluginInvocationSymbolGraphResult(directoryPath: outputDir.pathString) + } else { + throw InternalError("Build system \(buildSystem) doesn't have plugin support.") + } } } extension BuildSystem { fileprivate func buildIgnoringError(subset: BuildSubset) async -> Bool { do { - try await self.build(subset: subset) + _ = try await self.build(subset: subset, buildOutputs: []) return true } catch { return false diff --git a/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift b/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift index 9f29a66fdbc..4c4c06f5425 100644 --- a/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift +++ b/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift @@ -34,6 +34,29 @@ public enum BuildSubset { case target(String, for: BuildParameters.Destination? = .none) } +/// Represents possible extra build outputs for a build. Some build systems +/// can produce certain extra outputs in the process of building. Not all +/// build systems can produce all possible build outputs. The build output +/// results will contain equivalent results only if the build system was capable +/// of producing that extra output. +public enum BuildOutput { + case symbolGraph + case buildPlan +} + +/// Represents extra build outputs result that were requested with equivalent build +/// outputs. This can signal to the caller that the output was produced during +/// the process of the build, and provide any relevant details of the output. +public struct BuildOutputResult { + public var symbolGraph: Bool + public var buildPlan: BuildPlan? + + public init(symbolGraph: Bool = false, buildPlan: BuildPlan? = nil) { + self.symbolGraph = symbolGraph + self.buildPlan = buildPlan + } +} + /// A protocol that represents a build system used by SwiftPM for all build operations. This allows factoring out the /// implementation details between SwiftPM's `BuildOperation` and the Swift Build backed `SwiftBuildSystem`. public protocol BuildSystem: Cancellable { @@ -47,20 +70,17 @@ public protocol BuildSystem: Cancellable { /// Returns the package graph used by the build system. func getPackageGraph() async throws -> ModulesGraph - /// Builds a subset of the package graph. - /// - Parameters: - /// - subset: The subset of the package graph to build. - func build(subset: BuildSubset) async throws - - var buildPlan: BuildPlan { get throws } + /// - buildOutputs: Additional build outputs requested from the build system. + /// - Returns: A build output result with details about requested additional build outputs. + func build(subset: BuildSubset, buildOutputs: [BuildOutput]) async throws -> BuildOutputResult var hasIntegratedAPIDigesterSupport: Bool { get } } extension BuildSystem { - /// Builds the default subset: all targets excluding tests. + /// Builds the default subset: all targets excluding tests with no extra build outputs. public func build() async throws { - try await build(subset: .allExcludingTests) + _ = try await build(subset: .allExcludingTests, buildOutputs: []) } } diff --git a/Sources/SwiftBuildSupport/PIFBuilder.swift b/Sources/SwiftBuildSupport/PIFBuilder.swift index dc91af90b1f..2cd3870f3f3 100644 --- a/Sources/SwiftBuildSupport/PIFBuilder.swift +++ b/Sources/SwiftBuildSupport/PIFBuilder.swift @@ -167,7 +167,7 @@ public final class PIFBuilder { prettyPrint: Bool = true, preservePIFModelStructure: Bool = false, printPIFManifestGraphviz: Bool = false, - buildParameters: BuildParameters + buildParameters: BuildParameters, ) async throws -> String { let encoder = prettyPrint ? JSONEncoder.makeWithDefaults() : JSONEncoder() diff --git a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift index c8c4751ba8b..d750cb9695d 100644 --- a/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift +++ b/Sources/SwiftBuildSupport/PackagePIFProjectBuilder+Modules.swift @@ -730,6 +730,8 @@ extension PackagePIFProjectBuilder { // Custom source module build settings, if any. pifBuilder.delegate.configureSourceModuleBuildSettings(sourceModule: sourceModule, settings: &settings) + settings[.SYMBOL_GRAPH_EXTRACTOR_OUTPUT_DIR] = "$(TARGET_BUILD_DIR)/$(CURRENT_ARCH)/\(sourceModule.name).symbolgraphs" + // Until this point the build settings for the target have been the same between debug and release // configurations. // The custom manifest settings might cause them to diverge. diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index aa36f988fd6..8a6b2f4c28c 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -279,17 +279,23 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { SwiftLanguageVersion.supportedSwiftLanguageVersions } - public func build(subset: BuildSubset) async throws { + public func build(subset: BuildSubset, buildOutputs: [BuildOutput]) async throws -> BuildOutputResult { guard !buildParameters.shouldSkipBuilding else { - return + return BuildOutputResult() } try await writePIF(buildParameters: buildParameters) - try await startSWBuildOperation(pifTargetName: subset.pifTargetName) + try await startSWBuildOperation(pifTargetName: subset.pifTargetName, genSymbolGraph: buildOutputs.contains(.symbolGraph)) + + if buildOutputs.contains(.symbolGraph) { + return BuildOutputResult(symbolGraph: true) + } + + return BuildOutputResult() } - private func startSWBuildOperation(pifTargetName: String) async throws { + private func startSWBuildOperation(pifTargetName: String, genSymbolGraph: Bool) async throws { let buildStartTime = ContinuousClock.Instant.now try await withService(connectionMode: .inProcessStatic(swiftbuildServiceEntryPoint)) { service in @@ -339,7 +345,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { throw error } - let request = try self.makeBuildRequest(configuredTargets: configuredTargets, derivedDataPath: derivedDataPath) + let request = try self.makeBuildRequest(configuredTargets: configuredTargets, derivedDataPath: derivedDataPath, genSymbolGraph: genSymbolGraph) struct BuildState { private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] @@ -539,7 +545,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { ) } - private func makeBuildParameters() throws -> SwiftBuild.SWBBuildParameters { + private func makeBuildParameters(genSymbolGraph: Bool) throws -> SwiftBuild.SWBBuildParameters { // Generate the run destination parameters. let runDestination = makeRunDestination() @@ -557,6 +563,9 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { settings["SWIFT_EXEC"] = buildParameters.toolchain.swiftCompilerPath.pathString // FIXME: workaround for old Xcode installations such as what is in CI settings["LM_SKIP_METADATA_EXTRACTION"] = "YES" + if genSymbolGraph { + settings["RUN_SYMBOL_GRAPH_EXTRACT"] = "YES" + } let normalizedTriple = Triple(buildParameters.triple.triple, normalizing: true) if let deploymentTargetSettingName = normalizedTriple.deploymentTargetSettingName { @@ -621,9 +630,9 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { return params } - public func makeBuildRequest(configuredTargets: [SWBTargetGUID], derivedDataPath: Basics.AbsolutePath) throws -> SWBBuildRequest { + public func makeBuildRequest(configuredTargets: [SWBTargetGUID], derivedDataPath: Basics.AbsolutePath, genSymbolGraph: Bool) throws -> SWBBuildRequest { var request = SWBBuildRequest() - request.parameters = try makeBuildParameters() + request.parameters = try makeBuildParameters(genSymbolGraph: genSymbolGraph) request.configuredTargets = configuredTargets.map { SWBConfiguredTarget(guid: $0.rawValue, parameters: request.parameters) } request.useParallelTargets = true request.useImplicitDependencies = false diff --git a/Sources/XCBuildSupport/XcodeBuildSystem.swift b/Sources/XCBuildSupport/XcodeBuildSystem.swift index 64b5adf9623..796c9d2424a 100644 --- a/Sources/XCBuildSupport/XcodeBuildSystem.swift +++ b/Sources/XCBuildSupport/XcodeBuildSystem.swift @@ -160,9 +160,9 @@ public final class XcodeBuildSystem: SPMBuildCore.BuildSystem { return [] } - public func build(subset: BuildSubset) async throws { + public func build(subset: BuildSubset, buildOutputs: [BuildOutput]) async throws -> BuildOutputResult { guard !buildParameters.shouldSkipBuilding else { - return + return BuildOutputResult() } let pifBuilder = try await getPIFBuilder() @@ -244,9 +244,11 @@ public final class XcodeBuildSystem: SPMBuildCore.BuildSystem { throw Diagnostics.fatalError } - guard !self.logLevel.isQuiet else { return } + guard !self.logLevel.isQuiet else { return BuildOutputResult()} self.outputStream.send("Build complete!\n") self.outputStream.flush() + + return BuildOutputResult() } func createBuildParametersFile() throws -> AbsolutePath { diff --git a/Sources/swift-bootstrap/main.swift b/Sources/swift-bootstrap/main.swift index 0911ddee21c..ac682c02039 100644 --- a/Sources/swift-bootstrap/main.swift +++ b/Sources/swift-bootstrap/main.swift @@ -266,7 +266,7 @@ struct SwiftBootstrapBuildTool: AsyncParsableCommand { shouldDisableLocalRpath: shouldDisableLocalRpath, logLevel: logLevel ) - try await buildSystem.build(subset: .allExcludingTests) + _ = try await buildSystem.build(subset: .allExcludingTests, buildOutputs: []) } func createBuildSystem( diff --git a/Tests/CommandsTests/PackageCommandTests.swift b/Tests/CommandsTests/PackageCommandTests.swift index 9732f2cc554..a75af7ba1f6 100644 --- a/Tests/CommandsTests/PackageCommandTests.swift +++ b/Tests/CommandsTests/PackageCommandTests.swift @@ -3343,16 +3343,25 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { // Check that if we don't pass any target, we successfully get symbol graph information for all targets in the package, and at different paths. do { let (stdout, _) = try await self.execute(["generate-documentation"], packagePath: packageDir) - XCTAssertMatch(stdout, .and(.contains("MyLibrary:"), .contains(AbsolutePath("/mypackage/MyLibrary").pathString))) - XCTAssertMatch(stdout, .and(.contains("MyCommand:"), .contains(AbsolutePath("/mypackage/MyCommand").pathString))) - + if buildSystemProvider == .native { + XCTAssertMatch(stdout, .and(.contains("MyLibrary:"), .contains(AbsolutePath("/mypackage/MyLibrary").pathString))) + XCTAssertMatch(stdout, .and(.contains("MyCommand:"), .contains(AbsolutePath("/mypackage/MyCommand").pathString))) + } else if buildSystemProvider == .swiftbuild { + XCTAssertMatch(stdout, .and(.contains("MyLibrary:"), .contains(AbsolutePath("/MyLibrary.symbolgraphs").pathString))) + XCTAssertMatch(stdout, .and(.contains("MyCommand:"), .contains(AbsolutePath("/MyCommand.symbolgraphs").pathString))) + } } // Check that if we pass a target, we successfully get symbol graph information for just the target we asked for. do { let (stdout, _) = try await self.execute(["generate-documentation", "--target", "MyLibrary"], packagePath: packageDir) - XCTAssertMatch(stdout, .and(.contains("MyLibrary:"), .contains(AbsolutePath("/mypackage/MyLibrary").pathString))) - XCTAssertNoMatch(stdout, .and(.contains("MyCommand:"), .contains(AbsolutePath("/mypackage/MyCommand").pathString))) + if buildSystemProvider == .native { + XCTAssertMatch(stdout, .and(.contains("MyLibrary:"), .contains(AbsolutePath("/mypackage/MyLibrary").pathString))) + XCTAssertNoMatch(stdout, .and(.contains("MyCommand:"), .contains(AbsolutePath("/mypackage/MyCommand").pathString))) + } else if buildSystemProvider == .swiftbuild { + XCTAssertMatch(stdout, .and(.contains("MyLibrary:"), .contains(AbsolutePath("/MyLibrary.symbolgraphs").pathString))) + XCTAssertNoMatch(stdout, .and(.contains("MyCommand:"), .contains(AbsolutePath("/MyCommand.symbolgraphs").pathString))) + } } } } @@ -4119,7 +4128,8 @@ class PackageCommandSwiftBuildTests: PackageCommandTestCase { } override func testCommandPluginSymbolGraphCallbacks() async throws { - throw XCTSkip("SWBINTTODO: Symbol graph extraction does not yet work with swiftbuild build system") + try XCTSkipOnWindows(because: "TSCBasic/Path.swift:969: Assertion failed, https://github.com/swiftlang/swift-package-manager/issues/8602") + try await super.testCommandPluginSymbolGraphCallbacks() } override func testCommandPluginBuildingCallbacks() async throws { diff --git a/Tests/FunctionalTests/TraitTests.swift b/Tests/FunctionalTests/TraitTests.swift index eb1fe7e5940..5b65e2e51f6 100644 --- a/Tests/FunctionalTests/TraitTests.swift +++ b/Tests/FunctionalTests/TraitTests.swift @@ -595,7 +595,7 @@ struct TraitTests { // The swiftbuild build system doesn't yet have the ability for command plugins to request symbol graphs try await withKnownIssue( "https://github.com/swiftlang/swift-build/issues/609", - isIntermittent: (ProcessInfo.hostOperatingSystem == .windows), + isIntermittent: true, ) { let (stdout, _) = try await executeSwiftPackage( fixturePath.appending("Package10"), @@ -608,7 +608,7 @@ struct TraitTests { #expect(symbolGraph.contains("TypeGatedByPackage10Trait1")) #expect(symbolGraph.contains("TypeGatedByPackage10Trait2")) } when: { - buildSystem == .swiftbuild + buildSystem == .swiftbuild && ProcessInfo.hostOperatingSystem == .windows } } } From 6ac1fbc9b81eb5ec6f2cb39a9274f639a051c895 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 11 Jul 2025 10:06:29 -0400 Subject: [PATCH 2/8] Tidy up build result construction --- Sources/Build/BuildOperation.swift | 32 ++++++++----------- .../Commands/PackageCommands/APIDiff.swift | 4 +-- .../Commands/Snippets/Cards/SnippetCard.swift | 2 +- Sources/Commands/SwiftBuildCommand.swift | 2 +- Sources/Commands/SwiftRunCommand.swift | 8 ++--- Sources/Commands/SwiftTestCommand.swift | 2 +- .../Commands/Utilities/PluginDelegate.swift | 2 +- .../BuildSystem/BuildSystem.swift | 3 ++ Sources/SwiftBuildSupport/PIFBuilder.swift | 2 +- Sources/XCBuildSupport/XcodeBuildSystem.swift | 8 +++-- Sources/swift-bootstrap/main.swift | 2 +- 11 files changed, 33 insertions(+), 34 deletions(-) diff --git a/Sources/Build/BuildOperation.swift b/Sources/Build/BuildOperation.swift index 3d991a234f0..2b341cc1c2d 100644 --- a/Sources/Build/BuildOperation.swift +++ b/Sources/Build/BuildOperation.swift @@ -397,8 +397,10 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS /// Perform a build using the given build description and subset. public func build(subset: BuildSubset, buildOutputs: [BuildOutput]) async throws -> BuildResult { + var result = BuildResult(serializedDiagnosticPathsByTargetName: .failure(StringError("Building was skipped"))) + guard !self.config.shouldSkipBuilding(for: .target) else { - return BuildResult(serializedDiagnosticPathsByTargetName: .failure(StringError("Building was skipped"))) + return result } let buildStartTime = DispatchTime.now() @@ -422,11 +424,8 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS // any errors up-front. Returns true if we should proceed with the build // or false if not. It will already have thrown any appropriate error. guard try await self.compilePlugins(in: subset) else { - if buildOutputs.contains(.buildPlan), let buildPlan = try? self.buildPlan { - return BuildResult(serializedDiagnosticPathsByTargetName: .failure(StringError("Plugin compilation failed")), buildPlan: buildPlan) - } else { - return BuildResult(serializedDiagnosticPathsByTargetName: .failure(StringError("Plugin compilation failed"))) - } + result.serializedDiagnosticPathsByTargetName = .failure(StringError("Plugin compilation failed")) + return result } let configuration = self.config.configuration(for: .target) @@ -456,15 +455,18 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS ) guard success else { throw Diagnostics.fatalError } - let serializedDiagnosticResult: Result<[String: [AbsolutePath]], Error> + if buildOutputs.contains(.buildPlan) { + result.buildPlan = try buildPlan + } + var serializedDiagnosticPaths: [String: [AbsolutePath]] = [:] do { for module in try buildPlan.buildModules { serializedDiagnosticPaths[module.module.name, default: []].append(contentsOf: module.diagnosticFiles) } - serializedDiagnosticResult = .success(serializedDiagnosticPaths) + result.serializedDiagnosticPathsByTargetName = .success(serializedDiagnosticPaths) } catch { - serializedDiagnosticResult = .failure(error) + result.serializedDiagnosticPathsByTargetName = .failure(error) } // Create backwards-compatibility symlink to old build path. @@ -479,11 +481,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS underlyingError: error ) - if buildOutputs.contains(.buildPlan), let buildPlan = try? self.buildPlan { - return BuildResult(serializedDiagnosticPathsByTargetName: serializedDiagnosticResult, buildPlan: buildPlan) - } else { - return BuildResult(serializedDiagnosticPathsByTargetName: serializedDiagnosticResult) - } + return result } } @@ -500,11 +498,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS ) } - if buildOutputs.contains(.buildPlan), let buildPlan = try? self.buildPlan { - return BuildResult(serializedDiagnosticPathsByTargetName: serializedDiagnosticResult, buildPlan: buildPlan) - } else { - return BuildResult(serializedDiagnosticPathsByTargetName: serializedDiagnosticResult) - } + return result } /// Compiles any plugins specified or implied by the build subset, returning diff --git a/Sources/Commands/PackageCommands/APIDiff.swift b/Sources/Commands/PackageCommands/APIDiff.swift index 5bae87ba9fc..856d90e7e0e 100644 --- a/Sources/Commands/PackageCommands/APIDiff.swift +++ b/Sources/Commands/PackageCommands/APIDiff.swift @@ -123,8 +123,8 @@ struct APIDiff: AsyncSwiftCommand { let apiDigesterTool = SwiftAPIDigester(fileSystem: swiftCommandState.fileSystem, tool: apiDigesterPath) // Build the current package. - let buildOutputResult = try await buildSystem.build(subset: .allExcludingTests, buildOutputs: [.buildPlan]) - guard let buildPlan = buildOutputResult.buildPlan else { + let buildResult = try await buildSystem.build(subset: .allExcludingTests, buildOutputs: [.buildPlan]) + guard let buildPlan = buildResult.buildPlan else { throw ExitCode.failure } diff --git a/Sources/Commands/Snippets/Cards/SnippetCard.swift b/Sources/Commands/Snippets/Cards/SnippetCard.swift index 327df62ca39..0810efaf92c 100644 --- a/Sources/Commands/Snippets/Cards/SnippetCard.swift +++ b/Sources/Commands/Snippets/Cards/SnippetCard.swift @@ -113,7 +113,7 @@ struct SnippetCard: Card { func runExample() async throws { print("Building '\(snippet.path)'\n") let buildSystem = try await swiftCommandState.createBuildSystem(explicitProduct: snippet.name, traitConfiguration: .init()) - _ = try await buildSystem.build(subset: .product(snippet.name), buildOutputs: []) + try await buildSystem.build(subset: .product(snippet.name), buildOutputs: []) let executablePath = try swiftCommandState.productsBuildParameters.buildPath.appending(component: snippet.name) if let exampleTarget = try await buildSystem.getPackageGraph().module(for: snippet.name) { try ProcessEnv.chdir(exampleTarget.sources.paths[0].parentDirectory) diff --git a/Sources/Commands/SwiftBuildCommand.swift b/Sources/Commands/SwiftBuildCommand.swift index 7de511f5d92..361085f85f6 100644 --- a/Sources/Commands/SwiftBuildCommand.swift +++ b/Sources/Commands/SwiftBuildCommand.swift @@ -203,7 +203,7 @@ public struct SwiftBuildCommand: AsyncSwiftCommand { outputStream: TSCBasic.stdoutStream ) do { - _ = try await buildSystem.build(subset: subset, buildOutputs: []) + try await buildSystem.build(subset: subset, buildOutputs: []) } catch _ as Diagnostics { throw ExitCode.failure } diff --git a/Sources/Commands/SwiftRunCommand.swift b/Sources/Commands/SwiftRunCommand.swift index 77bdf23f11c..e15e95ee57e 100644 --- a/Sources/Commands/SwiftRunCommand.swift +++ b/Sources/Commands/SwiftRunCommand.swift @@ -168,9 +168,9 @@ public struct SwiftRunCommand: AsyncSwiftCommand { ) let productName = try await findProductName(in: buildSystem.getPackageGraph()) if options.shouldBuildTests { - _ = try await buildSystem.build(subset: .allIncludingTests, buildOutputs: []) + try await buildSystem.build(subset: .allIncludingTests, buildOutputs: []) } else if options.shouldBuild { - _ = try await buildSystem.build(subset: .product(productName), buildOutputs: []) + try await buildSystem.build(subset: .product(productName), buildOutputs: []) } let productRelativePath = try swiftCommandState.productsBuildParameters.executablePath(for: productName) @@ -224,9 +224,9 @@ public struct SwiftRunCommand: AsyncSwiftCommand { ) let productName = try await findProductName(in: buildSystem.getPackageGraph()) if options.shouldBuildTests { - _ = try await buildSystem.build(subset: .allIncludingTests, buildOutputs: []) + try await buildSystem.build(subset: .allIncludingTests, buildOutputs: []) } else if options.shouldBuild { - _ = try await buildSystem.build(subset: .product(productName), buildOutputs: []) + try await buildSystem.build(subset: .product(productName), buildOutputs: []) } let executablePath = try swiftCommandState.productsBuildParameters.buildPath.appending(component: productName) diff --git a/Sources/Commands/SwiftTestCommand.swift b/Sources/Commands/SwiftTestCommand.swift index e498fa5aa30..a0bf4acaf0b 100644 --- a/Sources/Commands/SwiftTestCommand.swift +++ b/Sources/Commands/SwiftTestCommand.swift @@ -1572,7 +1572,7 @@ private func buildTestsIfNeeded( .allIncludingTests } - _ = try await buildSystem.build(subset: subset, buildOutputs: []) + try await buildSystem.build(subset: subset, buildOutputs: []) // Find the test product. let testProducts = await buildSystem.builtTestProducts diff --git a/Sources/Commands/Utilities/PluginDelegate.swift b/Sources/Commands/Utilities/PluginDelegate.swift index 06778015cda..078a1a25377 100644 --- a/Sources/Commands/Utilities/PluginDelegate.swift +++ b/Sources/Commands/Utilities/PluginDelegate.swift @@ -249,7 +249,7 @@ final class PluginDelegate: PluginInvocationDelegate { traitConfiguration: .init(), toolsBuildParameters: toolsBuildParameters ) - _ = try await buildSystem.build(subset: .allIncludingTests, buildOutputs: []) + try await buildSystem.build(subset: .allIncludingTests, buildOutputs: []) // Clean out the code coverage directory that may contain stale `profraw` files from a previous run of // the code coverage tool. diff --git a/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift b/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift index 6721621e4e7..eb4519e142a 100644 --- a/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift +++ b/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift @@ -56,8 +56,11 @@ public protocol BuildSystem: Cancellable { /// Returns the package graph used by the build system. func getPackageGraph() async throws -> ModulesGraph + /// Builds a subset of the package graph. + /// - Parameters: /// - buildOutputs: Additional build outputs requested from the build system. /// - Returns: A build result with details about requested build and outputs. + @discardableResult func build(subset: BuildSubset, buildOutputs: [BuildOutput]) async throws -> BuildResult var hasIntegratedAPIDigesterSupport: Bool { get } diff --git a/Sources/SwiftBuildSupport/PIFBuilder.swift b/Sources/SwiftBuildSupport/PIFBuilder.swift index 2cd3870f3f3..dc91af90b1f 100644 --- a/Sources/SwiftBuildSupport/PIFBuilder.swift +++ b/Sources/SwiftBuildSupport/PIFBuilder.swift @@ -167,7 +167,7 @@ public final class PIFBuilder { prettyPrint: Bool = true, preservePIFModelStructure: Bool = false, printPIFManifestGraphviz: Bool = false, - buildParameters: BuildParameters, + buildParameters: BuildParameters ) async throws -> String { let encoder = prettyPrint ? JSONEncoder.makeWithDefaults() : JSONEncoder() diff --git a/Sources/XCBuildSupport/XcodeBuildSystem.swift b/Sources/XCBuildSupport/XcodeBuildSystem.swift index 3183f8c1905..74d89e4271d 100644 --- a/Sources/XCBuildSupport/XcodeBuildSystem.swift +++ b/Sources/XCBuildSupport/XcodeBuildSystem.swift @@ -161,8 +161,10 @@ public final class XcodeBuildSystem: SPMBuildCore.BuildSystem { } public func build(subset: BuildSubset, buildOutputs: [BuildOutput]) async throws -> BuildResult { + let buildResult = BuildResult(serializedDiagnosticPathsByTargetName: .failure(StringError("XCBuild does not support reporting serialized diagnostics."))) + guard !buildParameters.shouldSkipBuilding else { - return BuildResult(serializedDiagnosticPathsByTargetName: .failure(StringError("XCBuild does not support reporting serialized diagnostics."))) + return buildResult } let pifBuilder = try await getPIFBuilder() @@ -244,11 +246,11 @@ public final class XcodeBuildSystem: SPMBuildCore.BuildSystem { throw Diagnostics.fatalError } - guard !self.logLevel.isQuiet else { return BuildResult(serializedDiagnosticPathsByTargetName: .failure(StringError("XCBuild does not support reporting serialized diagnostics.")))} + guard !self.logLevel.isQuiet else { return buildResult } self.outputStream.send("Build complete!\n") self.outputStream.flush() - return BuildResult(serializedDiagnosticPathsByTargetName: .failure(StringError("XCBuild does not support reporting serialized diagnostics."))) + return buildResult } func createBuildParametersFile() throws -> AbsolutePath { diff --git a/Sources/swift-bootstrap/main.swift b/Sources/swift-bootstrap/main.swift index ac682c02039..7935ca9490b 100644 --- a/Sources/swift-bootstrap/main.swift +++ b/Sources/swift-bootstrap/main.swift @@ -266,7 +266,7 @@ struct SwiftBootstrapBuildTool: AsyncParsableCommand { shouldDisableLocalRpath: shouldDisableLocalRpath, logLevel: logLevel ) - _ = try await buildSystem.build(subset: .allExcludingTests, buildOutputs: []) + try await buildSystem.build(subset: .allExcludingTests, buildOutputs: []) } func createBuildSystem( From b7b01f22220a35822c8fde76e100017c74e1ea01 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 11 Jul 2025 10:11:23 -0400 Subject: [PATCH 3/8] Tidy more --- Sources/Commands/PackageCommands/Install.swift | 2 +- Sources/Commands/Utilities/PluginDelegate.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Commands/PackageCommands/Install.swift b/Sources/Commands/PackageCommands/Install.swift index 06c9a66f41a..900839fd971 100644 --- a/Sources/Commands/PackageCommands/Install.swift +++ b/Sources/Commands/PackageCommands/Install.swift @@ -88,7 +88,7 @@ extension SwiftPackageCommand { commandState.preferredBuildConfiguration = .release } - _ = try await commandState.createBuildSystem(explicitProduct: productToInstall.name, traitConfiguration: .init()) + try await commandState.createBuildSystem(explicitProduct: productToInstall.name, traitConfiguration: .init()) .build(subset: .product(productToInstall.name), buildOutputs: []) let binPath = try commandState.productsBuildParameters.buildPath.appending(component: productToInstall.name) diff --git a/Sources/Commands/Utilities/PluginDelegate.swift b/Sources/Commands/Utilities/PluginDelegate.swift index 078a1a25377..349b7dd7261 100644 --- a/Sources/Commands/Utilities/PluginDelegate.swift +++ b/Sources/Commands/Utilities/PluginDelegate.swift @@ -499,7 +499,7 @@ final class PluginDelegate: PluginInvocationDelegate { extension BuildSystem { fileprivate func buildIgnoringError(subset: BuildSubset) async -> Bool { do { - _ = try await self.build(subset: subset, buildOutputs: []) + try await self.build(subset: subset, buildOutputs: []) return true } catch { return false From c797b79ed337108915221f44c836ad8591a9e0f8 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 11 Jul 2025 17:15:42 -0400 Subject: [PATCH 4/8] Add todos for the symbol graph generation options --- Sources/Commands/PackageCommands/DumpCommands.swift | 1 + Sources/Commands/Utilities/PluginDelegate.swift | 1 + Sources/SPMBuildCore/BuildSystem/BuildSystem.swift | 6 ++++++ Sources/SwiftBuildSupport/SwiftBuildSystem.swift | 1 + 4 files changed, 9 insertions(+) diff --git a/Sources/Commands/PackageCommands/DumpCommands.swift b/Sources/Commands/PackageCommands/DumpCommands.swift index ac1e7cc7f82..10f1a3f2f6f 100644 --- a/Sources/Commands/PackageCommands/DumpCommands.swift +++ b/Sources/Commands/PackageCommands/DumpCommands.swift @@ -56,6 +56,7 @@ struct DumpSymbolGraph: AsyncSwiftCommand { traitConfiguration: .init(enableAllTraits: true), cacheBuildManifest: false ) + // TODO pass along the various flags as associated values to the symbol graph build output (e.g. includeSPISymbols) let buildResult = try await buildSystem.build(subset: .allExcludingTests, buildOutputs: [.symbolGraph, .buildPlan]) let symbolGraphDirectory = try swiftCommandState.productsBuildParameters.dataPath.appending("symbolgraph") diff --git a/Sources/Commands/Utilities/PluginDelegate.swift b/Sources/Commands/Utilities/PluginDelegate.swift index 349b7dd7261..fbe5f238c60 100644 --- a/Sources/Commands/Utilities/PluginDelegate.swift +++ b/Sources/Commands/Utilities/PluginDelegate.swift @@ -415,6 +415,7 @@ final class PluginDelegate: PluginInvocationDelegate { ) // Build the target, if needed. We are interested in symbol graph (ideally) or a build plan. + // TODO pass along the options as associated values to the symbol graph build output (e.g. includeSPI) let buildResult = try await buildSystem.build(subset: .target(targetName), buildOutputs: [.symbolGraph, .buildPlan]) if buildResult.symbolGraph { diff --git a/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift b/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift index eb4519e142a..92eb63477ea 100644 --- a/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift +++ b/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift @@ -40,6 +40,12 @@ public enum BuildSubset { /// result for indication that the output was produced. public enum BuildOutput { case symbolGraph + // TODO associated values for the following symbol graph options: + // "-skip-inherited-docs" + // "-symbol-graph-minimum-access-level", “” + // "-include-spi-symbols" + // "-emit-extension-block-symbols" + // "-emit-synthesized-members" case buildPlan } diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index c8def88d043..956eacf918a 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -568,6 +568,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { settings["LM_SKIP_METADATA_EXTRACTION"] = "YES" if genSymbolGraph { settings["RUN_SYMBOL_GRAPH_EXTRACT"] = "YES" + // TODO set additional symbol graph options from the build output here, such as "include-spi-symbols" } let normalizedTriple = Triple(buildParameters.triple.triple, normalizing: true) From a386fd7f303eec86a19c90a79b1bf6a0376e1059 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 11 Jul 2025 17:18:06 -0400 Subject: [PATCH 5/8] Skip pretty print symbol graph test until swift build supports it --- Tests/CommandsTests/PackageCommandTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/CommandsTests/PackageCommandTests.swift b/Tests/CommandsTests/PackageCommandTests.swift index 1ca2dbe3d8d..afc3088874f 100644 --- a/Tests/CommandsTests/PackageCommandTests.swift +++ b/Tests/CommandsTests/PackageCommandTests.swift @@ -571,6 +571,7 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { func testDumpSymbolGraphPrettyFormatting() async throws { // Depending on how the test is running, the `swift-symbolgraph-extract` tool might be unavailable. try XCTSkipIf((try? UserToolchain.default.getSymbolGraphExtract()) == nil, "skipping test because the `swift-symbolgraph-extract` tools isn't available") + try XCTSkipIf(buildSystemProvider == .swiftbuild, "skipping test because pretty printing isn't yet supported with swiftbuild build system via swift build and the swift compiler") try await fixture(name: "DependencyResolution/Internal/Simple") { fixturePath in let prettyGraphData = try await XCTAsyncUnwrap(await symbolGraph(atPath: fixturePath, withPrettyPrinting: true)) From cf9b61c4f2c0262afb6f7aba9a374f26544569d1 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Mon, 14 Jul 2025 15:16:37 -0400 Subject: [PATCH 6/8] Move the symbol graph output for a target into the buid system abstraction --- .../Commands/PackageCommands/DumpCommands.swift | 6 ++++-- Sources/Commands/Utilities/PluginDelegate.swift | 4 ++-- .../SPMBuildCore/BuildSystem/BuildSystem.swift | 15 +++++++++++++-- Sources/SwiftBuildSupport/SwiftBuildSystem.swift | 5 ++++- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/Sources/Commands/PackageCommands/DumpCommands.swift b/Sources/Commands/PackageCommands/DumpCommands.swift index 10f1a3f2f6f..f3bb785f487 100644 --- a/Sources/Commands/PackageCommands/DumpCommands.swift +++ b/Sources/Commands/PackageCommands/DumpCommands.swift @@ -66,14 +66,16 @@ struct DumpSymbolGraph: AsyncSwiftCommand { try? fs.removeFileTree(symbolGraphDirectory) try fs.createDirectory(symbolGraphDirectory, recursive: true) - if buildResult.symbolGraph { + if let symbolGraph = buildResult.symbolGraph { // The build system produced symbol graphs for us, one for each target. let buildPath = try swiftCommandState.productsBuildParameters.buildPath // Copy the symbol graphs from the target-specific locations to the single output directory for rootPackage in try await buildSystem.getPackageGraph().rootPackages { for module in rootPackage.modules { - if case let sgDir = buildPath.appending(components: "\(try swiftCommandState.productsBuildParameters.triple.archName)", "\(module.name).symbolgraphs"), fs.exists(sgDir) { + let sgDir = symbolGraph.outputLocationForTarget(module.name, try swiftCommandState.productsBuildParameters) + + if case let sgDir = buildPath.appending(components: sgDir), fs.exists(sgDir) { for sgFile in try fs.getDirectoryContents(sgDir) { try fs.copy(from: sgDir.appending(components: sgFile), to: symbolGraphDirectory.appending(sgFile)) } diff --git a/Sources/Commands/Utilities/PluginDelegate.swift b/Sources/Commands/Utilities/PluginDelegate.swift index fbe5f238c60..c2843f0e5f0 100644 --- a/Sources/Commands/Utilities/PluginDelegate.swift +++ b/Sources/Commands/Utilities/PluginDelegate.swift @@ -418,9 +418,9 @@ final class PluginDelegate: PluginInvocationDelegate { // TODO pass along the options as associated values to the symbol graph build output (e.g. includeSPI) let buildResult = try await buildSystem.build(subset: .target(targetName), buildOutputs: [.symbolGraph, .buildPlan]) - if buildResult.symbolGraph { + if let symbolGraph = buildResult.symbolGraph { let path = (try swiftCommandState.productsBuildParameters.buildPath) - return PluginInvocationSymbolGraphResult(directoryPath: "\(path)/\(try swiftCommandState.productsBuildParameters.triple.archName)/\(targetName).symbolgraphs") + return PluginInvocationSymbolGraphResult(directoryPath: "\(path)/\(symbolGraph.outputLocationForTarget(targetName, try swiftCommandState.productsBuildParameters).joined(separator:"/"))") } else if let buildPlan = buildResult.buildPlan { func lookupDescription( for moduleName: String, diff --git a/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift b/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift index 92eb63477ea..801d8795d03 100644 --- a/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift +++ b/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift @@ -80,14 +80,25 @@ extension BuildSystem { } } +public struct SymbolGraphResult { + public init(outputLocationForTarget: @escaping (String, BuildParameters) -> [String]) { + self.outputLocationForTarget = outputLocationForTarget + } + + /// Find the build path relative location of the symbol graph output directory + /// for a provided target and build parameters. Note that the directory may not + /// exist when the target doesn't have any symbol graph output, as one example. + public let outputLocationForTarget: (String, BuildParameters) -> [String] +} + public struct BuildResult { - package init(serializedDiagnosticPathsByTargetName: Result<[String: [AbsolutePath]], Error>, symbolGraph: Bool = false, buildPlan: BuildPlan? = nil) { + package init(serializedDiagnosticPathsByTargetName: Result<[String: [AbsolutePath]], Error>, symbolGraph: SymbolGraphResult? = nil, buildPlan: BuildPlan? = nil) { self.serializedDiagnosticPathsByTargetName = serializedDiagnosticPathsByTargetName self.symbolGraph = symbolGraph self.buildPlan = buildPlan } - public var symbolGraph: Bool + public var symbolGraph: SymbolGraphResult? public var buildPlan: BuildPlan? public var serializedDiagnosticPathsByTargetName: Result<[String: [AbsolutePath]], Error> } diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index 956eacf918a..4b88f73498b 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -511,7 +511,10 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { } catch { throw error } - return BuildResult(serializedDiagnosticPathsByTargetName: .success(serializedDiagnosticPathsByTargetName), symbolGraph: genSymbolGraph) + + return BuildResult(serializedDiagnosticPathsByTargetName: .success(serializedDiagnosticPathsByTargetName), symbolGraph: SymbolGraphResult(outputLocationForTarget: { target, buildParameters in + return ["\(buildParameters.triple.archName)", "\(target).symbolgraphs"] + })) } } From d28059b2ea4d66e6fe606a395f68bc074d438b74 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Mon, 14 Jul 2025 15:23:45 -0400 Subject: [PATCH 7/8] Mark test as intermittent --- Tests/FunctionalTests/TraitTests.swift | 38 ++++++++++++++------------ 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/Tests/FunctionalTests/TraitTests.swift b/Tests/FunctionalTests/TraitTests.swift index 5b65e2e51f6..e8eda347f88 100644 --- a/Tests/FunctionalTests/TraitTests.swift +++ b/Tests/FunctionalTests/TraitTests.swift @@ -560,24 +560,28 @@ struct TraitTests { buildSystem: BuildSystemProvider.Kind, configuration: BuildConfiguration, ) async throws { - try await fixture(name: "Traits") { fixturePath in - let (stdout, _) = try await executeSwiftPackage( - fixturePath.appending("Package10"), - configuration: configuration, - extraArgs: ["dump-symbol-graph", "--experimental-prune-unused-dependencies"], - buildSystem: buildSystem, - ) - let optionalPath = stdout - .lazy - .split(whereSeparator: \.isNewline) - .first { String($0).hasPrefix("Files written to ") }? - .dropFirst(17) + try await withKnownIssue(isIntermittent: true, { + try await fixture(name: "Traits") { fixturePath in + let (stdout, _) = try await executeSwiftPackage( + fixturePath.appending("Package10"), + configuration: configuration, + extraArgs: ["dump-symbol-graph", "--experimental-prune-unused-dependencies"], + buildSystem: buildSystem, + ) + let optionalPath = stdout + .lazy + .split(whereSeparator: \.isNewline) + .first { String($0).hasPrefix("Files written to ") }? + .dropFirst(17) - let path = try String(#require(optionalPath)) - let symbolGraph = try String(contentsOfFile: "\(path)/Package10Library1.symbols.json", encoding: .utf8) - #expect(symbolGraph.contains("TypeGatedByPackage10Trait1")) - #expect(symbolGraph.contains("TypeGatedByPackage10Trait2")) - } + let path = try String(#require(optionalPath)) + let symbolGraph = try String(contentsOfFile: "\(path)/Package10Library1.symbols.json", encoding: .utf8) + #expect(symbolGraph.contains("TypeGatedByPackage10Trait1")) + #expect(symbolGraph.contains("TypeGatedByPackage10Trait2")) + } + }, when: { + ProcessInfo.hostOperatingSystem == .windows + }) } @Test( From 34c0372b914258ceec40ebade9254a432c232fcd Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Tue, 15 Jul 2025 09:21:15 -0400 Subject: [PATCH 8/8] Skip dump symbol graph test with swiftbuild on Windows --- Tests/CommandsTests/PackageCommandTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/CommandsTests/PackageCommandTests.swift b/Tests/CommandsTests/PackageCommandTests.swift index afc3088874f..5e7bd581ad6 100644 --- a/Tests/CommandsTests/PackageCommandTests.swift +++ b/Tests/CommandsTests/PackageCommandTests.swift @@ -559,7 +559,8 @@ class PackageCommandTestCase: CommandsBuildProviderTestCase { func testDumpSymbolGraphCompactFormatting() async throws { // Depending on how the test is running, the `swift-symbolgraph-extract` tool might be unavailable. - try XCTSkipIf((try? UserToolchain.default.getSymbolGraphExtract()) == nil, "skipping test because the `swift-symbolgraph-extract` tools isn't available") + try XCTSkipIf(buildSystemProvider == .native && (try? UserToolchain.default.getSymbolGraphExtract()) == nil, "skipping test because the `swift-symbolgraph-extract` tools isn't available") + try XCTSkipIf(buildSystemProvider == .swiftbuild && ProcessInfo.hostOperatingSystem == .windows, "skipping test for Windows because of long file path issues") try await fixture(name: "DependencyResolution/Internal/Simple") { fixturePath in let compactGraphData = try await XCTAsyncUnwrap(await symbolGraph(atPath: fixturePath, withPrettyPrinting: false))