Skip to content

Commit 54cafa6

Browse files
committed
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 Also add a unit test to check these things. rdar://87234110&86785765
1 parent b73889f commit 54cafa6

File tree

2 files changed

+122
-23
lines changed

2 files changed

+122
-23
lines changed

Sources/Commands/SwiftPackageTool.swift

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,37 +1229,55 @@ final class PluginDelegate: PluginInvocationDelegate {
12291229
}
12301230

12311231
private func createSymbolGraph(forTarget targetName: String, options: PluginInvocationSymbolGraphOptions) throws -> PluginInvocationSymbolGraphResult {
1232-
// Current implementation uses `SymbolGraphExtract()` but we can probably do better in the future.
1233-
let buildParameters = try swiftTool.buildParameters()
1234-
let buildOperation = try swiftTool.createBuildOperation(cacheBuildManifest: false) // We only get a build plan if we don't cache
1235-
try buildOperation.build(subset: .target(targetName))
1236-
let symbolGraphExtract = try SymbolGraphExtract(tool: swiftTool.getToolchain().getSymbolGraphExtract())
1237-
let minimumAccessLevel: AccessLevel
1232+
// Current implementation uses `SymbolGraphExtract()` but in the future we should emit the symbol graph while building.
1233+
1234+
// Create a build operation for building the target., skipping the the cache because we need the build plan.
1235+
let buildOperation = try swiftTool.createBuildOperation(cacheBuildManifest: false)
1236+
1237+
// Find the target in the build operation's package graph; it's an error if we don't find it.
1238+
let packageGraph = try buildOperation.getPackageGraph()
1239+
guard let target = packageGraph.allTargets.first(where: { $0.name == targetName }) else {
1240+
throw StringError("could not find a target named “\(targetName)")
1241+
}
1242+
1243+
// Build the target, if needed.
1244+
try buildOperation.build(subset: .target(target.name))
1245+
1246+
// Configure the symbol graph extractor.
1247+
var symbolGraphExtractor = try SymbolGraphExtract(tool: swiftTool.getToolchain().getSymbolGraphExtract())
1248+
symbolGraphExtractor.skipSynthesizedMembers = !options.includeSynthesized
12381249
switch options.minimumAccessLevel {
12391250
case .private:
1240-
minimumAccessLevel = .private
1251+
symbolGraphExtractor.minimumAccessLevel = .private
12411252
case .fileprivate:
1242-
minimumAccessLevel = .fileprivate
1253+
symbolGraphExtractor.minimumAccessLevel = .fileprivate
12431254
case .internal:
1244-
minimumAccessLevel = .internal
1255+
symbolGraphExtractor.minimumAccessLevel = .internal
12451256
case .public:
1246-
minimumAccessLevel = .public
1257+
symbolGraphExtractor.minimumAccessLevel = .public
12471258
case .open:
1248-
minimumAccessLevel = .open
1259+
symbolGraphExtractor.minimumAccessLevel = .open
12491260
}
1250-
1251-
// Extract the symbol graph.
1252-
try symbolGraphExtract.dumpSymbolGraph(
1253-
buildPlan: buildOperation.buildPlan!,
1254-
prettyPrint: false,
1255-
skipSynthesisedMembers: !options.includeSynthesized,
1256-
minimumAccessLevel: minimumAccessLevel,
1257-
skipInheritedDocs: true,
1258-
includeSPISymbols: options.includeSPI)
1259-
1261+
symbolGraphExtractor.skipInheritedDocs = true
1262+
symbolGraphExtractor.includeSPISymbols = options.includeSPI
1263+
1264+
// Run the symbol graph extractor on the target.
1265+
guard let buildPlan = buildOperation.buildPlan else {
1266+
throw StringError("could not get the build plan from the build operation")
1267+
}
1268+
guard let package = packageGraph.package(for: target) else {
1269+
throw StringError("could not determine the package for target “\(target.name)")
1270+
}
1271+
let outputDir = buildPlan.buildParameters.dataPath.appending(components: "extracted-symbols", package.identity.description, target.name)
1272+
try symbolGraphExtractor.extractSymbolGraph(
1273+
target: target,
1274+
buildPlan: buildPlan,
1275+
outputRedirection: .collect,
1276+
verbose: false,
1277+
outputDirectory: outputDir)
1278+
12601279
// Return the results to the plugin.
1261-
return PluginInvocationSymbolGraphResult(
1262-
directoryPath: buildParameters.symbolGraph.pathString)
1280+
return PluginInvocationSymbolGraphResult(directoryPath: outputDir.pathString)
12631281
}
12641282
}
12651283

Tests/CommandsTests/PackageToolTests.swift

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

0 commit comments

Comments
 (0)