-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Make the symbol graph extraction requested by plugins more robust #3993
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
777ff86
d40dba5
9dd86bb
fe64961
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,97 +1,88 @@ | ||
/* | ||
This source file is part of the Swift.org open source project | ||
|
||
Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors | ||
Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors | ||
Licensed under Apache License v2.0 with Runtime Library Exception | ||
|
||
See http://swift.org/LICENSE.txt for license information | ||
See http://swift.org/CONTRIBUTORS.txt for Swift project authors | ||
*/ | ||
|
||
import Foundation | ||
|
||
import TSCBasic | ||
import TSCUtility | ||
|
||
import SPMBuildCore | ||
import ArgumentParser | ||
import Basics | ||
import Build | ||
import PackageGraph | ||
import PackageModel | ||
import SourceControl | ||
import Workspace | ||
import ArgumentParser | ||
import SPMBuildCore | ||
import TSCBasic | ||
import TSCUtility | ||
|
||
/// A wrapper for swift-symbolgraph-extract tool. | ||
public struct SymbolGraphExtract { | ||
let tool: AbsolutePath | ||
|
||
init(tool: AbsolutePath) { | ||
self.tool = tool | ||
|
||
var skipSynthesizedMembers = false | ||
var minimumAccessLevel = AccessLevel.public | ||
var skipInheritedDocs = false | ||
var includeSPISymbols = false | ||
var outputFormat = OutputFormat.json(pretty: false) | ||
|
||
/// Access control levels. | ||
public enum AccessLevel: String, RawRepresentable, CaseIterable, ExpressibleByArgument { | ||
// The cases reflect those found in `include/swift/AST/AttrKind.h` of the swift compiler (at commit 03f55d7bb4204ca54841218eb7cc175ae798e3bd) | ||
case `private`, `fileprivate`, `internal`, `public`, `open` | ||
} | ||
|
||
public func dumpSymbolGraph( | ||
/// Output format of the generated symbol graph. | ||
public enum OutputFormat { | ||
/// JSON format, optionally "pretty-printed" be more human-readable. | ||
case json(pretty: Bool) | ||
} | ||
|
||
/// 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. | ||
public func extractSymbolGraph( | ||
target: ResolvedTarget, | ||
buildPlan: BuildPlan, | ||
prettyPrint: Bool, | ||
skipSynthesisedMembers: Bool, | ||
minimumAccessLevel: AccessLevel, | ||
skipInheritedDocs: Bool, | ||
includeSPISymbols: Bool | ||
outputRedirection: Process.OutputRedirection = .none, | ||
logLevel: Basics.Diagnostic.Severity, | ||
outputDirectory: AbsolutePath | ||
) throws { | ||
let buildParameters = buildPlan.buildParameters | ||
let symbolGraphDirectory = buildPlan.buildParameters.symbolGraph | ||
try localFileSystem.createDirectory(symbolGraphDirectory, recursive: true) | ||
|
||
// Run the tool for each target in the root package. | ||
let targets = buildPlan.graph.rootPackages.flatMap{ $0.targets }.filter{ $0.type == .library || $0.type == .executable } | ||
for target in targets { | ||
var args = [String]() | ||
args += ["-module-name", target.c99name] | ||
args += try buildParameters.targetTripleArgs(for: target) | ||
|
||
args += buildPlan.createAPIToolCommonArgs(includeLibrarySearchPaths: true) | ||
args += ["-module-cache-path", buildParameters.moduleCache.pathString] | ||
|
||
args += ["-output-dir", symbolGraphDirectory.pathString] | ||
|
||
if prettyPrint { args.append("-pretty-print") } | ||
if skipSynthesisedMembers { args.append("-skip-synthesized-members") } | ||
if minimumAccessLevel != SwiftPackageTool.DumpSymbolGraph.defaultMinimumAccessLevel { | ||
args += ["-minimum-access-level", minimumAccessLevel.rawValue] | ||
try localFileSystem.createDirectory(outputDirectory, recursive: true) | ||
|
||
// Construct arguments for extracting symbols for a single target. | ||
var commandLine = [self.tool.pathString] | ||
commandLine += ["-module-name", target.c99name] | ||
commandLine += try buildParameters.targetTripleArgs(for: target) | ||
commandLine += buildPlan.createAPIToolCommonArgs(includeLibrarySearchPaths: true) | ||
commandLine += ["-module-cache-path", buildParameters.moduleCache.pathString] | ||
if logLevel <= .info { | ||
commandLine += ["-v"] | ||
} | ||
commandLine += ["-minimum-access-level", minimumAccessLevel.rawValue] | ||
if skipSynthesizedMembers { | ||
commandLine += ["-skip-synthesized-members"] | ||
} | ||
if skipInheritedDocs { | ||
commandLine += ["-skip-inherited-docs"] | ||
} | ||
if includeSPISymbols { | ||
commandLine += ["-include-spi-symbols"] | ||
} | ||
switch outputFormat { | ||
case .json(let pretty): | ||
if pretty { | ||
commandLine += ["-pretty-print"] | ||
} | ||
if skipInheritedDocs { args.append("-skip-inherited-docs") } | ||
if includeSPISymbols { args.append("-include-spi-symbols") } | ||
|
||
print("-- Emitting symbol graph for", target.name) | ||
try runTool(args) | ||
} | ||
print("Files written to", symbolGraphDirectory.pathString) | ||
} | ||
commandLine += ["-output-dir", outputDirectory.pathString] | ||
|
||
func runTool(_ args: [String]) throws { | ||
let arguments = [tool.pathString] + args | ||
// Run the extraction. | ||
let process = Process( | ||
arguments: arguments, | ||
outputRedirection: .none, | ||
verbose: verbosity != .concise | ||
) | ||
arguments: commandLine, | ||
outputRedirection: outputRedirection, | ||
verbose: logLevel <= .info) | ||
try process.launch() | ||
try process.waitUntilExit() | ||
} | ||
} | ||
|
||
/// Access control levels. | ||
public enum AccessLevel: String, RawRepresentable, CustomStringConvertible, CaseIterable { | ||
// The cases reflect those found in `include/swift/AST/AttrKind.h` of the swift compiler (at commit 03f55d7bb4204ca54841218eb7cc175ae798e3bd) | ||
case `private`, `fileprivate`, `internal`, `public`, `open` | ||
|
||
public var description: String { rawValue } | ||
} | ||
|
||
extension AccessLevel: ExpressibleByArgument {} | ||
|
||
extension BuildParameters { | ||
/// The directory containing artifacts generated by the symbolgraph-extract tool. | ||
var symbolGraph: AbsolutePath { | ||
dataPath.appending(component: "symbolgraph") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1442,4 +1442,90 @@ final class PackageToolTests: CommandsTestCase { | |
} | ||
} | ||
} | ||
|
||
func testCommandPluginSymbolGraphCallbacks() 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 testWithTemporaryDirectory { tmpPath in | ||
// Create a sample package with a library, and executable, and a plugin. | ||
let packageDir = tmpPath.appending(components: "MyPackage") | ||
try localFileSystem.writeFileContents(packageDir.appending(components: "Package.swift")) { | ||
$0 <<< """ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can now pass string directly, o need to go through the byte stream There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great, I didn't realize. All the other tests use the stream, but I can change this one over. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There seems to be a subtle difference, where for the block-and-stream version, the parent directory of the output file is automatically created, but for the others (string, bytes, etc) it's not. I'm inclined to leave this as is just in the interesting of getting more bugs fixed and then deal with all the tests in a separate PR, possibly also modifying the other calls to at least optionally create the parent directories of the output file. |
||
// swift-tools-version: 5.6 | ||
import PackageDescription | ||
let package = Package( | ||
name: "MyPackage", | ||
targets: [ | ||
.target( | ||
name: "MyLibrary" | ||
), | ||
.executableTarget( | ||
name: "MyCommand", | ||
dependencies: ["MyLibrary"] | ||
), | ||
.plugin( | ||
name: "MyPlugin", | ||
capability: .command( | ||
intent: .documentationGeneration() | ||
) | ||
), | ||
] | ||
) | ||
""" | ||
} | ||
try localFileSystem.writeFileContents(packageDir.appending(components: "Sources", "MyLibrary", "library.swift")) { | ||
$0 <<< """ | ||
public func GetGreeting() -> String { return "Hello" } | ||
""" | ||
} | ||
try localFileSystem.writeFileContents(packageDir.appending(components: "Sources", "MyCommand", "main.swift")) { | ||
$0 <<< """ | ||
import MyLibrary | ||
print("\\(GetGreeting()), World!") | ||
""" | ||
} | ||
try localFileSystem.writeFileContents(packageDir.appending(components: "Plugins", "MyPlugin", "plugin.swift")) { | ||
$0 <<< """ | ||
import PackagePlugin | ||
import Foundation | ||
|
||
@main | ||
struct MyCommandPlugin: CommandPlugin { | ||
func performCommand( | ||
context: PluginContext, | ||
targets: [Target], | ||
arguments: [String] | ||
) throws { | ||
// Ask for and print out the symbol graph directory for each target. | ||
for target in targets { | ||
let symbolGraph = try packageManager.getSymbolGraph(for: target, | ||
options: .init(minimumAccessLevel: .public)) | ||
print("\\(target.name): \\(symbolGraph.directoryPath)") | ||
} | ||
} | ||
} | ||
""" | ||
} | ||
|
||
// 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 result = try SwiftPMProduct.SwiftPackage.executeProcess(["generate-documentation"], packagePath: packageDir) | ||
let output = try result.utf8Output() + result.utf8stderrOutput() | ||
XCTAssertEqual(result.exitStatus, .terminated(code: 0), "output: \(output)") | ||
XCTAssertMatch(output, .and(.contains("MyLibrary:"), .contains("mypackage/MyLibrary"))) | ||
XCTAssertMatch(output, .and(.contains("MyCommand:"), .contains("mypackage/MyCommand"))) | ||
|
||
} | ||
|
||
// Check that if we pass a target, we successfully get symbol graph information for just the target we asked for. | ||
do { | ||
let result = try SwiftPMProduct.SwiftPackage.executeProcess(["--target", "MyLibrary", "generate-documentation"], packagePath: packageDir) | ||
let output = try result.utf8Output() + result.utf8stderrOutput() | ||
XCTAssertEqual(result.exitStatus, .terminated(code: 0), "output: \(output)") | ||
XCTAssertMatch(output, .and(.contains("MyLibrary:"), .contains("mypackage/MyLibrary"))) | ||
XCTAssertNoMatch(output, .and(.contains("MyCommand:"), .contains("mypackage/MyCommand"))) | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this support abstract file system instead of assuming local?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the tool only works locally there didn't seem to be much point, and could be misleading since only the local file system will work.