Skip to content

Commit c98af5b

Browse files
committed
[Dependency Scanning] Add ability to use the scanner's import-prescan mode
An ability to do a simple import pre-scan was added to the dependency scanner in swiftlang/swift#34028, with a corresponding libSwiftScan API added later. This PR adds the ability to invoke this import pre-scan from the driver.
1 parent faf9128 commit c98af5b

File tree

6 files changed

+177
-36
lines changed

6 files changed

+177
-36
lines changed

Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/InterModuleDependencyGraph.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,7 @@ public struct InterModuleDependencyGraph: Codable {
266266
/// Information about the main module.
267267
public var mainModule: ModuleInfo { modules[.swift(mainModuleName)]! }
268268
}
269+
270+
public struct InterModuleDependencyImports: Codable {
271+
public var imports: [String]
272+
}

Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/InterModuleDependencyOracle.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ public class InterModuleDependencyOracle {
5252
}
5353
}
5454

55+
@_spi(Testing) public func getImports(workingDirectory: AbsolutePath,
56+
commandLine: [String])
57+
throws -> InterModuleDependencyImports {
58+
precondition(hasScannerInstance)
59+
return try queue.sync {
60+
return try swiftScanLibInstance!.preScanImports(workingDirectory: workingDirectory,
61+
invocationCommand: commandLine)
62+
}
63+
}
64+
5565
/// Given a specified toolchain path, locate and instantiate an instance of the SwiftScan library
5666
/// Returns True if a library instance exists (either verified or newly-created).
5767
@_spi(Testing) public func verifyOrCreateScannerInstance(fileSystem: FileSystem,

Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift

Lines changed: 80 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,8 @@ internal extension Driver {
9494
contents)
9595
}
9696

