Skip to content

Commit 0181475

Browse files
authored
Symbol graph support for swiftbuild build system (#8923)
Refactor the build system protocol to make symbol graphs and build plans optional build outputs. Rewrite the dump-symbol-graph and the plugin delegate to use this new protocol and get support from the swiftbuild build system.
1 parent 7b1a918 commit 0181475

File tree

19 files changed

+290
-185
lines changed

19 files changed

+290
-185
lines changed

Sources/Build/BuildOperation.swift

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -396,9 +396,11 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
396396
}
397397

398398
/// Perform a build using the given build description and subset.
399-
public func build(subset: BuildSubset) async throws -> BuildResult {
399+
public func build(subset: BuildSubset, buildOutputs: [BuildOutput]) async throws -> BuildResult {
400+
var result = BuildResult(serializedDiagnosticPathsByTargetName: .failure(StringError("Building was skipped")))
401+
400402
guard !self.config.shouldSkipBuilding(for: .target) else {
401-
return BuildResult(serializedDiagnosticPathsByTargetName: .failure(StringError("Building was skipped")))
403+
return result
402404
}
403405

404406
let buildStartTime = DispatchTime.now()
@@ -422,7 +424,8 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
422424
// any errors up-front. Returns true if we should proceed with the build
423425
// or false if not. It will already have thrown any appropriate error.
424426
guard try await self.compilePlugins(in: subset) else {
425-
return BuildResult(serializedDiagnosticPathsByTargetName: .failure(StringError("Plugin compilation failed")))
427+
result.serializedDiagnosticPathsByTargetName = .failure(StringError("Plugin compilation failed"))
428+
return result
426429
}
427430

428431
let configuration = self.config.configuration(for: .target)
@@ -452,15 +455,18 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
452455
)
453456
guard success else { throw Diagnostics.fatalError }
454457

455-
let serializedDiagnosticResult: Result<[String: [AbsolutePath]], Error>
458+
if buildOutputs.contains(.buildPlan) {
459+
result.buildPlan = try buildPlan
460+
}
461+
456462
var serializedDiagnosticPaths: [String: [AbsolutePath]] = [:]
457463
do {
458464
for module in try buildPlan.buildModules {
459465
serializedDiagnosticPaths[module.module.name, default: []].append(contentsOf: module.diagnosticFiles)
460466
}
461-
serializedDiagnosticResult = .success(serializedDiagnosticPaths)
467+
result.serializedDiagnosticPathsByTargetName = .success(serializedDiagnosticPaths)
462468
} catch {
463-
serializedDiagnosticResult = .failure(error)
469+
result.serializedDiagnosticPathsByTargetName = .failure(error)
464470
}
465471

466472
// Create backwards-compatibility symlink to old build path.
@@ -474,7 +480,8 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
474480
warning: "unable to delete \(oldBuildPath), skip creating symbolic link",
475481
underlyingError: error
476482
)
477-
return BuildResult(serializedDiagnosticPathsByTargetName: serializedDiagnosticResult)
483+
484+
return result
478485
}
479486
}
480487

@@ -491,7 +498,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
491498
)
492499
}
493500

494-
return BuildResult(serializedDiagnosticPathsByTargetName: serializedDiagnosticResult)
501+
return result
495502
}
496503

497504
/// Compiles any plugins specified or implied by the build subset, returning

