diff --git a/Sources/CSwiftScan/include/swiftscan_header.h b/Sources/CSwiftScan/include/swiftscan_header.h index 36c075baa..3505cd8db 100644 --- a/Sources/CSwiftScan/include/swiftscan_header.h +++ b/Sources/CSwiftScan/include/swiftscan_header.h @@ -193,6 +193,8 @@ typedef struct { //=== Cleanup Functions ---------------------------------------------------===// void + (*swiftscan_string_dispose)(swiftscan_string_ref_t); + void (*swiftscan_string_set_dispose)(swiftscan_string_set_t *); void (*swiftscan_dependency_graph_dispose)(swiftscan_dependency_graph_t); @@ -207,6 +209,11 @@ typedef struct { void (*swiftscan_scan_invocation_dispose)(swiftscan_scan_invocation_t); + //=== Target Info Functions-------- ---------------------------------------===// + swiftscan_string_ref_t + (*swiftscan_compiler_target_info_query_v2)(swiftscan_scan_invocation_t, + const char *); + //=== Functionality Query Functions ---------------------------------------===// swiftscan_string_set_t * (*swiftscan_compiler_supported_arguments_query)(void); diff --git a/Sources/SwiftDriver/Driver/Driver.swift b/Sources/SwiftDriver/Driver/Driver.swift index 07e77a9d8..131e321c1 100644 --- a/Sources/SwiftDriver/Driver/Driver.swift +++ b/Sources/SwiftDriver/Driver/Driver.swift @@ -137,7 +137,7 @@ public struct Driver { public let integratedDriver: Bool /// The file system which we should interact with. - let fileSystem: FileSystem + @_spi(Testing) public let fileSystem: FileSystem /// Diagnostic engine for emitting warnings, errors, etc. public let diagnosticEngine: DiagnosticsEngine @@ -522,12 +522,15 @@ public struct Driver { compilerMode: self.compilerMode, env: env, executor: self.executor, fileSystem: fileSystem, useStaticResourceDir: self.useStaticResourceDir, + workingDirectory: self.workingDirectory, compilerExecutableDir: compilerExecutableDir) // Compute the host machine's triple self.hostTriple = try Self.computeHostTriple(&self.parsedOptions, diagnosticsEngine: diagnosticEngine, toolchain: self.toolchain, executor: self.executor, + fileSystem: fileSystem, + workingDirectory: self.workingDirectory, swiftCompilerPrefixArgs: self.swiftCompilerPrefixArgs) // Classify and collect all of the input files. @@ -2896,18 +2899,18 @@ extension Driver { diagnosticsEngine: DiagnosticsEngine, toolchain: Toolchain, executor: DriverExecutor, + fileSystem: FileSystem, + workingDirectory: AbsolutePath?, swiftCompilerPrefixArgs: [String]) throws -> Triple { let frontendOverride = try FrontendOverride(&parsedOptions, diagnosticsEngine) frontendOverride.setUpForTargetInfo(toolchain) defer { frontendOverride.setUpForCompilation(toolchain) } - return try executor.execute( - job: toolchain.printTargetInfoJob(target: nil, targetVariant: nil, - swiftCompilerPrefixArgs: - frontendOverride.prefixArgsForTargetInfo), - capturingJSONOutputAs: FrontendTargetInfo.self, - forceResponseFiles: false, - recordedInputModificationDates: [:]).target.triple + return try Self.computeTargetInfo(target: nil, targetVariant: nil, + swiftCompilerPrefixArgs: frontendOverride.prefixArgsForTargetInfo, + toolchain: toolchain, fileSystem: fileSystem, + workingDirectory: workingDirectory, + executor: executor).target.triple } static func computeToolchain( @@ -2918,6 +2921,7 @@ extension Driver { executor: DriverExecutor, fileSystem: FileSystem, useStaticResourceDir: Bool, + workingDirectory: AbsolutePath?, compilerExecutableDir: AbsolutePath? ) throws -> (Toolchain, FrontendTargetInfo, [String]) { let explicitTarget = (parsedOptions.getLastArgument(.target)?.asSingle) @@ -2961,18 +2965,16 @@ extension Driver { // Query the frontend for target information. do { - var info: FrontendTargetInfo = try executor.execute( - job: toolchain.printTargetInfoJob( - target: explicitTarget, targetVariant: explicitTargetVariant, - sdkPath: sdkPath, resourceDirPath: resourceDirPath, - runtimeCompatibilityVersion: - parsedOptions.getLastArgument(.runtimeCompatibilityVersion)?.asSingle, - useStaticResourceDir: useStaticResourceDir, - swiftCompilerPrefixArgs: frontendOverride.prefixArgsForTargetInfo - ), - capturingJSONOutputAs: FrontendTargetInfo.self, - forceResponseFiles: false, - recordedInputModificationDates: [:]) + var info: FrontendTargetInfo = + try Self.computeTargetInfo(target: explicitTarget, targetVariant: explicitTargetVariant, + sdkPath: sdkPath, resourceDirPath: resourceDirPath, + runtimeCompatibilityVersion: + parsedOptions.getLastArgument(.runtimeCompatibilityVersion)?.asSingle, + useStaticResourceDir: useStaticResourceDir, + swiftCompilerPrefixArgs: frontendOverride.prefixArgsForTargetInfo, + toolchain: toolchain, fileSystem: fileSystem, + workingDirectory: workingDirectory, + executor: executor) // Parse the runtime compatibility version. If present, it will override // what is reported by the frontend. diff --git a/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift b/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift index 622c7e079..9d1e5681e 100644 --- a/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift +++ b/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift @@ -236,7 +236,7 @@ public extension Driver { return fallbackToFrontend } - private func sanitizeCommandForLibScanInvocation(_ command: inout [String]) { + static func sanitizeCommandForLibScanInvocation(_ command: inout [String]) { // Remove the tool executable to only leave the arguments. When passing the // command line into libSwiftScan, the library is itself the tool and only // needs to parse the remaining arguments. @@ -257,10 +257,10 @@ public extension Driver { let isSwiftScanLibAvailable = !(try initSwiftScanLib()) if isSwiftScanLibAvailable { let cwd = workingDirectory ?? fileSystem.currentWorkingDirectory! - var command = try itemizedJobCommand(of: preScanJob, - useResponseFiles: .disabled, - using: executor.resolver) - sanitizeCommandForLibScanInvocation(&command) + var command = try Self.itemizedJobCommand(of: preScanJob, + useResponseFiles: .disabled, + using: executor.resolver) + Self.sanitizeCommandForLibScanInvocation(&command) imports = try interModuleDependencyOracle.getImports(workingDirectory: cwd, moduleAliases: moduleOutputInfo.aliases, @@ -293,10 +293,10 @@ public extension Driver { let isSwiftScanLibAvailable = !(try initSwiftScanLib()) if isSwiftScanLibAvailable { let cwd = workingDirectory ?? fileSystem.currentWorkingDirectory! - var command = try itemizedJobCommand(of: scannerJob, - useResponseFiles: .disabled, - using: executor.resolver) - sanitizeCommandForLibScanInvocation(&command) + var command = try Self.itemizedJobCommand(of: scannerJob, + useResponseFiles: .disabled, + using: executor.resolver) + Self.sanitizeCommandForLibScanInvocation(&command) dependencyGraph = try interModuleDependencyOracle.getDependencies(workingDirectory: cwd, moduleAliases: moduleOutputInfo.aliases, @@ -339,10 +339,10 @@ public extension Driver { let isSwiftScanLibAvailable = !(try initSwiftScanLib()) if isSwiftScanLibAvailable { let cwd = workingDirectory ?? fileSystem.currentWorkingDirectory! - var command = try itemizedJobCommand(of: batchScanningJob, - useResponseFiles: .disabled, - using: executor.resolver) - sanitizeCommandForLibScanInvocation(&command) + var command = try Self.itemizedJobCommand(of: batchScanningJob, + useResponseFiles: .disabled, + using: executor.resolver) + Self.sanitizeCommandForLibScanInvocation(&command) moduleVersionedGraphMap = try interModuleDependencyOracle.getBatchDependencies(workingDirectory: cwd, moduleAliases: moduleOutputInfo.aliases, @@ -485,8 +485,8 @@ public extension Driver { contents) } - fileprivate func itemizedJobCommand(of job: Job, useResponseFiles: ResponseFileHandling, - using resolver: ArgsResolver) throws -> [String] { + static func itemizedJobCommand(of job: Job, useResponseFiles: ResponseFileHandling, + using resolver: ArgsResolver) throws -> [String] { let (args, _) = try resolver.resolveArgumentList(for: job, useResponseFiles: useResponseFiles) return args diff --git a/Sources/SwiftDriver/Jobs/PrintTargetInfoJob.swift b/Sources/SwiftDriver/Jobs/PrintTargetInfoJob.swift index a5ff3723b..a903175aa 100644 --- a/Sources/SwiftDriver/Jobs/PrintTargetInfoJob.swift +++ b/Sources/SwiftDriver/Jobs/PrintTargetInfoJob.swift @@ -10,6 +10,10 @@ // //===----------------------------------------------------------------------===// +import protocol TSCBasic.FileSystem +import class Foundation.JSONDecoder +import struct TSCBasic.AbsolutePath + /// Swift versions are major.minor. struct SwiftVersion { var major: Int @@ -173,3 +177,85 @@ extension Toolchain { ) } } + +extension Driver { + @_spi(Testing) public static func queryTargetInfoInProcess(of toolchain: Toolchain, + fileSystem: FileSystem, + workingDirectory: AbsolutePath?, + invocationCommand: [String]) throws -> FrontendTargetInfo? { + let optionalSwiftScanLibPath = try toolchain.lookupSwiftScanLib() + if let swiftScanLibPath = optionalSwiftScanLibPath, + fileSystem.exists(swiftScanLibPath) { + let libSwiftScanInstance = try SwiftScan(dylib: swiftScanLibPath) + if libSwiftScanInstance.canQueryTargetInfo() { + let cwd = try workingDirectory ?? fileSystem.currentWorkingDirectory ?? fileSystem.tempDirectory + let compilerExecutablePath = try toolchain.resolvedTool(.swiftCompiler).path + let targetInfoData = + try libSwiftScanInstance.queryTargetInfoJSON(workingDirectory: cwd, + compilerExecutablePath: compilerExecutablePath, + invocationCommand: invocationCommand) + do { + return try JSONDecoder().decode(FrontendTargetInfo.self, from: targetInfoData) + } catch let decodingError as DecodingError { + let stringToDecode = String(data: targetInfoData, encoding: .utf8) + let errorDesc: String + switch decodingError { + case let .typeMismatch(type, context): + errorDesc = "type mismatch: \(type), path: \(context.codingPath)" + case let .valueNotFound(type, context): + errorDesc = "value missing: \(type), path: \(context.codingPath)" + case let .keyNotFound(key, context): + errorDesc = "key missing: \(key), path: \(context.codingPath)" + case let .dataCorrupted(context): + errorDesc = "data corrupted at path: \(context.codingPath)" + @unknown default: + errorDesc = "unknown decoding error" + } + throw Error.unableToDecodeFrontendTargetInfo( + stringToDecode, + invocationCommand, + errorDesc) + } + } + } + return nil + } + + static func computeTargetInfo(target: Triple?, + targetVariant: Triple?, + sdkPath: VirtualPath? = nil, + resourceDirPath: VirtualPath? = nil, + runtimeCompatibilityVersion: String? = nil, + requiresInPlaceExecution: Bool = false, + useStaticResourceDir: Bool = false, + swiftCompilerPrefixArgs: [String], + toolchain: Toolchain, + fileSystem: FileSystem, + workingDirectory: AbsolutePath?, + executor: DriverExecutor) throws -> FrontendTargetInfo { + let frontendTargetInfoJob = + try toolchain.printTargetInfoJob(target: target, targetVariant: targetVariant, + sdkPath: sdkPath, resourceDirPath: resourceDirPath, + runtimeCompatibilityVersion: runtimeCompatibilityVersion, + requiresInPlaceExecution: requiresInPlaceExecution, + useStaticResourceDir: useStaticResourceDir, + swiftCompilerPrefixArgs: swiftCompilerPrefixArgs) + var command = try Self.itemizedJobCommand(of: frontendTargetInfoJob, + useResponseFiles: .disabled, + using: executor.resolver) + Self.sanitizeCommandForLibScanInvocation(&command) + if let targetInfo = + try Self.queryTargetInfoInProcess(of: toolchain, fileSystem: fileSystem, + workingDirectory: workingDirectory, + invocationCommand: command) { + return targetInfo + } + + // Fallback: Invoke `swift-frontend -print-target-info` and decode the output + return try executor.execute( + job: frontendTargetInfoJob, + capturingJSONOutputAs: FrontendTargetInfo.self, + forceResponseFiles: false, + recordedInputModificationDates: [:]) + } +} diff --git a/Sources/SwiftDriver/SwiftScan/SwiftScan.swift b/Sources/SwiftDriver/SwiftScan/SwiftScan.swift index f68479469..7e5d04fac 100644 --- a/Sources/SwiftDriver/SwiftScan/SwiftScan.swift +++ b/Sources/SwiftDriver/SwiftScan/SwiftScan.swift @@ -14,6 +14,8 @@ import func Foundation.strdup import func Foundation.free +import class Foundation.JSONDecoder +import struct Foundation.Data import protocol TSCBasic.DiagnosticData import struct TSCBasic.AbsolutePath @@ -80,7 +82,7 @@ internal extension swiftscan_diagnostic_severity_t { } /// Wrapper for libSwiftScan, taking care of initialization, shutdown, and dispatching dependency scanning queries. -internal final class SwiftScan { +@_spi(Testing) public final class SwiftScan { /// The path to the libSwiftScan dylib. let path: AbsolutePath @@ -269,6 +271,10 @@ internal final class SwiftScan { api.swiftscan_diagnostic_get_severity != nil && api.swiftscan_diagnostics_set_dispose != nil } + + @_spi(Testing) public func supportsStringDispose() -> Bool { + return api.swiftscan_string_dispose != nil + } @_spi(Testing) public func queryScannerDiagnostics() throws -> [ScannerDiagnosticPayload] { var result: [ScannerDiagnosticPayload] = [] @@ -311,6 +317,37 @@ internal final class SwiftScan { throw DependencyScanningError.argumentQueryFailed } } + + @_spi(Testing) public func canQueryTargetInfo() -> Bool { + return api.swiftscan_compiler_target_info_query_v2 != nil && + api.swiftscan_string_set_dispose != nil + } + + func queryTargetInfoJSON(workingDirectory: AbsolutePath, + compilerExecutablePath: AbsolutePath, + invocationCommand: [String]) throws -> Data { + // Create and configure the scanner invocation + let invocation = api.swiftscan_scan_invocation_create() + defer { api.swiftscan_scan_invocation_dispose(invocation) } + api.swiftscan_scan_invocation_set_working_directory(invocation, + workingDirectory + .description + .cString(using: String.Encoding.utf8)) + withArrayOfCStrings(invocationCommand) { invocationStringArray in + api.swiftscan_scan_invocation_set_argv(invocation, + Int32(invocationCommand.count), + invocationStringArray) + } + + let targetInfoString: String = try compilerExecutablePath.description.withCString { cstring in + let targetInfoStringRef = api.swiftscan_compiler_target_info_query_v2(invocation, cstring) + defer { api.swiftscan_string_dispose(targetInfoStringRef) } + return try toSwiftString(targetInfoStringRef) + } + + let targetInfoData = Data(targetInfoString.utf8) + return targetInfoData + } } // Used for testing purposes only @@ -347,6 +384,10 @@ private extension swiftscan_functions_t { self.swiftscan_compiler_supported_features_query = try loadOptional("swiftscan_compiler_supported_features_query") + // Target Info query + self.swiftscan_compiler_target_info_query_v2 = + try loadOptional("swiftscan_compiler_target_info_query_v2") + // Dependency scanner serialization/deserialization features self.swiftscan_scanner_cache_serialize = try loadOptional("swiftscan_scanner_cache_serialize") @@ -370,6 +411,8 @@ private extension swiftscan_functions_t { try loadOptional("swiftscan_diagnostic_get_severity") self.swiftscan_diagnostics_set_dispose = try loadOptional("swiftscan_diagnostics_set_dispose") + self.swiftscan_string_dispose = + try loadOptional("swiftscan_string_dispose") // isFramework on binary module dependencies self.swiftscan_swift_binary_detail_get_is_framework = diff --git a/Tests/SwiftDriverTests/SwiftDriverTests.swift b/Tests/SwiftDriverTests/SwiftDriverTests.swift index b5018dd58..9bb204bbd 100644 --- a/Tests/SwiftDriverTests/SwiftDriverTests.swift +++ b/Tests/SwiftDriverTests/SwiftDriverTests.swift @@ -4781,6 +4781,21 @@ final class SwiftDriverTests: XCTestCase { XCTAssertTrue(job.commandLine.contains(.flag("-resource-dir"))) } + // In-process query + do { + let targetInfoArgs = ["-print-target-info", "-sdk", "bar", "-resource-dir", "baz"] + let driver = try Driver(args: ["swift"] + targetInfoArgs) + let swiftScanLibPath = try XCTUnwrap(driver.toolchain.lookupSwiftScanLib()) + if localFileSystem.exists(swiftScanLibPath) { + let libSwiftScanInstance = try SwiftScan(dylib: swiftScanLibPath) + if libSwiftScanInstance.canQueryTargetInfo() { + XCTAssertTrue(try driver.verifyBeingAbleToQueryTargetInfoInProcess(workingDirectory: localFileSystem.currentWorkingDirectory, + invocationCommand: targetInfoArgs)) + } + } + + } + do { struct MockExecutor: DriverExecutor { let resolver: ArgsResolver @@ -4803,7 +4818,11 @@ final class SwiftDriverTests: XCTestCase { } } + // Override path to libSwiftScan to force the fallback of using the executor + var hideSwiftScanEnv = ProcessEnv.vars + hideSwiftScanEnv["SWIFT_DRIVER_SWIFTSCAN_LIB"] = "/bad/path/lib_InternalSwiftScan.dylib" XCTAssertThrowsError(try Driver(args: ["swift", "-print-target-info"], + env: hideSwiftScanEnv, executor: MockExecutor(resolver: ArgsResolver(fileSystem: InMemoryFileSystem())))) { error in if case .unableToDecodeFrontendTargetInfo = error as? Driver.Error {} diff --git a/Tests/TestUtilities/DriverExtensions.swift b/Tests/TestUtilities/DriverExtensions.swift index 595b2365e..9d9d18cfa 100644 --- a/Tests/TestUtilities/DriverExtensions.swift +++ b/Tests/TestUtilities/DriverExtensions.swift @@ -42,6 +42,18 @@ extension Driver { public static func sdkArgumentsForTesting() throws -> [String]? { try cachedSDKPath.map {["-sdk", try $0.get()]} } + + + public func verifyBeingAbleToQueryTargetInfoInProcess(workingDirectory: AbsolutePath?, + invocationCommand: [String]) throws -> Bool { + guard try Self.queryTargetInfoInProcess(of: toolchain, + fileSystem: fileSystem, + workingDirectory: workingDirectory, + invocationCommand: invocationCommand) != nil else { + return false + } + return true + } } /// Set to nil if cannot perform on this host