97-
mutating func performDependencyScan() throws -> InterModuleDependencyGraph {
98-
let scannerJob = try dependencyScanningJob()
99-
let forceResponseFiles = parsedOptions.hasArgument(.driverForceResponseFiles)
100-
let dependencyGraph: InterModuleDependencyGraph
101-
97+
/// Returns false if the lib is available and ready to use
98+
private func initSwiftScanLib() throws -> Bool {
10299
// If `-nonlib-dependency-scanner` was specified or the libSwiftScan library cannot be found,
103100
// attempt to fallback to using `swift-frontend -scan-dependencies` invocations for dependency
104101
// scanning.
@@ -110,20 +107,58 @@ internal extension Driver {
110107
fallbackToFrontend = true
111108
diagnosticEngine.emit(.warn_scanner_frontend_fallback())
112109
}
110+
return fallbackToFrontend
111+
}
112+
113+
private func sanitizeCommandForLibScanInvocation(_ command: inout [String]) {
114+
// Remove the tool executable to only leave the arguments
115+
command.removeFirst()
116+
// We generate full swiftc -frontend -scan-dependencies invocations in order to also be
117+
// able to launch them as standalone jobs. Frontend's argument parser won't recognize
118+
// -frontend when passed directly.
119+
if command.first == "-frontend" {
120+
command.removeFirst()
121+
}
122+
}
123+
124+
mutating func performImportPrescan() throws -> InterModuleDependencyImports {
125+
let preScanJob = try importPreScanningJob()
126+
let forceResponseFiles = parsedOptions.hasArgument(.driverForceResponseFiles)
127+
let imports: InterModuleDependencyImports
128+
129+
if !(try initSwiftScanLib()) {
130+
let cwd = workingDirectory ?? fileSystem.currentWorkingDirectory!
131+
var command = try itemizedJobCommand(of: preScanJob,
132+
forceResponseFiles: forceResponseFiles,
133+
using: executor.resolver)
134+
sanitizeCommandForLibScanInvocation(&command)
135+
imports =
136+
try interModuleDependencyOracle.getImports(workingDirectory: cwd,
137+
commandLine: command)
138+
139+
} else {
140+
// Fallback to legacy invocation of the dependency scanner with
141+
// `swift-frontend -scan-dependencies -import-prescan`
142+
imports =
143+
try self.executor.execute(job: preScanJob,
144+
capturingJSONOutputAs: InterModuleDependencyImports.self,
145+
forceResponseFiles: forceResponseFiles,
146+
recordedInputModificationDates: recordedInputModificationDates)
147+
}
148+
return imports
149+
}
150+
151+
mutating func performDependencyScan() throws -> InterModuleDependencyGraph {
152+
let scannerJob = try dependencyScanningJob()
153+
let forceResponseFiles = parsedOptions.hasArgument(.driverForceResponseFiles)
154+
let dependencyGraph: InterModuleDependencyGraph
113155

114-
if (!fallbackToFrontend) {
156+
if !(try initSwiftScanLib()) {
115157
let cwd = workingDirectory ?? fileSystem.currentWorkingDirectory!
116158
var command = try itemizedJobCommand(of: scannerJob,
117159
forceResponseFiles: forceResponseFiles,
118160
using: executor.resolver)
119-
// Remove the tool executable to only leave the arguments
120-
command.removeFirst()
121-
// We generate full swiftc -frontend -scan-dependencies invocations in order to also be
122-
// able to launch them as standalone jobs. Frontend's argument parser won't recognize
123-
// -frontend when passed directly.
124-
if command.first == "-frontend" {
125-
command.removeFirst()
126-
}
161+
sanitizeCommandForLibScanInvocation(&command)
127162
dependencyGraph =
128163
try interModuleDependencyOracle.getDependencies(workingDirectory: cwd,
129164
commandLine: command)
@@ -143,33 +178,14 @@ internal extension Driver {
143178
throws -> [ModuleDependencyId: [InterModuleDependencyGraph]] {
144179
let batchScanningJob = try batchDependencyScanningJob(for: moduleInfos)
145180
let forceResponseFiles = parsedOptions.hasArgument(.driverForceResponseFiles)
146-
147-
// If `-nonlib-dependency-scanner` was specified or the libSwiftScan library cannot be found,
148-
// attempt to fallback to using `swift-frontend -scan-dependencies` invocations for dependency
149-
// scanning.
150-
var fallbackToFrontend = parsedOptions.hasArgument(.driverScanDependenciesNonLib)
151-
let scanLibPath = try Self.getScanLibPath(of: toolchain, hostTriple: hostTriple, env: env)
152-
if try interModuleDependencyOracle
153-
.verifyOrCreateScannerInstance(fileSystem: fileSystem,
154-
swiftScanLibPath: scanLibPath) == false {
155-
fallbackToFrontend = true
156-
diagnosticEngine.emit(.warn_scanner_frontend_fallback())
157-
}
158-
159181
let moduleVersionedGraphMap: [ModuleDependencyId: [InterModuleDependencyGraph]]
160-
if (!fallbackToFrontend) {
182+
183+
if !(try initSwiftScanLib()) {
161184
let cwd = workingDirectory ?? fileSystem.currentWorkingDirectory!
162185
var command = try itemizedJobCommand(of: batchScanningJob,
163186
forceResponseFiles: forceResponseFiles,
164187
using: executor.resolver)
165-
// Remove the tool executable to only leave the arguments
166-
command.removeFirst()
167-
// We generate full swiftc -frontend -scan-dependencies invocations in order to also be
168-
// able to launch them as standalone jobs. Frontend's argument parser won't recognize
169-
// -frontend when passed directly.
170-
if command.first == "-frontend" {
171-
command.removeFirst()
172-
}
188+
sanitizeCommandForLibScanInvocation(&command)
173189
moduleVersionedGraphMap =
174190
try interModuleDependencyOracle.getBatchDependencies(workingDirectory: cwd,
175191
commandLine: command,
@@ -226,6 +242,34 @@ internal extension Driver {
226242
return moduleVersionedGraphMap
227243
}
228244

245+
/// Precompute the set of module names as imported by the current module
246+
mutating func importPreScanningJob() throws -> Job {
247+
// Aggregate the fast dependency scanner arguments
248+
var inputs: [TypedVirtualPath] = []
249+
var commandLine: [Job.ArgTemplate] = swiftCompilerPrefixArgs.map { Job.ArgTemplate.flag($0) }
250+
commandLine.appendFlag("-frontend")
251+
commandLine.appendFlag("-scan-dependencies")
252+
commandLine.appendFlag("-import-prescan")
253+
try addCommonFrontendOptions(commandLine: &commandLine, inputs: &inputs,
254+
bridgingHeaderHandling: .precompiled,
255+
moduleDependencyGraphUse: .dependencyScan)
256+
// FIXME: MSVC runtime flags
257+
258+
// Pass on the input files
259+
commandLine.append(contentsOf: inputFiles.map { .path($0.file) })
260+
261+
// Construct the scanning job.
262+
return Job(moduleName: moduleOutputInfo.name,
263+
kind: .scanDependencies,
264+
tool: VirtualPath.absolute(try toolchain.getToolPath(.swiftCompiler)),
265+
commandLine: commandLine,
266+
displayInputs: inputs,
267+
inputs: inputs,
268+
primaryInputs: [],
269+
outputs: [TypedVirtualPath(file: .standardOutput, type: .jsonDependencies)],
270+
supportsResponseFiles: true)
271+
}
272+
229273
/// Precompute the dependencies for a given collection of modules using swift frontend's batch scanning mode
230274
mutating func batchDependencyScanningJob(for moduleInfos: [BatchScanModuleInfo]) throws -> Job {
231275
var inputs: [TypedVirtualPath] = []

Sources/SwiftDriver/SwiftScan/DependencyGraphBuilder.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ internal extension SwiftScan {
4242
return resultGraph
4343
}
4444

45+
/// From a reference to a binary-format set of module imports return by libSwiftScan pre-scan query,
46+
/// construct an instance of an `InterModuleDependencyImports` set
47+
func constructImportSet(from importSetRef: swiftscan_import_set_t) throws
48+
-> InterModuleDependencyImports {
49+
guard let importsRef = api.swiftscan_import_set_get_imports(importSetRef) else {
50+
throw DependencyScanningError.missingField("import_set.imports")
51+
}
52+
return InterModuleDependencyImports(imports: try toSwiftStringArray(importsRef.pointee))
53+
}
54+
4555
/// From a reference to a binary-format dependency graph collection returned by libSwiftScan batch scan query,
4656
/// corresponding to the specified batch scan input (`BatchScanModuleInfo`), construct instances of
4757
/// `InterModuleDependencyGraph` for each result.

Sources/SwiftDriver/SwiftScan/SwiftScan.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,33 @@ internal final class SwiftScan {
9292
dylib.leak()
9393
}
9494

95+
func preScanImports(workingDirectory: AbsolutePath,
96+
invocationCommand: [String]) throws -> InterModuleDependencyImports {
97+
// Create and configure the scanner invocation
98+
let invocation = api.swiftscan_scan_invocation_create()
99+
defer { api.swiftscan_scan_invocation_dispose(invocation) }
100+
api.swiftscan_scan_invocation_set_working_directory(invocation,
101+
workingDirectory
102+
.description
103+
.cString(using: String.Encoding.utf8))
104+
withArrayOfCStrings(invocationCommand) { invocationStringArray in
105+
api.swiftscan_scan_invocation_set_argv(invocation,
106+
Int32(invocationCommand.count),
107+
invocationStringArray)
108+
}
109+
110+
let importSetRefOrNull = api.swiftscan_import_set_create(scanner, invocation)
111+
guard let importSetRef = importSetRefOrNull else {
112+
throw DependencyScanningError.dependencyScanFailed
113+
}
114+
115+
let importSet = try constructImportSet(from: importSetRef)
116+
// Free the memory allocated for the in-memory representation of the import set
117+
// returned by the scanner, now that we have translated it.
118+
api.swiftscan_import_set_dispose(importSetRef)
119+
return importSet
120+
}
121+
95122
func scanDependencies(workingDirectory: AbsolutePath,
96123
invocationCommand: [String]) throws -> InterModuleDependencyGraph {
97124
// Create and configure the scanner invocation

Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,52 @@ final class ExplicitModuleBuildTests: XCTestCase {
658658
return (stdLibPath, shimsPath, driver.toolchain, driver.hostTriple)
659659
}
660660

661+
/// Test the libSwiftScan dependency scanning (import-prescan).
662+
func testDependencyImportPrescan() throws {
663+
let (stdLibPath, shimsPath, toolchain, hostTriple) = try getDriverArtifactsForScanning()
664+
665+
// The dependency oracle wraps an instance of libSwiftScan and ensures thread safety across
666+
// queries.
667+
let dependencyOracle = InterModuleDependencyOracle()
668+
let scanLibPath = try Driver.getScanLibPath(of: toolchain,
669+
hostTriple: hostTriple,
670+
env: ProcessEnv.vars)
671+
guard try dependencyOracle
672+
.verifyOrCreateScannerInstance(fileSystem: localFileSystem,
673+
swiftScanLibPath: scanLibPath) else {
674+
XCTFail("Dependency scanner library not found")
675+
return
676+
}
677+
678+
// Create a simple test case.
679+
try withTemporaryDirectory { path in
680+
let main = path.appending(component: "testDependencyScanning.swift")
681+
try localFileSystem.writeFileContents(main) {
682+
$0 <<< "import C;"
683+
$0 <<< "import E;"
684+
$0 <<< "import G;"
685+
}
686+
let packageRootPath = URL(fileURLWithPath: #file).pathComponents
687+
.prefix(while: { $0 != "Tests" }).joined(separator: "/").dropFirst()
688+
let testInputsPath = packageRootPath + "/TestInputs"
689+
let cHeadersPath : String = testInputsPath + "/ExplicitModuleBuilds/CHeaders"
690+
let swiftModuleInterfacesPath : String = testInputsPath + "/ExplicitModuleBuilds/Swift"
691+
let scannerCommand = ["-scan-dependencies",
692+
"-import-prescan",
693+
"-I", cHeadersPath,
694+
"-I", swiftModuleInterfacesPath,
695+
"-I", stdLibPath.description,
696+
"-I", shimsPath.description,
697+
main.pathString]
698+
699+
let imports =
700+
try! dependencyOracle.getImports(workingDirectory: path,
701+
commandLine: scannerCommand)
702+
let expectedImports = ["C", "E", "G", "Swift", "SwiftOnoneSupport"]
703+
XCTAssertEqual(Set(imports.imports), Set(expectedImports))
704+
}
705+
}
706+
661707
/// Test the libSwiftScan dependency scanning.
662708
func testDependencyScanning() throws {
663709
let (stdLibPath, shimsPath, toolchain, hostTriple) = try getDriverArtifactsForScanning()

0 commit comments

Comments
 (0)