Sources/Commands/PackageCommands/APIDiff.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,17 @@ struct APIDiff: AsyncSwiftCommand {
123123
let apiDigesterTool = SwiftAPIDigester(fileSystem: swiftCommandState.fileSystem, tool: apiDigesterPath)
124124

125125
// Build the current package.
126-
try await buildSystem.build()
126+
let buildResult = try await buildSystem.build(subset: .allExcludingTests, buildOutputs: [.buildPlan])
127+
guard let buildPlan = buildResult.buildPlan else {
128+
throw ExitCode.failure
129+
}
127130

128131
// Dump JSON for the baseline package.
129132
let baselineDumper = try APIDigesterBaselineDumper(
130133
baselineRevision: baselineRevision,
131134
packageRoot: swiftCommandState.getPackageRoot(),
132-
productsBuildParameters: try buildSystem.buildPlan.destinationBuildParameters,
133-
toolsBuildParameters: try buildSystem.buildPlan.toolsBuildParameters,
135+
productsBuildParameters: buildPlan.destinationBuildParameters,
136+
toolsBuildParameters: buildPlan.toolsBuildParameters,
134137
apiDigesterTool: apiDigesterTool,
135138
observabilityScope: swiftCommandState.observabilityScope
136139
)
@@ -159,7 +162,7 @@ struct APIDiff: AsyncSwiftCommand {
159162
if let comparisonResult = try apiDigesterTool.compareAPIToBaseline(
160163
at: moduleBaselinePath,
161164
for: module,
162-
buildPlan: try buildSystem.buildPlan,
165+
buildPlan: buildPlan,
163166
except: breakageAllowlistPath
164167
) {
165168
return comparisonResult

Sources/Commands/PackageCommands/DumpCommands.swift

Lines changed: 62 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -52,54 +52,79 @@ struct DumpSymbolGraph: AsyncSwiftCommand {
5252
//
5353
// We turn build manifest caching off because we need the build plan.
5454
let buildSystem = try await swiftCommandState.createBuildSystem(
55-
explicitBuildSystem: .native,
5655
// We are enabling all traits for dumping the symbol graph.
5756
traitConfiguration: .init(enableAllTraits: true),
5857
cacheBuildManifest: false
5958
)
60-
try await buildSystem.build()
59+
// TODO pass along the various flags as associated values to the symbol graph build output (e.g. includeSPISymbols)
60+
let buildResult = try await buildSystem.build(subset: .allExcludingTests, buildOutputs: [.symbolGraph, .buildPlan])
6161

62-
// Configure the symbol graph extractor.
63-
let symbolGraphExtractor = try SymbolGraphExtract(
64-
fileSystem: swiftCommandState.fileSystem,
65-
tool: swiftCommandState.getTargetToolchain().getSymbolGraphExtract(),
66-
observabilityScope: swiftCommandState.observabilityScope,
67-
skipSynthesizedMembers: skipSynthesizedMembers,
68-
minimumAccessLevel: minimumAccessLevel,
69-
skipInheritedDocs: skipInheritedDocs,
70-
includeSPISymbols: includeSPISymbols,
71-
emitExtensionBlockSymbols: extensionBlockSymbolBehavior == .emitExtensionBlockSymbols,
72-
outputFormat: .json(pretty: prettyPrint)
73-
)
62+
let symbolGraphDirectory = try swiftCommandState.productsBuildParameters.dataPath.appending("symbolgraph")
7463

75-
// Run the tool once for every library and executable target in the root package.
76-
let buildPlan = try buildSystem.buildPlan
77-
let modulesGraph = try await buildSystem.getPackageGraph()
78-
let symbolGraphDirectory = buildPlan.destinationBuildParameters.dataPath.appending("symbolgraph")
79-
for description in buildPlan.buildModules {
80-
guard description.module.type == .library,
81-
modulesGraph.rootPackages[description.package.id] != nil
82-
else {
83-
continue
84-
}
64+
let fs = swiftCommandState.fileSystem
65+
66+
try? fs.removeFileTree(symbolGraphDirectory)
67+
try fs.createDirectory(symbolGraphDirectory, recursive: true)
68+
69+
if let symbolGraph = buildResult.symbolGraph {
70+
// The build system produced symbol graphs for us, one for each target.
71+
let buildPath = try swiftCommandState.productsBuildParameters.buildPath
8572

86-
print("-- Emitting symbol graph for", description.module.name)
87-
let result = try symbolGraphExtractor.extractSymbolGraph(
88-
for: description,
89-
outputRedirection: .collect(redirectStderr: true),
90-
outputDirectory: symbolGraphDirectory,
91-
verboseOutput: swiftCommandState.logLevel <= .info
73+
// Copy the symbol graphs from the target-specific locations to the single output directory
74+
for rootPackage in try await buildSystem.getPackageGraph().rootPackages {
75+
for module in rootPackage.modules {
76+
let sgDir = symbolGraph.outputLocationForTarget(module.name, try swiftCommandState.productsBuildParameters)
77+
78+
if case let sgDir = buildPath.appending(components: sgDir), fs.exists(sgDir) {
79+
for sgFile in try fs.getDirectoryContents(sgDir) {
80+
try fs.copy(from: sgDir.appending(components: sgFile), to: symbolGraphDirectory.appending(sgFile))
81+
}
82+
}
83+
}
84+
}
85+
} else if let buildPlan = buildResult.buildPlan {
86+
// Otherwise, with a build plan we can run the symbol graph extractor tool on the built targets.
87+
let symbolGraphExtractor = try SymbolGraphExtract(
88+
fileSystem: swiftCommandState.fileSystem,
89+
tool: swiftCommandState.getTargetToolchain().getSymbolGraphExtract(),
90+
observabilityScope: swiftCommandState.observabilityScope,
91+
skipSynthesizedMembers: skipSynthesizedMembers,
92+
minimumAccessLevel: minimumAccessLevel,
93+
skipInheritedDocs: skipInheritedDocs,
94+
includeSPISymbols: includeSPISymbols,
95+
emitExtensionBlockSymbols: extensionBlockSymbolBehavior == .emitExtensionBlockSymbols,
96+
outputFormat: .json(pretty: prettyPrint)
9297
)
9398

94-
if result.exitStatus != .terminated(code: 0) {
95-
let commandline = "\nUsing commandline: \(result.arguments)"
96-
switch result.output {
97-
case .success(let value):
98-
swiftCommandState.observabilityScope.emit(error: "Failed to emit symbol graph for '\(description.module.c99name)': \(String(decoding: value, as: UTF8.self))\(commandline)")
99-
case .failure(let error):
100-
swiftCommandState.observabilityScope.emit(error: "Internal error while emitting symbol graph for '\(description.module.c99name)': \(error)\(commandline)")
99+
// Run the tool once for every library and executable target in the root package.
100+
let modulesGraph = try await buildSystem.getPackageGraph()
101+
for description in buildPlan.buildModules {
102+
guard description.module.type == .library,
103+
modulesGraph.rootPackages[description.package.id] != nil
104+
else {
105+
continue
106+
}
107+
108+
print("-- Emitting symbol graph for", description.module.name)
109+
let result = try symbolGraphExtractor.extractSymbolGraph(
110+
for: description,
111+
outputRedirection: .collect(redirectStderr: true),
112+
outputDirectory: symbolGraphDirectory,
113+
verboseOutput: swiftCommandState.logLevel <= .info
114+
)
115+
116+
if result.exitStatus != .terminated(code: 0) {
117+
let commandline = "\nUsing commandline: \(result.arguments)"
118+
switch result.output {
119+
case .success(let value):
120+
swiftCommandState.observabilityScope.emit(error: "Failed to emit symbol graph for '\(description.module.c99name)': \(String(decoding: value, as: UTF8.self))\(commandline)")
121+
case .failure(let error):
122+
swiftCommandState.observabilityScope.emit(error: "Internal error while emitting symbol graph for '\(description.module.c99name)': \(error)\(commandline)")
123+
}
101124
}
102125
}
126+
} else {
127+
throw InternalError("Build system \(buildSystem) cannot produce a symbol graph.")
103128
}
104129

105130
print("Files written to", symbolGraphDirectory.pathString)

Sources/Commands/PackageCommands/Install.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ extension SwiftPackageCommand {
8989
}
9090

9191
try await commandState.createBuildSystem(explicitProduct: productToInstall.name, traitConfiguration: .init())
92-
.build(subset: .product(productToInstall.name))
92+
.build(subset: .product(productToInstall.name), buildOutputs: [])
9393

9494
let binPath = try commandState.productsBuildParameters.buildPath.appending(component: productToInstall.name)
9595
let finalBinPath = swiftpmBinDir.appending(component: binPath.basename)

Sources/Commands/PackageCommands/Migrate.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,15 @@ extension SwiftPackageCommand {
101101
// Next, let's build all of the individual targets or the
102102
// whole project to get diagnostic files.
103103
print("> Starting the build")
104+
104105
var diagnosticsPaths: [String: [AbsolutePath]] = [:]
105106
if !targets.isEmpty {
106107
for target in targets {
107-
let buildResult = try await buildSystem.build(subset: .target(target))
108+
let buildResult = try await buildSystem.build(subset: .target(target), buildOutputs: [])
108109
diagnosticsPaths.merge(try buildResult.serializedDiagnosticPathsByTargetName.get(), uniquingKeysWith: { $0 + $1 })
109110
}
110111
} else {
111-
diagnosticsPaths = try await buildSystem.build(subset: .allIncludingTests).serializedDiagnosticPathsByTargetName.get()
112+
diagnosticsPaths = try await buildSystem.build(subset: .allIncludingTests, buildOutputs: []).serializedDiagnosticPathsByTargetName.get()
112113
}
113114

114115
var summary = SwiftFixIt.Summary(numberOfFixItsApplied: 0, numberOfFilesChanged: 0)

Sources/Commands/PackageCommands/PluginCommand.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,11 +351,10 @@ struct PluginCommand: AsyncSwiftCommand {
351351
for: try pluginScriptRunner.hostTriple
352352
) { name, path in
353353
// 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.
354-
try await buildSystem.build(subset: .product(name, for: .host))
354+
let buildResult = try await buildSystem.build(subset: .product(name, for: .host), buildOutputs: [.buildPlan])
355355

356-
// TODO determine if there is a common way to calculate the build tool binary path that doesn't depend on the build system.
357-
if buildSystemKind == .native {
358-
if let builtTool = try buildSystem.buildPlan.buildProducts.first(where: {
356+
if let buildPlan = buildResult.buildPlan {
357+
if let builtTool = buildPlan.buildProducts.first(where: {
359358
$0.product.name == name && $0.buildParameters.destination == .host
360359
}) {
361360
return try builtTool.binaryPath

Sources/Commands/Snippets/Cards/SnippetCard.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ struct SnippetCard: Card {
113113
func runExample() async throws {
114114
print("Building '\(snippet.path)'\n")
115115
let buildSystem = try await swiftCommandState.createBuildSystem(explicitProduct: snippet.name, traitConfiguration: .init())
116-
try await buildSystem.build(subset: .product(snippet.name))
116+
try await buildSystem.build(subset: .product(snippet.name), buildOutputs: [])
117117
let executablePath = try swiftCommandState.productsBuildParameters.buildPath.appending(component: snippet.name)
118118
if let exampleTarget = try await buildSystem.getPackageGraph().module(for: snippet.name) {
119119
try ProcessEnv.chdir(exampleTarget.sources.paths[0].parentDirectory)

Sources/Commands/SwiftBuildCommand.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ public struct SwiftBuildCommand: AsyncSwiftCommand {
203203
outputStream: TSCBasic.stdoutStream
204204
)
205205
do {
206-
try await buildSystem.build(subset: subset)
206+
try await buildSystem.build(subset: subset, buildOutputs: [])
207207
} catch _ as Diagnostics {
208208
throw ExitCode.failure
209209
}

Sources/Commands/SwiftRunCommand.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,13 @@ public struct SwiftRunCommand: AsyncSwiftCommand {
145145
)
146146

147147
// Perform build.
148-
try await buildSystem.build()
148+
let buildResult = try await buildSystem.build(subset: .allExcludingTests, buildOutputs: [.buildPlan])
149+
guard let buildPlan = buildResult.buildPlan else {
150+
throw ExitCode.failure
151+
}
149152

150153
// Execute the REPL.
151-
let arguments = try buildSystem.buildPlan.createREPLArguments()
154+
let arguments = try buildPlan.createREPLArguments()
152155
print("Launching Swift REPL with arguments: \(arguments.joined(separator: " "))")
153156
try self.run(
154157
fileSystem: swiftCommandState.fileSystem,
@@ -165,9 +168,9 @@ public struct SwiftRunCommand: AsyncSwiftCommand {
165168
)
166169
let productName = try await findProductName(in: buildSystem.getPackageGraph())
167170
if options.shouldBuildTests {
168-
try await buildSystem.build(subset: .allIncludingTests)
171+
try await buildSystem.build(subset: .allIncludingTests, buildOutputs: [])
169172
} else if options.shouldBuild {
170-
try await buildSystem.build(subset: .product(productName))
173+
try await buildSystem.build(subset: .product(productName), buildOutputs: [])
171174
}
172175

173176
let productRelativePath = try swiftCommandState.productsBuildParameters.executablePath(for: productName)
@@ -221,9 +224,9 @@ public struct SwiftRunCommand: AsyncSwiftCommand {
221224
)
222225
let productName = try await findProductName(in: buildSystem.getPackageGraph())
223226
if options.shouldBuildTests {
224-
try await buildSystem.build(subset: .allIncludingTests)
227+
try await buildSystem.build(subset: .allIncludingTests, buildOutputs: [])
225228
} else if options.shouldBuild {
226-
try await buildSystem.build(subset: .product(productName))
229+
try await buildSystem.build(subset: .product(productName), buildOutputs: [])
227230
}
228231

229232
let executablePath = try swiftCommandState.productsBuildParameters.buildPath.appending(component: productName)

Sources/Commands/SwiftTestCommand.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1572,7 +1572,7 @@ private func buildTestsIfNeeded(
15721572
.allIncludingTests
15731573
}
15741574

1575-
try await buildSystem.build(subset: subset)
1575+
try await buildSystem.build(subset: subset, buildOutputs: [])
15761576

15771577
// Find the test product.
15781578
let testProducts = await buildSystem.builtTestProducts

0 commit comments

Comments
 (0)