Skip to content

Commit 850c772

Browse files
authored
Make the symbol graph extraction requested by plugins more robust (#3993)
* Rework the SymbolGraphExtract so it's more suitable for being called in the context of plugins. This includes parameterizing the output format and the output path, as well as the diagnostics that are printed, and applying the extractor to a target at a time and not always to all the targets in a package. The original behavior for the `dump-symbol-graph` command is unmodified, but some of the specifics are hoisted into the DumpSymbolGraph command implementation rather than in the reusable SymbolGraphExtract type. * Make the symbol graph generation more robust by using the refactored SymbolGraphExtract: - only generate symbol graph information for the specified target - use a unique symbol graph output directory for each package/target combination - added a unit test to check these things. * Skip the test in a bootstrap situation where we're using a Swift compiler that hasn't built the `swift-symbolgraph-extract` tool. There is a `SWIFT_SYMBOLGRAPH_EXTRACT` env var that could be used to point it another binary, but on platforms other than macOS we don't have an existing Xcode or anything that would be guaranteed to contain this tool. This test should still run on full builds and on self tests, where the tool is available. rdar://87234110&86785765
1 parent 14cd25a commit 850c772

File tree

3 files changed

+211
-102
lines changed

3 files changed

+211
-102
lines changed

Sources/Commands/SwiftPackageTool.swift

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ extension SwiftPackageTool {
531531
struct DumpSymbolGraph: SwiftCommand {
532532
static let configuration = CommandConfiguration(
533533
abstract: "Dump Symbol Graph")
534-
static let defaultMinimumAccessLevel = AccessLevel.public
534+
static let defaultMinimumAccessLevel = SymbolGraphExtract.AccessLevel.public
535535

536536
@OptionGroup(_hiddenFromHelp: true)
537537
var swiftOptions: SwiftToolOptions
@@ -542,7 +542,7 @@ extension SwiftPackageTool {
542542
@Flag(help: "Skip members inherited through classes or default implementations.")
543543
var skipSynthesizedMembers = false
544544

545-
@Option(help: "Include symbols with this access level or more. Possible values: \(AccessLevel.allValueStrings.joined(separator: " | "))")
545+
@Option(help: "Include symbols with this access level or more. Possible values: \(SymbolGraphExtract.AccessLevel.allValueStrings.joined(separator: " | "))")
546546
var minimumAccessLevel = defaultMinimumAccessLevel
547547

548548
@Flag(help: "Skip emitting doc comments for members inherited through classes or default implementations.")
@@ -552,23 +552,34 @@ extension SwiftPackageTool {
552552
var includeSPISymbols = false
553553

554554
func run(_ swiftTool: SwiftTool) throws {
555-
let symbolGraphExtract = try SymbolGraphExtract(
556-
tool: swiftTool.getToolchain().getSymbolGraphExtract())
557-
558555
// Build the current package.
559556
//
560557
// We turn build manifest caching off because we need the build plan.
561558
let buildOp = try swiftTool.createBuildOperation(cacheBuildManifest: false)
562559
try buildOp.build()
563560

564-
try symbolGraphExtract.dumpSymbolGraph(
565-
buildPlan: buildOp.buildPlan!,
566-
prettyPrint: prettyPrint,
567-
skipSynthesisedMembers: skipSynthesizedMembers,
561+
// Configure the symbol graph extractor.
562+
let symbolGraphExtractor = try SymbolGraphExtract(
563+
tool: swiftTool.getToolchain().getSymbolGraphExtract(),
564+
skipSynthesizedMembers: skipSynthesizedMembers,
568565
minimumAccessLevel: minimumAccessLevel,
569566
skipInheritedDocs: skipInheritedDocs,
570-
includeSPISymbols: includeSPISymbols
571-
)
567+
includeSPISymbols: includeSPISymbols)
568+
569+
// Run the tool once for every library and executable target in the root package.
570+
let buildPlan = buildOp.buildPlan!
571+
let symbolGraphDirectory = buildPlan.buildParameters.dataPath.appending(component: "symbolgraph")
572+
let targets = buildPlan.graph.rootPackages.flatMap{ $0.targets }.filter{ $0.type == .library || $0.type == .executable }
573+
for target in targets {
574+
print("-- Emitting symbol graph for", target.name)
575+
try symbolGraphExtractor.extractSymbolGraph(
576+
target: target,
577+
buildPlan: buildPlan,
578+
logLevel: swiftTool.logLevel,
579+
outputDirectory: symbolGraphDirectory)
580+
}
581+
582+
print("Files written to", symbolGraphDirectory.pathString)
572583
}
573584
}
574585

@@ -1217,37 +1228,58 @@ final class PluginDelegate: PluginInvocationDelegate {
12171228
}
12181229

12191230
private func createSymbolGraph(forTarget targetName: String, options: PluginInvocationSymbolGraphOptions) throws -> PluginInvocationSymbolGraphResult {
1220-
// Current implementation uses `SymbolGraphExtract()` but we can probably do better in the future.
1221-
let buildParameters = try swiftTool.buildParameters()
1222-
let buildOperation = try swiftTool.createBuildOperation(cacheBuildManifest: false) // We only get a build plan if we don't cache
1223-
try buildOperation.build(subset: .target(targetName))
1224-
let symbolGraphExtract = try SymbolGraphExtract(tool: swiftTool.getToolchain().getSymbolGraphExtract())
1225-
let minimumAccessLevel: AccessLevel
1231+
// Current implementation uses `SymbolGraphExtract()` but in the future we should emit the symbol graph while building.
1232+
1233+
// Create a build operation for building the target., skipping the the cache because we need the build plan.
1234+
let buildOperation = try swiftTool.createBuildOperation(cacheBuildManifest: false)
1235+
1236+
// Find the target in the build operation's package graph; it's an error if we don't find it.
1237+
let packageGraph = try buildOperation.getPackageGraph()
1238+
guard let target = packageGraph.allTargets.first(where: { $0.name == targetName }) else {
1239+
throw StringError("could not find a target named “\(targetName)")
1240+
}
1241+
1242+
// Build the target, if needed.
1243+
try buildOperation.build(subset: .target(target.name))
1244+
1245+
// Configure the symbol graph extractor.
1246+
var symbolGraphExtractor = try SymbolGraphExtract(tool: swiftTool.getToolchain().getSymbolGraphExtract())
1247+
symbolGraphExtractor.skipSynthesizedMembers = !options.includeSynthesized
12261248
switch options.minimumAccessLevel {
12271249
case .private:
1228-
minimumAccessLevel = .private
1250+
symbolGraphExtractor.minimumAccessLevel = .private
12291251
case .fileprivate:
1230-
minimumAccessLevel = .fileprivate
1252+
symbolGraphExtractor.minimumAccessLevel = .fileprivate
12311253
case .internal:
1232-
minimumAccessLevel = .internal
1254+
symbolGraphExtractor.minimumAccessLevel = .internal
12331255
case .public:
1234-
minimumAccessLevel = .public
1256+
symbolGraphExtractor.minimumAccessLevel = .public
12351257
case .open:
1236-
minimumAccessLevel = .open
1258+
symbolGraphExtractor.minimumAccessLevel = .open
12371259
}
1238-
1239-
// Extract the symbol graph.
1240-
try symbolGraphExtract.dumpSymbolGraph(
1241-
buildPlan: buildOperation.buildPlan!,
1242-
prettyPrint: false,
1243-
skipSynthesisedMembers: !options.includeSynthesized,
1244-
minimumAccessLevel: minimumAccessLevel,
1245-
skipInheritedDocs: true,
1246-
includeSPISymbols: options.includeSPI)
1247-
1260+
symbolGraphExtractor.skipInheritedDocs = true
1261+
symbolGraphExtractor.includeSPISymbols = options.includeSPI
1262+
1263+
// Determine the output directory, and remove any old version if it already exists.
1264+
guard let buildPlan = buildOperation.buildPlan else {
1265+
throw StringError("could not get the build plan from the build operation")
1266+
}
1267+
guard let package = packageGraph.package(for: target) else {
1268+
throw StringError("could not determine the package for target “\(target.name)")
1269+
}
1270+
let outputDir = buildPlan.buildParameters.dataPath.appending(components: "extracted-symbols", package.identity.description, target.name)
1271+
try localFileSystem.removeFileTree(outputDir)
1272+
1273+
// Run the symbol graph extractor on the target.
1274+
try symbolGraphExtractor.extractSymbolGraph(
1275+
target: target,
1276+
buildPlan: buildPlan,
1277+
outputRedirection: .collect,
1278+
logLevel: .warning,
1279+
outputDirectory: outputDir)
1280+
12481281
// Return the results to the plugin.
1249-
return PluginInvocationSymbolGraphResult(
1250-
directoryPath: buildParameters.symbolGraph.pathString)
1282+
return PluginInvocationSymbolGraphResult(directoryPath: outputDir.pathString)
12511283
}
12521284
}
12531285

Lines changed: 59 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,88 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
4+
Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See http://swift.org/LICENSE.txt for license information
88
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

11-
import Foundation
12-
13-
import TSCBasic
14-
import TSCUtility
15-
16-
import SPMBuildCore
11+
import ArgumentParser
12+
import Basics
1713
import Build
1814
import PackageGraph
1915
import PackageModel
20-
import SourceControl
21-
import Workspace
22-
import ArgumentParser
16+
import SPMBuildCore
17+
import TSCBasic
18+
import TSCUtility
2319

2420
/// A wrapper for swift-symbolgraph-extract tool.
2521
public struct SymbolGraphExtract {
2622
let tool: AbsolutePath
27-
28-
init(tool: AbsolutePath) {
29-
self.tool = tool
23+
24+
var skipSynthesizedMembers = false
25+
var minimumAccessLevel = AccessLevel.public
26+
var skipInheritedDocs = false
27+
var includeSPISymbols = false
28+
var outputFormat = OutputFormat.json(pretty: false)
29+
30+
/// Access control levels.
31+
public enum AccessLevel: String, RawRepresentable, CaseIterable, ExpressibleByArgument {
32+
// The cases reflect those found in `include/swift/AST/AttrKind.h` of the swift compiler (at commit 03f55d7bb4204ca54841218eb7cc175ae798e3bd)
33+
case `private`, `fileprivate`, `internal`, `public`, `open`
3034
}
3135

32-
public func dumpSymbolGraph(
36+
/// Output format of the generated symbol graph.
37+
public enum OutputFormat {
38+
/// JSON format, optionally "pretty-printed" be more human-readable.
39+
case json(pretty: Bool)
40+
}
41+
42+
/// Creates a symbol graph for `target` in `outputDirectory` using the build information from `buildPlan`. The `outputDirection` determines how the output from the tool subprocess is handled, and `verbosity` specifies how much console output to ask the tool to emit.
43+
public func extractSymbolGraph(
44+
target: ResolvedTarget,
3345
buildPlan: BuildPlan,
34-
prettyPrint: Bool,
35-
skipSynthesisedMembers: Bool,
36-
minimumAccessLevel: AccessLevel,
37-
skipInheritedDocs: Bool,
38-
includeSPISymbols: Bool
46+
outputRedirection: Process.OutputRedirection = .none,
47+
logLevel: Basics.Diagnostic.Severity,
48+
outputDirectory: AbsolutePath
3949
) throws {
4050
let buildParameters = buildPlan.buildParameters
41-
let symbolGraphDirectory = buildPlan.buildParameters.symbolGraph
42-
try localFileSystem.createDirectory(symbolGraphDirectory, recursive: true)
43-
44-
// Run the tool for each target in the root package.
45-
let targets = buildPlan.graph.rootPackages.flatMap{ $0.targets }.filter{ $0.type == .library || $0.type == .executable }
46-
for target in targets {
47-
var args = [String]()
48-
args += ["-module-name", target.c99name]
49-
args += try buildParameters.targetTripleArgs(for: target)
50-
51-
args += buildPlan.createAPIToolCommonArgs(includeLibrarySearchPaths: true)
52-
args += ["-module-cache-path", buildParameters.moduleCache.pathString]
53-
54-
args += ["-output-dir", symbolGraphDirectory.pathString]
55-
56-
if prettyPrint { args.append("-pretty-print") }
57-
if skipSynthesisedMembers { args.append("-skip-synthesized-members") }
58-
if minimumAccessLevel != SwiftPackageTool.DumpSymbolGraph.defaultMinimumAccessLevel {
59-
args += ["-minimum-access-level", minimumAccessLevel.rawValue]
51+
try localFileSystem.createDirectory(outputDirectory, recursive: true)
52+
53+
// Construct arguments for extracting symbols for a single target.
54+
var commandLine = [self.tool.pathString]
55+
commandLine += ["-module-name", target.c99name]
56+
commandLine += try buildParameters.targetTripleArgs(for: target)
57+
commandLine += buildPlan.createAPIToolCommonArgs(includeLibrarySearchPaths: true)
58+
commandLine += ["-module-cache-path", buildParameters.moduleCache.pathString]
59+
if logLevel <= .info {
60+
commandLine += ["-v"]
61+
}
62+
commandLine += ["-minimum-access-level", minimumAccessLevel.rawValue]
63+
if skipSynthesizedMembers {
64+
commandLine += ["-skip-synthesized-members"]
65+
}
66+
if skipInheritedDocs {
67+
commandLine += ["-skip-inherited-docs"]
68+
}
69+
if includeSPISymbols {
70+
commandLine += ["-include-spi-symbols"]
71+
}
72+
switch outputFormat {
73+
case .json(let pretty):
74+
if pretty {
75+
commandLine += ["-pretty-print"]
6076
}
61-
if skipInheritedDocs { args.append("-skip-inherited-docs") }
62-
if includeSPISymbols { args.append("-include-spi-symbols") }
63-
64-
print("-- Emitting symbol graph for", target.name)
65-
try runTool(args)
6677
}
67-
print("Files written to", symbolGraphDirectory.pathString)
68-
}
78+
commandLine += ["-output-dir", outputDirectory.pathString]
6979

70-
func runTool(_ args: [String]) throws {
71-
let arguments = [tool.pathString] + args
80+
// Run the extraction.
7281
let process = Process(
73-
arguments: arguments,
74-
outputRedirection: .none,
75-
verbose: verbosity != .concise
76-
)
82+
arguments: commandLine,
83+
outputRedirection: outputRedirection,
84+
verbose: logLevel <= .info)
7785
try process.launch()
7886
try process.waitUntilExit()
7987
}
8088
}
81-
82-
/// Access control levels.
83-
public enum AccessLevel: String, RawRepresentable, CustomStringConvertible, CaseIterable {
84-
// The cases reflect those found in `include/swift/AST/AttrKind.h` of the swift compiler (at commit 03f55d7bb4204ca54841218eb7cc175ae798e3bd)
85-
case `private`, `fileprivate`, `internal`, `public`, `open`
86-
87-
public var description: String { rawValue }
88-
}
89-
90-
extension AccessLevel: ExpressibleByArgument {}
91-
92-
extension BuildParameters {
93-
/// The directory containing artifacts generated by the symbolgraph-extract tool.
94-
var symbolGraph: AbsolutePath {
95-
dataPath.appending(component: "symbolgraph")
96-
}
97-
}

Tests/CommandsTests/PackageToolTests.swift

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,4 +1442,90 @@ final class PackageToolTests: CommandsTestCase {
14421442
}
14431443
}
14441444
}
1445+
1446+
func testCommandPluginSymbolGraphCallbacks() throws {
1447+
// Depending on how the test is running, the `swift-symbolgraph-extract` tool might be unavailable.
1448+
try XCTSkipIf((try? UserToolchain.default.getSymbolGraphExtract()) == nil, "skipping test because the `swift-symbolgraph-extract` tools isn't available")
1449+
1450+
try testWithTemporaryDirectory { tmpPath in
1451+
// Create a sample package with a library, and executable, and a plugin.
1452+
let packageDir = tmpPath.appending(components: "MyPackage")
1453+
try localFileSystem.writeFileContents(packageDir.appending(components: "Package.swift")) {
1454+
$0 <<< """
1455+
// swift-tools-version: 5.6
1456+
import PackageDescription
1457+
let package = Package(
1458+
name: "MyPackage",
1459+
targets: [
1460+
.target(
1461+
name: "MyLibrary"
1462+
),
1463+
.executableTarget(
1464+
name: "MyCommand",
1465+
dependencies: ["MyLibrary"]
1466+
),
1467+
.plugin(
1468+
name: "MyPlugin",
1469+
capability: .command(
1470+
intent: .documentationGeneration()
1471+
)
1472+
),
1473+
]
1474+
)
1475+
"""
1476+
}
1477+
try localFileSystem.writeFileContents(packageDir.appending(components: "Sources", "MyLibrary", "library.swift")) {
1478+
$0 <<< """
1479+
public func GetGreeting() -> String { return "Hello" }
1480+
"""
1481+
}
1482+
try localFileSystem.writeFileContents(packageDir.appending(components: "Sources", "MyCommand", "main.swift")) {
1483+
$0 <<< """
1484+
import MyLibrary
1485+
print("\\(GetGreeting()), World!")
1486+
"""
1487+
}
1488+
try localFileSystem.writeFileContents(packageDir.appending(components: "Plugins", "MyPlugin", "plugin.swift")) {
1489+
$0 <<< """
1490+
import PackagePlugin
1491+
import Foundation
1492+
1493+
@main
1494+
struct MyCommandPlugin: CommandPlugin {
1495+
func performCommand(
1496+
context: PluginContext,
1497+
targets: [Target],
1498+
arguments: [String]
1499+
) throws {
1500+
// Ask for and print out the symbol graph directory for each target.
1501+
for target in targets {
1502+
let symbolGraph = try packageManager.getSymbolGraph(for: target,
1503+
options: .init(minimumAccessLevel: .public))
1504+
print("\\(target.name): \\(symbolGraph.directoryPath)")
1505+
}
1506+
}
1507+
}
1508+
"""
1509+
}
1510+
1511+
// 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.
1512+
do {
1513+
let result = try SwiftPMProduct.SwiftPackage.executeProcess(["generate-documentation"], packagePath: packageDir)
1514+
let output = try result.utf8Output() + result.utf8stderrOutput()
1515+
XCTAssertEqual(result.exitStatus, .terminated(code: 0), "output: \(output)")
1516+
XCTAssertMatch(output, .and(.contains("MyLibrary:"), .contains("mypackage/MyLibrary")))
1517+
XCTAssertMatch(output, .and(.contains("MyCommand:"), .contains("mypackage/MyCommand")))
1518+
1519+
}
1520+
1521+
// Check that if we pass a target, we successfully get symbol graph information for just the target we asked for.
1522+
do {
1523+
let result = try SwiftPMProduct.SwiftPackage.executeProcess(["--target", "MyLibrary", "generate-documentation"], packagePath: packageDir)
1524+
let output = try result.utf8Output() + result.utf8stderrOutput()
1525+
XCTAssertEqual(result.exitStatus, .terminated(code: 0), "output: \(output)")
1526+
XCTAssertMatch(output, .and(.contains("MyLibrary:"), .contains("mypackage/MyLibrary")))
1527+
XCTAssertNoMatch(output, .and(.contains("MyCommand:"), .contains("mypackage/MyCommand")))
1528+
}
1529+
}
1530+
}
14451531
}

0 commit comments

Comments
 (0)