diff --git a/README.md b/README.md index 5d0c6edc4..bd35ddb05 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ $ apt-get install libncurses-dev be found, e.g.: ``` -$ swift build -Xcc -Xcc -I/path/to/build/Ninja-Release/swift-.../include -Xcc -I/path/to/build/Ninja-Release/llvm-.../include -Xcc -I/path/to/source/llvm-project/llvm/include --product makeOptions +$ swift build -Xcc -I/path/to/build/Ninja-Release/swift-.../include -Xcc -I/path/to/build/Ninja-Release/llvm-.../include -Xcc -I/path/to/source/llvm-project/llvm/include --product makeOptions ``` Then, run `makeOptions` and redirect the output to overwrite `Options.swift`: diff --git a/Sources/CSwiftScan/include/swiftscan_header.h b/Sources/CSwiftScan/include/swiftscan_header.h index 79ea98bd4..63bf16913 100644 --- a/Sources/CSwiftScan/include/swiftscan_header.h +++ b/Sources/CSwiftScan/include/swiftscan_header.h @@ -18,7 +18,7 @@ #include #define SWIFTSCAN_VERSION_MAJOR 0 -#define SWIFTSCAN_VERSION_MINOR 5 +#define SWIFTSCAN_VERSION_MINOR 6 //=== Public Scanner Data Types -------------------------------------------===// @@ -78,17 +78,15 @@ typedef struct swiftscan_scan_invocation_s *swiftscan_scan_invocation_t; typedef void *swiftscan_scanner_t; //=== CAS/Caching Specification -------------------------------------------===// -typedef struct swiftscan_cas_s *swiftscan_cas_t; typedef struct swiftscan_cas_options_s *swiftscan_cas_options_t; - -typedef enum { - SWIFTSCAN_OUTPUT_TYPE_OBJECT = 0, - SWIFTSCAN_OUTPUT_TYPE_SWIFTMODULE = 1, - SWIFTSCAN_OUTPUT_TYPE_SWIFTINTERFACE = 2, - SWIFTSCAN_OUTPUT_TYPE_SWIFTPRIVATEINTERFACE = 3, - SWIFTSCAN_OUTPUT_TYPE_CLANG_MODULE = 4, - SWIFTSCAN_OUTPUT_TYPE_CLANG_PCH = 5 -} swiftscan_output_kind_t; +typedef struct swiftscan_cas_s *swiftscan_cas_t; +typedef struct swiftscan_cached_compilation_s *swiftscan_cached_compilation_t; +typedef struct swiftscan_cached_output_s *swiftscan_cached_output_t; +typedef struct swiftscan_cache_replay_instance_s + *swiftscan_cache_replay_instance_t; +typedef struct swiftscan_cache_replay_result_s *swiftscan_cache_replay_result_t; +typedef struct swiftscan_cache_cancellation_token_s + *swiftscan_cache_cancellation_token_t; //=== libSwiftScan Functions ------------------------------------------------===// @@ -281,18 +279,74 @@ typedef struct { const char *path); void (*swiftscan_cas_options_set_plugin_path)(swiftscan_cas_options_t options, const char *path); - bool (*swiftscan_cas_options_set_option)(swiftscan_cas_options_t options, - const char *name, const char *value, - swiftscan_string_ref_t *error); + bool (*swiftscan_cas_options_set_plugin_option)( + swiftscan_cas_options_t options, const char *name, const char *value, + swiftscan_string_ref_t *error); swiftscan_cas_t (*swiftscan_cas_create_from_options)( swiftscan_cas_options_t options, swiftscan_string_ref_t *error); void (*swiftscan_cas_dispose)(swiftscan_cas_t cas); swiftscan_string_ref_t (*swiftscan_cas_store)(swiftscan_cas_t cas, uint8_t *data, unsigned size, swiftscan_string_ref_t *error); - swiftscan_string_ref_t (*swiftscan_compute_cache_key)( - swiftscan_cas_t cas, int argc, const char *argv, const char *input, - swiftscan_output_kind_t, swiftscan_string_ref_t *error); + swiftscan_string_ref_t (*swiftscan_cache_compute_key)( + swiftscan_cas_t cas, int argc, const char **argv, const char *input, + swiftscan_string_ref_t *error); + + //=== Scanner Caching Query/Replay Operations -----------------------------===// + swiftscan_cached_compilation_t (*swiftscan_cache_query)( + swiftscan_cas_t cas, const char *key, bool globally, + swiftscan_string_ref_t *error); + void (*swiftscan_cache_query_async)( + swiftscan_cas_t cas, const char *key, bool globally, void *ctx, + void (*callback)(void *ctx, swiftscan_cached_compilation_t, + swiftscan_string_ref_t error), + swiftscan_cache_cancellation_token_t *); + + + unsigned (*swiftscan_cached_compilation_get_num_outputs)( + swiftscan_cached_compilation_t); + swiftscan_cached_output_t (*swiftscan_cached_compilation_get_output)( + swiftscan_cached_compilation_t, unsigned idx); + bool (*swiftscan_cached_compilation_is_uncacheable)( + swiftscan_cached_compilation_t); + void (*swiftscan_cached_compilation_make_global_async)( + swiftscan_cached_compilation_t, void *ctx, + void (*callback)(void *ctx, swiftscan_string_ref_t error), + swiftscan_cache_cancellation_token_t *); + void (*swiftscan_cached_compilation_dispose)(swiftscan_cached_compilation_t); + + bool (*swiftscan_cached_output_load)(swiftscan_cached_output_t, + swiftscan_string_ref_t *error); + void (*swiftscan_cached_output_load_async)( + swiftscan_cached_output_t, void *ctx, + void (*callback)(void *ctx, bool success, swiftscan_string_ref_t error), + swiftscan_cache_cancellation_token_t *); + bool (*swiftscan_cached_output_is_materialized)(swiftscan_cached_output_t); + swiftscan_string_ref_t (*swiftscan_cached_output_get_casid)( + swiftscan_cached_output_t); + swiftscan_string_ref_t (*swiftscan_cached_output_get_name)( + swiftscan_cached_output_t); + void (*swiftscan_cached_output_dispose)(swiftscan_cached_output_t); + + void (*swiftscan_cache_action_cancel)(swiftscan_cache_cancellation_token_t); + void (*swiftscan_cache_cancellation_token_dispose)( + swiftscan_cache_cancellation_token_t); + + swiftscan_cache_replay_instance_t (*swiftscan_cache_replay_instance_create)( + int argc, const char **argv, swiftscan_string_ref_t *error); + void (*swiftscan_cache_replay_instance_dispose)( + swiftscan_cache_replay_instance_t); + + swiftscan_cache_replay_result_t (*swiftscan_cache_replay_compilation)( + swiftscan_cache_replay_instance_t, swiftscan_cached_compilation_t, + swiftscan_string_ref_t *error); + + swiftscan_string_ref_t (*swiftscan_cache_replay_result_get_stdout)( + swiftscan_cache_replay_result_t); + swiftscan_string_ref_t (*swiftscan_cache_replay_result_get_stderr)( + swiftscan_cache_replay_result_t); + void (*swiftscan_cache_replay_result_dispose)( + swiftscan_cache_replay_result_t); } swiftscan_functions_t; diff --git a/Sources/SwiftDriver/CMakeLists.txt b/Sources/SwiftDriver/CMakeLists.txt index d7785e768..2f9ad8344 100644 --- a/Sources/SwiftDriver/CMakeLists.txt +++ b/Sources/SwiftDriver/CMakeLists.txt @@ -18,6 +18,7 @@ add_library(SwiftDriver SwiftScan/DependencyGraphBuilder.swift SwiftScan/Loader.swift SwiftScan/SwiftScan.swift + SwiftScan/SwiftScanCAS.swift Driver/CompilerMode.swift Driver/DebugInfo.swift diff --git a/Sources/SwiftDriver/Driver/Driver.swift b/Sources/SwiftDriver/Driver/Driver.swift index d0eee5354..e6a5eee6e 100644 --- a/Sources/SwiftDriver/Driver/Driver.swift +++ b/Sources/SwiftDriver/Driver/Driver.swift @@ -224,8 +224,10 @@ public struct Driver { /// Should use file lists for inputs (number of inputs exceeds `fileListThreshold`). let shouldUseInputFileList: Bool - /// VirtualPath for shared all sources file list. `nil` if unused. - @_spi(Testing) public let allSourcesFileList: VirtualPath? + /// VirtualPath for shared all sources file list. `nil` if unused. This is used as a cache for + /// the file list computed during CompileJob creation and only holds valid to be query by tests + /// after planning to build. + @_spi(Testing) public var allSourcesFileList: VirtualPath? = nil /// The mode in which the compiler will execute. @_spi(Testing) public let compilerMode: CompilerMode @@ -272,6 +274,43 @@ public struct Driver { let enableCaching: Bool let useClangIncludeTree: Bool + /// CAS instance used for compilation. + public var cas: SwiftScanCAS? = nil + + /// Is swift caching enabled. + lazy var isCachingEnabled: Bool = { + return enableCaching && isFeatureSupported(.cache_compile_job) + }() + + /// Scanner prefix mapping. + let scannerPrefixMap: [AbsolutePath: AbsolutePath] + let scannerPrefixMapSDK: AbsolutePath? + let scannerPrefixMapToolchain: AbsolutePath? + lazy var prefixMapping: [(AbsolutePath, AbsolutePath)] = { + var mapping: [(AbsolutePath, AbsolutePath)] = scannerPrefixMap.map { + return ($0.key, $0.value) + } + do { + guard isFrontendArgSupported(.scannerPrefixMap) else { + return [] + } + if let sdkMapping = scannerPrefixMapSDK, + let sdkPath = absoluteSDKPath { + mapping.append((sdkPath, sdkMapping)) + } + if let toolchainMapping = scannerPrefixMapToolchain { + let toolchainPath = try toolchain.executableDir.parentDirectory // usr + .parentDirectory // toolchain + mapping.append((toolchainPath, toolchainMapping)) + } + // The mapping needs to be sorted so the mapping is determinisitic. + // The sorting order is reversed so /tmp/tmp is preferred over /tmp in remapping. + return mapping.sorted { $0.0 > $1.0 } + } catch { + return mapping.sorted { $0.0 > $1.0 } + } + }() + /// Code & data for incremental compilation. Nil if not running in incremental mode. /// Set during planning because needs the jobs to look at outputs. @_spi(Testing) public private(set) var incrementalCompilationState: IncrementalCompilationState? = nil @@ -385,6 +424,7 @@ public struct Driver { @_spi(Testing) public enum KnownCompilerFeature: String { case emit_abi_descriptor = "emit-abi-descriptor" + case cache_compile_job = "cache-compile-job" } lazy var sdkPath: VirtualPath? = { @@ -597,7 +637,18 @@ public struct Driver { let cachingEnableOverride = parsedOptions.hasArgument(.driverExplicitModuleBuild) && env.keys.contains("SWIFT_ENABLE_CACHING") self.enableCaching = parsedOptions.hasArgument(.cacheCompileJob) || cachingEnableOverride - self.useClangIncludeTree = enableCaching && env.keys.contains("SWIFT_CACHING_USE_INCLUDE_TREE") + self.useClangIncludeTree = !parsedOptions.hasArgument(.noClangIncludeTree) && !env.keys.contains("SWIFT_CACHING_USE_CLANG_CAS_FS") + self.scannerPrefixMap = try Self.computeScanningPrefixMapper(&parsedOptions) + if let sdkMapping = parsedOptions.getLastArgument(.scannerPrefixMapSdk)?.asSingle { + self.scannerPrefixMapSDK = try AbsolutePath(validating: sdkMapping) + } else { + self.scannerPrefixMapSDK = nil + } + if let toolchainMapping = parsedOptions.getLastArgument(.scannerPrefixMapToolchain)?.asSingle { + self.scannerPrefixMapToolchain = try AbsolutePath(validating: toolchainMapping) + } else { + self.scannerPrefixMapToolchain = nil + } // Compute the working directory. workingDirectory = try parsedOptions.getLastArgument(.workingDirectory).map { workingDirectoryArg in @@ -678,13 +729,6 @@ public struct Driver { self.fileListThreshold = try Self.computeFileListThreshold(&self.parsedOptions, diagnosticsEngine: diagnosticsEngine) self.shouldUseInputFileList = inputFiles.count > fileListThreshold - if shouldUseInputFileList { - let swiftInputs = inputFiles.filter(\.type.isPartOfSwiftCompilation) - self.allSourcesFileList = try VirtualPath.createUniqueFilelist(RelativePath(validating: "sources"), - .list(swiftInputs.map(\.file))) - } else { - self.allSourcesFileList = nil - } self.lto = Self.ltoKind(&parsedOptions, diagnosticsEngine: diagnosticsEngine) // Figure out the primary outputs from the driver. @@ -3524,4 +3568,18 @@ extension Driver { } return options } + + static func computeScanningPrefixMapper(_ parsedOptions: inout ParsedOptions) throws -> [AbsolutePath: AbsolutePath] { + var mapping: [AbsolutePath: AbsolutePath] = [:] + for opt in parsedOptions.arguments(for: .scannerPrefixMap) { + let pluginArg = opt.argument.asSingle.split(separator: "=", maxSplits: 1) + if pluginArg.count != 2 { + throw Error.invalidArgumentValue(Option.scannerPrefixMap.spelling, opt.argument.asSingle) + } + let key = try AbsolutePath(validating: String(pluginArg[0])) + let value = try AbsolutePath(validating: String(pluginArg[1])) + mapping[key] = value + } + return mapping + } } diff --git a/Sources/SwiftDriver/Execution/ArgsResolver.swift b/Sources/SwiftDriver/Execution/ArgsResolver.swift index 79f9ff88d..6c8e73fbb 100644 --- a/Sources/SwiftDriver/Execution/ArgsResolver.swift +++ b/Sources/SwiftDriver/Execution/ArgsResolver.swift @@ -65,12 +65,16 @@ public final class ArgsResolver { public func resolveArgumentList(for job: Job, useResponseFiles: ResponseFileHandling = .heuristic) throws -> ([String], usingResponseFile: Bool) { let tool = try resolve(.path(job.tool)) - var arguments = [tool] + (try job.commandLine.map { try resolve($0) }) + var arguments = [tool] + (try resolveArgumentList(for: job.commandLine)) let usingResponseFile = try createResponseFileIfNeeded(for: job, resolvedArguments: &arguments, useResponseFiles: useResponseFiles) return (arguments, usingResponseFile) } + public func resolveArgumentList(for commandLine: [Job.ArgTemplate]) throws -> [String] { + return try commandLine.map { try resolve($0) } + } + @available(*, deprecated, message: "use resolveArgumentList(for:,useResponseFiles:,quotePaths:)") public func resolveArgumentList(for job: Job, forceResponseFiles: Bool, quotePaths: Bool = false) throws -> [String] { diff --git a/Sources/SwiftDriver/ExplicitModuleBuilds/ExplicitDependencyBuildPlanner.swift b/Sources/SwiftDriver/ExplicitModuleBuilds/ExplicitDependencyBuildPlanner.swift index 1e1728289..b54371946 100644 --- a/Sources/SwiftDriver/ExplicitModuleBuilds/ExplicitDependencyBuildPlanner.swift +++ b/Sources/SwiftDriver/ExplicitModuleBuilds/ExplicitDependencyBuildPlanner.swift @@ -45,7 +45,7 @@ public typealias ExternalTargetModuleDetailsMap = [ModuleDependencyId: ExternalT /// Whether we are using the integrated driver via libSwiftDriver shared lib private let integratedDriver: Bool private let mainModuleName: String? - private let enableCAS: Bool + private let cas: SwiftScanCAS? private let swiftScanOracle: InterModuleDependencyOracle /// Clang PCM names contain a hash of the command-line arguments that were used to build them. @@ -60,7 +60,7 @@ public typealias ExternalTargetModuleDetailsMap = [ModuleDependencyId: ExternalT dependencyOracle: InterModuleDependencyOracle, integratedDriver: Bool = true, supportsExplicitInterfaceBuild: Bool = false, - enableCAS: Bool = false) throws { + cas: SwiftScanCAS? = nil) throws { self.dependencyGraph = dependencyGraph self.toolchain = toolchain self.swiftScanOracle = dependencyOracle @@ -68,7 +68,7 @@ public typealias ExternalTargetModuleDetailsMap = [ModuleDependencyId: ExternalT self.mainModuleName = dependencyGraph.mainModuleName self.reachabilityMap = try dependencyGraph.computeTransitiveClosure() self.supportsExplicitInterfaceBuild = supportsExplicitInterfaceBuild - self.enableCAS = enableCAS + self.cas = cas } /// Supports resolving bridging header pch command from swiftScan. @@ -136,9 +136,6 @@ public typealias ExternalTargetModuleDetailsMap = [ModuleDependencyId: ExternalT for moduleId in swiftDependencies { let moduleInfo = try dependencyGraph.moduleInfo(of: moduleId) var inputs: [TypedVirtualPath] = [] - let outputs: [TypedVirtualPath] = [ - TypedVirtualPath(file: moduleInfo.modulePath.path, type: .swiftModule) - ] var commandLine: [Job.ArgTemplate] = [] // First, take the command line options provided in the dependency information let moduleDetails = try dependencyGraph.swiftModuleDetails(of: moduleId) @@ -155,9 +152,18 @@ public typealias ExternalTargetModuleDetailsMap = [ModuleDependencyId: ExternalT throw Driver.Error.malformedModuleDependency(moduleId.moduleName, "no `moduleInterfacePath` object") } - inputs.append(TypedVirtualPath(file: moduleInterfacePath.path, - type: .swiftInterface)) + let inputInterfacePath = TypedVirtualPath(file: moduleInterfacePath.path, type: .swiftInterface) + inputs.append(inputInterfacePath) + let outputModulePath = TypedVirtualPath(file: moduleInfo.modulePath.path, type: .swiftModule) + let outputs = [outputModulePath] + + let cacheKeys : [TypedVirtualPath : String] + if let key = moduleDetails.moduleCacheKey { + cacheKeys = [inputInterfacePath: key] + } else { + cacheKeys = [:] + } // Add precompiled module candidates, if present if let compiledCandidateList = moduleDetails.compiledModuleCandidates { for compiledCandidate in compiledCandidateList { @@ -173,7 +179,8 @@ public typealias ExternalTargetModuleDetailsMap = [ModuleDependencyId: ExternalT commandLine: commandLine, inputs: inputs, primaryInputs: [], - outputs: outputs + outputs: outputs, + outputCacheKeys: cacheKeys )) } return jobs @@ -205,15 +212,20 @@ public typealias ExternalTargetModuleDetailsMap = [ModuleDependencyId: ExternalT try resolveExplicitModuleDependencies(moduleId: moduleId, inputs: &inputs, commandLine: &commandLine) - let moduleMapPath = moduleDetails.moduleMapPath.path - let modulePCMPath = moduleInfo.modulePath - outputs.append(TypedVirtualPath(file: modulePCMPath.path, type: .pcm)) + let moduleMapPath = TypedVirtualPath(file: moduleDetails.moduleMapPath.path, type: .clangModuleMap) + let modulePCMPath = TypedVirtualPath(file: moduleInfo.modulePath.path, type: .pcm) + outputs.append(modulePCMPath) // The only required input is the .modulemap for this module. // Command line options in the dependency scanner output will include the // required modulemap, so here we must only add it to the list of inputs. - inputs.append(TypedVirtualPath(file: moduleMapPath, - type: .clangModuleMap)) + inputs.append(moduleMapPath) + let cacheKeys : [TypedVirtualPath : String] + if let key = moduleDetails.moduleCacheKey { + cacheKeys = [moduleMapPath: key] + } else { + cacheKeys = [:] + } jobs.append(Job( moduleName: moduleId.moduleName, @@ -222,7 +234,8 @@ public typealias ExternalTargetModuleDetailsMap = [ModuleDependencyId: ExternalT commandLine: commandLine, inputs: inputs, primaryInputs: [], - outputs: outputs + outputs: outputs, + outputCacheKeys: cacheKeys )) } return jobs @@ -247,7 +260,7 @@ public typealias ExternalTargetModuleDetailsMap = [ModuleDependencyId: ExternalT type: .swiftModule)) let prebuiltHeaderDependencyPaths = dependencyModule.prebuiltHeaderDependencyPaths ?? [] - if enableCAS && !prebuiltHeaderDependencyPaths.isEmpty { + if cas != nil && !prebuiltHeaderDependencyPaths.isEmpty { throw Driver.Error.unsupportedConfigurationForCaching("module \(dependencyModule.moduleName) has prebuilt header dependency") } @@ -277,9 +290,9 @@ public typealias ExternalTargetModuleDetailsMap = [ModuleDependencyId: ExternalT try serializeModuleDependencies(for: moduleId, swiftDependencyArtifacts: swiftDependencyArtifacts, clangDependencyArtifacts: clangDependencyArtifacts) - if enableCAS { + if let cas = self.cas { // When using a CAS, write JSON into CAS and pass the ID on command-line. - let casID = try swiftScanOracle.store(data: dependencyFileContent) + let casID = try cas.store(data: dependencyFileContent) commandLine.appendFlag("-explicit-swift-module-map-file") commandLine.appendFlag(casID) } else { @@ -445,7 +458,7 @@ public typealias ExternalTargetModuleDetailsMap = [ModuleDependencyId: ExternalT return } - assert(!enableCAS, "Caching build should always return command-line from scanner") + assert(cas == nil, "Caching build should always return command-line from scanner") // Prohibit the frontend from implicitly building textual modules into binary modules. commandLine.appendFlags("-disable-implicit-swift-modules", "-Xcc", "-fno-implicit-modules", diff --git a/Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/InterModuleDependencyOracle.swift b/Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/InterModuleDependencyOracle.swift index 6065ab71f..046426e5a 100644 --- a/Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/InterModuleDependencyOracle.swift +++ b/Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/InterModuleDependencyOracle.swift @@ -13,6 +13,7 @@ import protocol TSCBasic.FileSystem import struct TSCBasic.AbsolutePath import struct Foundation.Data +import var TSCBasic.localFileSystem import Dispatch @@ -145,13 +146,6 @@ public class InterModuleDependencyOracle { return swiftScan.supportsBinaryModuleHeaderDependencies } - @_spi(Testing) public func supportsCaching() throws -> Bool { - guard let swiftScan = swiftScanLibInstance else { - fatalError("Attempting to query supported scanner API with no scanner instance.") - } - return swiftScan.supportsCaching - } - @_spi(Testing) public func supportsBridgingHeaderPCHCommand() throws -> Bool { guard let swiftScan = swiftScanLibInstance else { fatalError("Attempting to query supported scanner API with no scanner instance.") @@ -171,26 +165,11 @@ public class InterModuleDependencyOracle { return diags.isEmpty ? nil : diags } - public func createCAS(pluginPath: AbsolutePath?, onDiskPath: AbsolutePath?, pluginOptions: [(String, String)]) throws { - guard let swiftScan = swiftScanLibInstance else { - fatalError("Attempting to reset scanner cache with no scanner instance.") - } - try swiftScan.createCAS(pluginPath: pluginPath?.pathString, onDiskPath: onDiskPath?.pathString, pluginOptions: pluginOptions) - } - - public func store(data: Data) throws -> String { - guard let swiftScan = swiftScanLibInstance else { - fatalError("Attempting to reset scanner cache with no scanner instance.") - } - return try swiftScan.store(data:data) - } - - public func computeCacheKeyForOutput(kind: FileType, commandLine: [Job.ArgTemplate], input: VirtualPath.Handle?) throws -> String { + @_spi(Testing) public func createCAS(pluginPath: AbsolutePath?, onDiskPath: AbsolutePath?, pluginOptions: [(String, String)]) throws -> SwiftScanCAS { guard let swiftScan = swiftScanLibInstance else { fatalError("Attempting to reset scanner cache with no scanner instance.") } - let inputPath = input?.description ?? "" - return try swiftScan.computeCacheKeyForOutput(kind: kind, commandLine: commandLine.stringArray, input: inputPath) + return try swiftScan.createCAS(pluginPath: pluginPath?.pathString, onDiskPath: onDiskPath?.pathString, pluginOptions: pluginOptions) } private var hasScannerInstance: Bool { self.swiftScanLibInstance != nil } diff --git a/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift b/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift index c0af39cb5..e23b1d878 100644 --- a/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift +++ b/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift @@ -119,11 +119,18 @@ public extension Driver { commandLine.appendPath(dependencyPlaceholderMapFile) } - try commandLine.appendLast(.clangIncludeTree, from: &parsedOptions) if isFrontendArgSupported(.clangScannerModuleCachePath) { try commandLine.appendLast(.clangScannerModuleCachePath, from: &parsedOptions) } + if isFrontendArgSupported(.scannerPrefixMap) { + // construct `-scanner-prefix-mapper` for scanner. + for (key, value) in prefixMapping { + commandLine.appendFlag(.scannerPrefixMap) + commandLine.appendFlag(key.pathString + "=" + value.pathString) + } + } + // Pass on the input files commandLine.append(contentsOf: inputFiles.filter { $0.type == .swift }.map { .path($0.file) }) return (inputs, commandLine) @@ -165,10 +172,10 @@ public extension Driver { diagnosticEngine.emit(.warn_scanner_frontend_fallback()) } } - if !fallbackToFrontend && enableCaching { - try interModuleDependencyOracle.createCAS(pluginPath: try getCASPluginPath(), - onDiskPath: try getOnDiskCASPath(), - pluginOptions: try getCASPluginOptions()) + if !fallbackToFrontend && isCachingEnabled { + self.cas = try interModuleDependencyOracle.createCAS(pluginPath: try getCASPluginPath(), + onDiskPath: try getOnDiskCASPath(), + pluginOptions: try getCASPluginOptions()) } return fallbackToFrontend } diff --git a/Sources/SwiftDriver/Jobs/CompileJob.swift b/Sources/SwiftDriver/Jobs/CompileJob.swift index 48763386a..c8e68efe5 100644 --- a/Sources/SwiftDriver/Jobs/CompileJob.swift +++ b/Sources/SwiftDriver/Jobs/CompileJob.swift @@ -97,7 +97,8 @@ extension Driver { .moduleTrace, .yamlOptimizationRecord, .bitstreamOptimizationRecord, .pcm, .pch, .clangModuleMap, .jsonCompilerFeatures, .jsonTargetInfo, .jsonSwiftArtifacts, .indexUnitOutputPath, .modDepCache, .jsonAPIBaseline, .jsonABIBaseline, - .swiftConstValues, .jsonAPIDescriptor, nil: + .swiftConstValues, .jsonAPIDescriptor, .moduleSummary, .moduleSemanticInfo, + .cachedDiagnostics, nil: return false } } @@ -112,13 +113,18 @@ extension Driver { outputType: FileType?, commandLine: inout [Job.ArgTemplate]) throws -> ([TypedVirtualPath], [TypedVirtualPath]) { - let useInputFileList: Bool - if let allSourcesFileList = allSourcesFileList { - useInputFileList = true + let useInputFileList = shouldUseInputFileList + if let sourcesFileList = allSourcesFileList { commandLine.appendFlag(.filelist) - commandLine.appendPath(allSourcesFileList) - } else { - useInputFileList = false + commandLine.appendPath(sourcesFileList) + } else if shouldUseInputFileList { + let swiftInputs = inputFiles.filter(\.type.isPartOfSwiftCompilation) + let remappedSourcesFileList = try VirtualPath.createUniqueFilelist(RelativePath(validating: "sources"), + .list(swiftInputs.map{ return remapPath($0.file) })) + // Remember the filelist created. + self.allSourcesFileList = remappedSourcesFileList + commandLine.appendFlag(.filelist) + commandLine.appendPath(remappedSourcesFileList) } let usePrimaryInputFileList = primaryInputs.count > fileListThreshold @@ -126,7 +132,7 @@ extension Driver { // primary file list commandLine.appendFlag(.primaryFilelist) let fileList = try VirtualPath.createUniqueFilelist(RelativePath(validating: "primaryInputs"), - .list(primaryInputs.map(\.file))) + .list(primaryInputs.map{ return remapPath($0.file) })) commandLine.appendPath(fileList) } @@ -166,12 +172,11 @@ extension Driver { let isPrimary = usesPrimaryFileInputs && primaryInputFiles.contains(input) if isPrimary { if !usePrimaryInputFileList { - commandLine.appendFlag(.primaryFile) - commandLine.appendPath(input.file) + try addPathOption(option: .primaryFile, path: input.file, to:&commandLine) } } else { if !useInputFileList { - commandLine.appendPath(input.file) + try addPathArgument(input.file, to: &commandLine) } } @@ -392,6 +397,9 @@ extension Driver { } else { displayInputs = primaryInputs } + // Only swift input files are contributing to the cache keys. + let cacheContributingInputs = displayInputs.filter() { $0.type == .swift } + let cacheKeys = try computeOutputCacheKeyForJob(commandLine: commandLine, inputs: cacheContributingInputs) return Job( moduleName: moduleOutputInfo.name, @@ -402,6 +410,7 @@ extension Driver { inputs: inputs, primaryInputs: primaryInputs, outputs: outputs, + outputCacheKeys: cacheKeys, inputOutputMap: inputOutputMap ) } @@ -464,7 +473,8 @@ extension FileType { .bitstreamOptimizationRecord, .swiftInterface, .privateSwiftInterface, .swiftSourceInfoFile, .clangModuleMap, .jsonSwiftArtifacts, .indexUnitOutputPath, .modDepCache, .jsonAPIBaseline, .jsonABIBaseline, - .swiftConstValues, .jsonAPIDescriptor: + .swiftConstValues, .jsonAPIDescriptor, .moduleSummary, .moduleSemanticInfo, + .cachedDiagnostics: fatalError("Output type can never be a primary output") } } diff --git a/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift b/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift index 7fc77d4ee..d26e69bb5 100644 --- a/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift +++ b/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift @@ -75,6 +75,7 @@ extension Driver { break } + let jobNeedPathRemap: Bool // If in ExplicitModuleBuild mode and the dependency graph has been computed, add module // dependencies. // May also be used for generation of the dependency graph itself in ExplicitModuleBuild mode. @@ -83,8 +84,10 @@ extension Driver { switch kind { case .generatePCH: try addExplicitPCHBuildArguments(inputs: &inputs, commandLine: &commandLine) + jobNeedPathRemap = true case .compile, .emitModule, .interpret, .verifyModuleInterface: try addExplicitModuleBuildArguments(inputs: &inputs, commandLine: &commandLine) + jobNeedPathRemap = true case .backend, .mergeModule, .compileModuleFromInterface, .generatePCM, .dumpPCM, .repl, .printTargetInfo, .versionRequest, .autolinkExtract, .generateDSYM, @@ -92,8 +95,10 @@ extension Driver { .emitSupportedFeatures, .moduleWrap, .generateAPIBaseline, .generateABIBaseline, .compareAPIBaseline, .compareABIBaseline: - break // Do not support creating from dependency scanner output. + jobNeedPathRemap = false } + } else { + jobNeedPathRemap = false } if let variant = parsedOptions.getLastArgument(.targetVariant)?.asSingle { @@ -145,24 +150,22 @@ extension Driver { try commandLine.appendLast(.targetCpu, from: &parsedOptions) if let sdkPath = frontendTargetInfo.sdkPath?.path { - commandLine.appendFlag(.sdk) - commandLine.append(.path(VirtualPath.lookup(sdkPath))) + try addPathOption(option: .sdk, path: VirtualPath.lookup(sdkPath), to: &commandLine, remap: jobNeedPathRemap) } for args: (Option, Option) in [ (.visualcToolsRoot, .visualcToolsVersion), (.windowsSdkRoot, .windowsSdkVersion) ] { - let (rootArg, versionArg) = args - if let value = parsedOptions.getLastArgument(rootArg)?.asSingle, - isFrontendArgSupported(rootArg) { - commandLine.appendFlag(rootArg.spelling) - commandLine.appendPath(try .init(validating: value)) + let (rootOpt, versionOpt) = args + if let rootArg = parsedOptions.last(for: rootOpt), + isFrontendArgSupported(rootOpt) { + try addPathOption(rootArg, to: &commandLine, remap: jobNeedPathRemap) } - if let value = parsedOptions.getLastArgument(versionArg)?.asSingle, - isFrontendArgSupported(versionArg) { - commandLine.appendFlags(versionArg.spelling, value) + if let value = parsedOptions.getLastArgument(versionOpt)?.asSingle, + isFrontendArgSupported(versionOpt) { + commandLine.appendFlags(versionOpt.spelling, value) } } @@ -315,19 +318,21 @@ extension Driver { try commandLine.appendLast(.enableBuiltinModule, from: &parsedOptions) } - if !useClangIncludeTree, let workingDirectory = workingDirectory { + if !(isCachingEnabled && useClangIncludeTree), let workingDirectory = workingDirectory { // Add -Xcc -working-directory before any other -Xcc options to ensure it is // overridden by an explicit -Xcc -working-directory, although having a // different working directory is probably incorrect. commandLine.appendFlag(.Xcc) commandLine.appendFlag(.workingDirectory) commandLine.appendFlag(.Xcc) - commandLine.appendPath(.absolute(workingDirectory)) + try addPathArgument(.absolute(workingDirectory), to: &commandLine, remap: jobNeedPathRemap) } // Resource directory. - commandLine.appendFlag(.resourceDir) - commandLine.appendPath(VirtualPath.lookup(frontendTargetInfo.runtimeResourcePath.path)) + try addPathOption(option: .resourceDir, + path: VirtualPath.lookup(frontendTargetInfo.runtimeResourcePath.path), + to: &commandLine, + remap: jobNeedPathRemap) if self.useStaticResourceDir { commandLine.appendFlag("-use-static-resource-dir") @@ -354,7 +359,7 @@ extension Driver { } // CAS related options. - if enableCaching { + if isCachingEnabled { commandLine.appendFlag(.cacheCompileJob) if let casPath = try getOnDiskCASPath() { commandLine.appendFlag(.casPath) @@ -366,15 +371,16 @@ extension Driver { } try commandLine.appendAll(.casPluginOption, from: &parsedOptions) try commandLine.appendLast(.cacheRemarks, from: &parsedOptions) + if !useClangIncludeTree { + commandLine.appendFlag(.noClangIncludeTree) + } } - if useClangIncludeTree { - commandLine.appendFlag(.clangIncludeTree) - } + addCacheReplayMapping(to: &commandLine) // Pass through any subsystem flags. try commandLine.appendAll(.Xllvm, from: &parsedOptions) - if !useClangIncludeTree { + if !(isCachingEnabled && useClangIncludeTree) { try commandLine.appendAll(.Xcc, from: &parsedOptions) } @@ -389,16 +395,16 @@ extension Driver { // of a lookup failure. if parsedOptions.contains(.pchOutputDir) && !parsedOptions.contains(.driverExplicitModuleBuild) { - commandLine.appendPath(VirtualPath.lookup(importedObjCHeader)) + try addPathArgument(VirtualPath.lookup(importedObjCHeader), to:&commandLine, remap: jobNeedPathRemap) try commandLine.appendLast(.pchOutputDir, from: &parsedOptions) if !compilerMode.isSingleCompilation { commandLine.appendFlag(.pchDisableValidation) } } else { - commandLine.appendPath(VirtualPath.lookup(pch)) + try addPathArgument(VirtualPath.lookup(pch), to:&commandLine, remap: jobNeedPathRemap) } } else { - commandLine.appendPath(VirtualPath.lookup(importedObjCHeader)) + try addPathArgument(VirtualPath.lookup(importedObjCHeader), to:&commandLine, remap: jobNeedPathRemap) } } @@ -438,18 +444,18 @@ extension Driver { } } - func addBridgingHeaderPCHCacheKeyArguments(commandLine: inout [Job.ArgTemplate], - pchCompileJob: Job?) throws { - guard let pchJob = pchCompileJob, enableCaching else { return } + mutating func addBridgingHeaderPCHCacheKeyArguments(commandLine: inout [Job.ArgTemplate], + pchCompileJob: Job?) throws { + guard let pchJob = pchCompileJob, isCachingEnabled else { return } // The pch input file (the bridging header) is added as last inputs to the job. guard let inputFile = pchJob.inputs.last else { assertionFailure("no input files from pch job"); return } assert(inputFile.type == .objcHeader, "Expect objc header input type") - let bridgingHeaderCacheKey = try interModuleDependencyOracle.computeCacheKeyForOutput(kind: .pch, - commandLine: pchJob.commandLine, - input: inputFile.fileHandle) + let bridgingHeaderCacheKey = try computeOutputCacheKey(commandLine: pchJob.commandLine, + input: inputFile) + guard let key = bridgingHeaderCacheKey else { return } commandLine.appendFlag("-bridging-header-pch-key") - commandLine.appendFlag(bridgingHeaderCacheKey) + commandLine.appendFlag(key) } mutating func addFrontendSupplementaryOutputArguments(commandLine: inout [Job.ArgTemplate], @@ -635,7 +641,7 @@ extension Driver { var entries = [VirtualPath.Handle: [FileType: VirtualPath.Handle]]() for input in primaryInputs { if let output = inputOutputMap[input]?.first { - addEntry(&entries, input: input, output: output) + try addEntry(&entries, input: input, output: output) } else { // Primary inputs are expected to appear in the output file map even // if they have no corresponding outputs. @@ -654,7 +660,7 @@ extension Driver { } for flaggedPair in flaggedInputOutputPairs { - addEntry(&entries, input: flaggedPair.input, output: flaggedPair.output) + try addEntry(&entries, input: flaggedPair.input, output: flaggedPair.output) } // To match the legacy driver behavior, make sure we add an entry for the // file under indexing and the primary output file path. @@ -688,7 +694,7 @@ extension Driver { try commandLine.appendLast(.symbolGraphMinimumAccessLevel, from: &parsedOptions) } - func addEntry(_ entries: inout [VirtualPath.Handle: [FileType: VirtualPath.Handle]], input: TypedVirtualPath?, output: TypedVirtualPath) { + mutating func addEntry(_ entries: inout [VirtualPath.Handle: [FileType: VirtualPath.Handle]], input: TypedVirtualPath?, output: TypedVirtualPath) throws { let entryInput: VirtualPath.Handle if let input = input?.fileHandle, input != OutputFileMap.singleInputKey { entryInput = input @@ -698,7 +704,8 @@ extension Driver { } entryInput = firstSourceInputHandle } - entries[entryInput, default: [:]][output.type] = output.fileHandle + let inputEntry = isCachingEnabled ? remapPath(VirtualPath.lookup(entryInput)).intern() : entryInput + entries[inputEntry, default: [:]][output.type] = output.fileHandle } /// Adds all dependencies required for an explicit module build @@ -735,3 +742,116 @@ extension Driver { return job.moduleName == moduleOutputInfo.name } } + +extension Driver { + private func getAbsolutePathFromVirtualPath(_ path: VirtualPath) -> AbsolutePath? { + guard let cwd = workingDirectory ?? fileSystem.currentWorkingDirectory else { + return nil + } + return path.resolvedRelativePath(base: cwd).absolutePath + } + + private mutating func remapPath(absolute path: AbsolutePath) -> AbsolutePath { + guard !prefixMapping.isEmpty else { + return path + } + for (prefix, value) in prefixMapping { + if path.isDescendantOfOrEqual(to: prefix) { + return value.appending(path.relative(to: prefix)) + } + } + return path + } + + public mutating func remapPath(_ path: VirtualPath) -> VirtualPath { + guard !prefixMapping.isEmpty, + let absPath = getAbsolutePathFromVirtualPath(path) else { + return path + } + let mappedPath = remapPath(absolute: absPath) + return try! VirtualPath(path: mappedPath.pathString) + } + + /// Helper function to add path to commandLine. Function will validate the path, and remap the path if needed. + public mutating func addPathArgument(_ path: VirtualPath, to commandLine: inout [Job.ArgTemplate], remap: Bool = true) throws { + guard remap && isCachingEnabled else { + commandLine.appendPath(path) + return + } + let mappedPath = remapPath(path) + commandLine.appendPath(mappedPath) + } + + public mutating func addPathOption(_ option: ParsedOption, to commandLine: inout [Job.ArgTemplate], remap: Bool = true) throws { + let path = try VirtualPath(path: option.argument.asSingle) + try addPathOption(option: option.option, path: path, to: &commandLine, remap: remap) + } + + public mutating func addPathOption(option: Option, path: VirtualPath, to commandLine: inout [Job.ArgTemplate], remap: Bool = true) throws { + commandLine.appendFlag(option) + let needRemap = remap && option.attributes.contains(.argumentIsPath) && + !option.attributes.contains(.cacheInvariant) + try addPathArgument(path, to: &commandLine, remap: needRemap) + } + + /// Helper function to add last argument with path to command-line. + public mutating func addLastArgumentWithPath(_ options: Option..., + from parsedOptions: inout ParsedOptions, + to commandLine: inout [Job.ArgTemplate], + remap: Bool = true) throws { + guard let parsedOption = parsedOptions.last(for: options) else { + return + } + try addPathOption(parsedOption, to: &commandLine, remap: remap) + } + + /// Helper function to add all arguments with path to command-line. + public mutating func addAllArgumentsWithPath(_ options: Option..., + from parsedOptions: inout ParsedOptions, + to commandLine: inout [Job.ArgTemplate], + remap: Bool) throws { + for matching in parsedOptions.arguments(for: options) { + try addPathOption(matching, to: &commandLine, remap: remap) + } + } + + public mutating func addCacheReplayMapping(to commandLine: inout [Job.ArgTemplate]) { + if isCachingEnabled && isFrontendArgSupported(.scannerPrefixMap) { + for (key, value) in prefixMapping { + commandLine.appendFlag("-cache-replay-prefix-map") + commandLine.appendFlag(value.pathString + "=" + key.pathString) + } + } + } +} + +extension Driver { + public mutating func computeOutputCacheKeyForJob(commandLine: [Job.ArgTemplate], + inputs: [TypedVirtualPath]) throws -> [TypedVirtualPath: String] { + // No caching setup, return empty dictionary. + guard let cas = self.cas else { + return [:] + } + // Resolve command-line first. + let resolver = try ArgsResolver(fileSystem: fileSystem) + let arguments: [String] = try resolver.resolveArgumentList(for: commandLine) + + return try inputs.reduce(into: [:]) { keys, input in + let remappedPath = remapPath(input.file) + keys[input] = try cas.computeCacheKey(commandLine: arguments, input: remappedPath.name) + } + } + + public mutating func computeOutputCacheKey(commandLine: [Job.ArgTemplate], + input: TypedVirtualPath) throws -> String? { + // No caching setup, return empty dictionary. + guard let cas = self.cas else { + return nil + } + // Resolve command-line first. + let resolver = try ArgsResolver(fileSystem: fileSystem) + let arguments: [String] = try resolver.resolveArgumentList(for: commandLine) + let remappedPath = remapPath(input.file) + return try cas.computeCacheKey(commandLine: arguments, input: remappedPath.name) + } +} diff --git a/Sources/SwiftDriver/Jobs/GeneratePCHJob.swift b/Sources/SwiftDriver/Jobs/GeneratePCHJob.swift index 628b6d9ed..9628c1fa7 100644 --- a/Sources/SwiftDriver/Jobs/GeneratePCHJob.swift +++ b/Sources/SwiftDriver/Jobs/GeneratePCHJob.swift @@ -32,6 +32,7 @@ extension Driver { if try supportsBridgingHeaderPCHCommand() { try addExplicitPCHBuildArguments(inputs: &inputs, commandLine: &commandLine) + addCacheReplayMapping(to: &commandLine) } else { try addGeneratePCHFlags(commandLine: &commandLine, inputs: &inputs) } @@ -67,7 +68,9 @@ extension Driver { outputs.append(output) inputs.append(input) - commandLine.appendPath(input.file) + try addPathArgument(input.file, to: &commandLine) + + let cacheKeys = try computeOutputCacheKeyForJob(commandLine: commandLine, inputs: [input]) return Job( moduleName: moduleOutputInfo.name, @@ -77,7 +80,8 @@ extension Driver { displayInputs: [], inputs: inputs, primaryInputs: [], - outputs: outputs + outputs: outputs, + outputCacheKeys: cacheKeys ) } } diff --git a/Sources/SwiftDriver/Jobs/GeneratePCMJob.swift b/Sources/SwiftDriver/Jobs/GeneratePCMJob.swift index 86f1a4543..225a4bffe 100644 --- a/Sources/SwiftDriver/Jobs/GeneratePCMJob.swift +++ b/Sources/SwiftDriver/Jobs/GeneratePCMJob.swift @@ -51,6 +51,7 @@ extension Driver { commandLine: &commandLine, inputs: &inputs, kind: .generatePCM, bridgingHeaderHandling: .ignored) try commandLine.appendLast(.indexStorePath, from: &parsedOptions) + let cacheKeys = try computeOutputCacheKeyForJob(commandLine: commandLine, inputs: [input]) return Job( moduleName: moduleOutputInfo.name, @@ -60,7 +61,8 @@ extension Driver { displayInputs: [], inputs: inputs, primaryInputs: [], - outputs: outputs + outputs: outputs, + outputCacheKeys: cacheKeys ) } diff --git a/Sources/SwiftDriver/Jobs/Job.swift b/Sources/SwiftDriver/Jobs/Job.swift index 0c3571165..1313f8e2a 100644 --- a/Sources/SwiftDriver/Jobs/Job.swift +++ b/Sources/SwiftDriver/Jobs/Job.swift @@ -101,6 +101,9 @@ public struct Job: Codable, Equatable, Hashable { /// The kind of job. public var kind: Kind + /// The Cache Key for the compilation. It is a dictionary from input file to its output cache key. + public var outputCacheKeys: [TypedVirtualPath: String] + /// A map from a primary input to all of its corresponding outputs private var compileInputOutputMap: [TypedVirtualPath : [TypedVirtualPath]] @@ -113,6 +116,7 @@ public struct Job: Codable, Equatable, Hashable { inputs: [TypedVirtualPath], primaryInputs: [TypedVirtualPath], outputs: [TypedVirtualPath], + outputCacheKeys: [TypedVirtualPath: String] = [:], inputOutputMap: [TypedVirtualPath : [TypedVirtualPath]] = [:], extraEnvironment: [String: String] = [:], requiresInPlaceExecution: Bool = false @@ -125,6 +129,7 @@ public struct Job: Codable, Equatable, Hashable { self.inputs = inputs self.primaryInputs = primaryInputs self.outputs = outputs + self.outputCacheKeys = outputCacheKeys self.compileInputOutputMap = inputOutputMap self.extraEnvironment = extraEnvironment self.requiresInPlaceExecution = requiresInPlaceExecution @@ -289,6 +294,21 @@ extension Job.Kind { return false } } + + /// Whether this job supports caching. + public var supportCaching: Bool { + switch self { + case .compile, .emitModule, .generatePCH, .compileModuleFromInterface, + .generatePCM, .verifyModuleInterface: + return true + case .backend, .mergeModule, .dumpPCM, .interpret, .repl, .printTargetInfo, + .versionRequest, .autolinkExtract, .generateDSYM, .help, .link, + .verifyDebugInfo, .scanDependencies, .emitSupportedFeatures, .moduleWrap, + .generateAPIBaseline, .generateABIBaseline, .compareAPIBaseline, + .compareABIBaseline: + return false + } + } } // MARK: - Job.ArgTemplate + Codable diff --git a/Sources/SwiftDriver/Jobs/Planning.swift b/Sources/SwiftDriver/Jobs/Planning.swift index fd5e23e51..b630a7cd1 100644 --- a/Sources/SwiftDriver/Jobs/Planning.swift +++ b/Sources/SwiftDriver/Jobs/Planning.swift @@ -688,8 +688,8 @@ extension Driver { dependencyOracle: interModuleDependencyOracle, integratedDriver: integratedDriver, supportsExplicitInterfaceBuild: - isFrontendArgSupported(.explicitInterfaceModuleBuild), - enableCAS: enableCaching) + isFrontendArgSupported(.explicitInterfaceModuleBuild), + cas: cas) return try explicitDependencyBuildPlanner!.generateExplicitModuleDependenciesBuildJobs() } diff --git a/Sources/SwiftDriver/Jobs/VerifyModuleInterfaceJob.swift b/Sources/SwiftDriver/Jobs/VerifyModuleInterfaceJob.swift index cacc385ca..0f1c845d1 100644 --- a/Sources/SwiftDriver/Jobs/VerifyModuleInterfaceJob.swift +++ b/Sources/SwiftDriver/Jobs/VerifyModuleInterfaceJob.swift @@ -11,18 +11,17 @@ //===----------------------------------------------------------------------===// extension Driver { - func computeCacheKeyForInterface(emitModuleJob: Job, - interfaceKind: FileType) throws -> String? { + mutating func computeCacheKeyForInterface(emitModuleJob: Job, + interfaceKind: FileType) throws -> String? { assert(interfaceKind == .swiftInterface || interfaceKind == .privateSwiftInterface, "only expect interface output kind") let isNeeded = emitModuleJob.outputs.contains { $0.type == interfaceKind } - guard enableCaching && isNeeded else { return nil } + guard isCachingEnabled && isNeeded else { return nil } // Assume swiftinterface file is always the supplementary output for first input file. - let mainInput = emitModuleJob.inputs[0] - return try interModuleDependencyOracle.computeCacheKeyForOutput(kind: interfaceKind, - commandLine: emitModuleJob.commandLine, - input: mainInput.fileHandle) + let key = try computeOutputCacheKey(commandLine: emitModuleJob.commandLine, + input: emitModuleJob.inputs[0]) + return key } @_spi(Testing) @@ -35,7 +34,7 @@ extension Driver { var commandLine: [Job.ArgTemplate] = swiftCompilerPrefixArgs.map { Job.ArgTemplate.flag($0) } var inputs: [TypedVirtualPath] = [interfaceInput] commandLine.appendFlags("-frontend", "-typecheck-module-from-interface") - commandLine.appendPath(interfaceInput.file) + try addPathArgument(interfaceInput.file, to: &commandLine) try addCommonFrontendOptions(commandLine: &commandLine, inputs: &inputs, kind: .verifyModuleInterface) // FIXME: MSVC runtime flags @@ -58,6 +57,8 @@ extension Driver { if !optIn && isFrontendArgSupported(.downgradeTypecheckInterfaceError) { commandLine.appendFlag(.downgradeTypecheckInterfaceError) } + + let cacheKeys = try computeOutputCacheKeyForJob(commandLine: commandLine, inputs: [interfaceInput]) return Job( moduleName: moduleOutputInfo.name, kind: .verifyModuleInterface, @@ -66,7 +67,8 @@ extension Driver { displayInputs: [interfaceInput], inputs: inputs, primaryInputs: [], - outputs: outputs + outputs: outputs, + outputCacheKeys: cacheKeys ) } } diff --git a/Sources/SwiftDriver/SwiftScan/SwiftScan.swift b/Sources/SwiftDriver/SwiftScan/SwiftScan.swift index a77cb1227..d048d96a7 100644 --- a/Sources/SwiftDriver/SwiftScan/SwiftScan.swift +++ b/Sources/SwiftDriver/SwiftScan/SwiftScan.swift @@ -98,9 +98,6 @@ internal extension swiftscan_diagnostic_severity_t { /// Instance of a scanner, which maintains shared state across scan queries. let scanner: swiftscan_scanner_t; - /// Optional CAS instance. - var cas: swiftscan_cas_t? = nil - @_spi(Testing) public init(dylib path: AbsolutePath) throws { self.path = path #if os(Windows) @@ -117,9 +114,6 @@ internal extension swiftscan_diagnostic_severity_t { deinit { api.swiftscan_scanner_dispose(self.scanner) - if let scan_cas = cas { - api.swiftscan_cas_dispose(scan_cas) - } // FIXME: is it safe to dlclose() swiftscan? If so, do that here. // For now, let the handle leak. dylib.leak() @@ -289,10 +283,10 @@ internal extension swiftscan_diagnostic_severity_t { api.swiftscan_cas_options_dispose != nil && api.swiftscan_cas_options_set_ondisk_path != nil && api.swiftscan_cas_options_set_plugin_path != nil && - api.swiftscan_cas_options_set_option != nil && + api.swiftscan_cas_options_set_plugin_option != nil && api.swiftscan_cas_create_from_options != nil && api.swiftscan_cas_dispose != nil && - api.swiftscan_compute_cache_key != nil && + api.swiftscan_cache_compute_key != nil && api.swiftscan_cas_store != nil && api.swiftscan_swift_textual_detail_get_module_cache_key != nil && api.swiftscan_swift_binary_detail_get_module_cache_key != nil && @@ -392,16 +386,18 @@ internal extension swiftscan_diagnostic_severity_t { } } - private func handleCASError(_ closure: (inout swiftscan_string_ref_t) -> Bool) throws { + func handleCASError(_ closure: (inout swiftscan_string_ref_t) -> T) throws -> T{ var err_msg : swiftscan_string_ref_t = swiftscan_string_ref_t() - guard !closure(&err_msg) else { + let ret = closure(&err_msg) + if err_msg.length != 0 { let err_str = try toSwiftString(err_msg) api.swiftscan_string_dispose(err_msg) throw DependencyScanningError.casError(err_str) } + return ret } - func createCAS(pluginPath: String?, onDiskPath: String?, pluginOptions: [(String, String)]) throws { + func createCAS(pluginPath: String?, onDiskPath: String?, pluginOptions: [(String, String)]) throws -> SwiftScanCAS { let casOpts = api.swiftscan_cas_options_create() defer { api.swiftscan_cas_options_dispose(casOpts) @@ -414,65 +410,13 @@ internal extension swiftscan_diagnostic_severity_t { } for (name, value) in pluginOptions { try handleCASError { err_msg in - return api.swiftscan_cas_options_set_option(casOpts, name, value, &err_msg) + _ = api.swiftscan_cas_options_set_plugin_option(casOpts, name, value, &err_msg) } } - try handleCASError { err_msg in - self.cas = api.swiftscan_cas_create_from_options(casOpts, &err_msg) - return self.cas == nil - } - } - - func store(data: Data) throws -> String { - guard let scan_cas = self.cas else { - throw DependencyScanningError.casError("cannot store into CAS because CAS is not yet created") - } - let bytes = UnsafeMutablePointer.allocate(capacity: data.count) - data.copyBytes(to: bytes, count: data.count) - var casid: swiftscan_string_ref_t = swiftscan_string_ref_t() - try handleCASError { err_msg in - casid = api.swiftscan_cas_store(scan_cas, bytes, UInt32(data.count), &err_msg) - return casid.data == nil + let cas = try handleCASError { err_msg in + api.swiftscan_cas_create_from_options(casOpts, &err_msg) } - return try toSwiftString(casid) - } - - private func getSwiftScanOutputKind(kind: FileType) -> swiftscan_output_kind_t { - switch (kind) { - case .object: - return SWIFTSCAN_OUTPUT_TYPE_OBJECT - case .swiftModule: - return SWIFTSCAN_OUTPUT_TYPE_SWIFTMODULE - case .swiftInterface: - return SWIFTSCAN_OUTPUT_TYPE_SWIFTINTERFACE - case .privateSwiftInterface: - return SWIFTSCAN_OUTPUT_TYPE_SWIFTPRIVATEINTERFACE - case .pcm: - return SWIFTSCAN_OUTPUT_TYPE_CLANG_MODULE - case .pch: - return SWIFTSCAN_OUTPUT_TYPE_CLANG_PCH - default: - fatalError("Unsupported") - } - } - - func computeCacheKeyForOutput(kind: FileType, commandLine: [String], input: String) throws -> String { - guard let scan_cas = self.cas else { - throw DependencyScanningError.casError("cannot compute CacheKey for compilation because CAS is not yet created") - } - var casid: swiftscan_string_ref_t = swiftscan_string_ref_t() - try handleCASError { err_msg in - withArrayOfCStrings(commandLine) { commandArray in - casid = api.swiftscan_compute_cache_key(scan_cas, - Int32(commandLine.count), - commandArray, - input.cString(using: String.Encoding.utf8), - getSwiftScanOutputKind(kind: kind), - &err_msg) - } - return casid.data == nil - } - return try toSwiftString(casid) + return SwiftScanCAS(cas: cas!, scanner: self) } } @@ -558,14 +502,39 @@ private extension swiftscan_functions_t { self.swiftscan_cas_options_create = try loadOptional("swiftscan_cas_options_create") self.swiftscan_cas_options_set_plugin_path = try loadOptional("swiftscan_cas_options_set_plugin_path") self.swiftscan_cas_options_set_ondisk_path = try loadOptional("swiftscan_cas_options_set_ondisk_path") - self.swiftscan_cas_options_set_option = try loadOptional("swiftscan_cas_options_set_option") + self.swiftscan_cas_options_set_plugin_option = try loadOptional("swiftscan_cas_options_set_plugin_option") self.swiftscan_cas_options_dispose = try loadOptional("swiftscan_cas_options_dispose") self.swiftscan_cas_create_from_options = try loadOptional("swiftscan_cas_create_from_options") self.swiftscan_cas_dispose = try loadOptional("swiftscan_cas_dispose") - self.swiftscan_compute_cache_key = - try loadOptional("swiftscan_compute_cache_key") + self.swiftscan_cache_compute_key = try loadOptional("swiftscan_cache_compute_key") self.swiftscan_cas_store = try loadOptional("swiftscan_cas_store") + self.swiftscan_cache_query = try loadOptional("swiftscan_cache_query") + self.swiftscan_cache_query_async = try loadOptional("swiftscan_cache_query_async") + + self.swiftscan_cached_compilation_get_num_outputs = try loadOptional("swiftscan_cached_compilation_get_num_outputs") + self.swiftscan_cached_compilation_get_output = try loadOptional("swiftscan_cached_compilation_get_output") + self.swiftscan_cached_compilation_make_global_async = try loadOptional("swiftscan_cached_compilation_make_global_async") + self.swiftscan_cached_compilation_is_uncacheable = try loadOptional("swiftscan_cached_compilation_is_uncacheable") + self.swiftscan_cached_compilation_dispose = try loadOptional("swiftscan_cached_compilation_dispose") + + self.swiftscan_cached_output_load = try loadOptional("swiftscan_cached_output_load") + self.swiftscan_cached_output_load_async = try loadOptional("swiftscan_cached_output_load_async") + self.swiftscan_cached_output_is_materialized = try loadOptional("swiftscan_cached_output_is_materialized") + self.swiftscan_cached_output_get_casid = try loadOptional("swiftscan_cached_output_get_casid") + self.swiftscan_cached_output_get_name = try loadOptional("swiftscan_cached_output_get_name") + self.swiftscan_cached_output_dispose = try loadOptional("swiftscan_cached_output_dispose") + + self.swiftscan_cache_action_cancel = try loadOptional("swiftscan_cache_action_cancel") + self.swiftscan_cache_cancellation_token_dispose = try loadOptional("swiftscan_cache_cancellation_token_dispose") + + self.swiftscan_cache_replay_instance_create = try loadOptional("swiftscan_cache_replay_instance_create") + self.swiftscan_cache_replay_instance_dispose = try loadOptional("swiftscan_cache_replay_instance_dispose") + self.swiftscan_cache_replay_compilation = try loadOptional("swiftscan_cache_replay_compilation") + + self.swiftscan_cache_replay_result_get_stdout = try loadOptional("swiftscan_cache_replay_result_get_stdout") + self.swiftscan_cache_replay_result_get_stderr = try loadOptional("swiftscan_cache_replay_result_get_stderr") + self.swiftscan_cache_replay_result_dispose = try loadOptional("swiftscan_cache_replay_result_dispose") // Swift Overlay Dependencies self.swiftscan_swift_textual_detail_get_swift_overlay_dependencies = @@ -694,13 +663,14 @@ private extension swiftscan_functions_t { // TODO: Move to TSC? /// Perform an `action` passing it a `const char **` constructed out of `[String]` -func withArrayOfCStrings(_ strings: [String], - _ action: (UnsafeMutablePointer?>?) -> Void) +func withArrayOfCStrings(_ strings: [String], + _ action: (UnsafeMutablePointer?>?) -> T) -> T { let cstrings = strings.map { strdup($0) } + [nil] let unsafeCStrings = cstrings.map { UnsafePointer($0) } - let _ = unsafeCStrings.withUnsafeBufferPointer { + let result = unsafeCStrings.withUnsafeBufferPointer { action(UnsafeMutablePointer(mutating: $0.baseAddress)) } for ptr in cstrings { if let ptr = ptr { free(ptr) } } + return result } diff --git a/Sources/SwiftDriver/SwiftScan/SwiftScanCAS.swift b/Sources/SwiftDriver/SwiftScan/SwiftScanCAS.swift new file mode 100644 index 000000000..ce0e90527 --- /dev/null +++ b/Sources/SwiftDriver/SwiftScan/SwiftScanCAS.swift @@ -0,0 +1,338 @@ +//===------------------------ SwiftScanCAS.swift --------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@_implementationOnly import CSwiftScan +import struct Foundation.Data + +// Swift Package Manager is building with `-disable-implicit-concurrency-module-import` +// to avoid warnings on old SDKs. Explicity importing concurrency if available +// and only adds async APIs when concurrency is available. +#if canImport(_Concurrency) +import _Concurrency +#endif + +public final class CachedCompilation { + let ptr: swiftscan_cached_compilation_t + private let lib: SwiftScan + + init(_ ptr: swiftscan_cached_compilation_t, lib: SwiftScan) { + self.ptr = ptr + self.lib = lib + } + + public lazy var count: UInt32 = { + lib.api.swiftscan_cached_compilation_get_num_outputs(ptr) + }() + + public var isUncacheable: Bool { + lib.api.swiftscan_cached_compilation_is_uncacheable(ptr) + } + + deinit { + lib.api.swiftscan_cached_compilation_dispose(ptr) + } +} + +extension CachedCompilation: Sequence { + public typealias Element = CachedOutput + public struct Iterator: IteratorProtocol { + public typealias Element = CachedOutput + let limit: UInt32 + let ptr: swiftscan_cached_compilation_t + let lib: SwiftScan + var idx: UInt32 = 0 + public mutating func next() -> CachedOutput? { + guard idx < self.limit else { return nil } + let output = self.lib.api.swiftscan_cached_compilation_get_output(self.ptr, idx) + idx += 1 + // output can never be nil. + return CachedOutput(output!, lib: self.lib) + } + } + public func makeIterator() -> Iterator { + return Iterator(limit: self.count, ptr: self.ptr, lib: self.lib) + } +} + +public final class CachedOutput { + let ptr: swiftscan_cached_output_t + private let lib: SwiftScan + + init(_ ptr: swiftscan_cached_output_t, lib: SwiftScan) { + self.ptr = ptr + self.lib = lib + } + + public func load() throws -> Bool { + try lib.handleCASError { err_msg in + lib.api.swiftscan_cached_output_load(ptr, &err_msg) + } + } + + public var isMaterialized: Bool { + lib.api.swiftscan_cached_output_is_materialized(ptr) + } + + public func getCASID() throws -> String { + let id = lib.api.swiftscan_cached_output_get_casid(ptr) + defer { lib.api.swiftscan_string_dispose(id) } + return try lib.toSwiftString(id) + } + + public func getOutputKindName() throws -> String { + let kind = lib.api.swiftscan_cached_output_get_name(ptr) + defer { lib.api.swiftscan_string_dispose(kind) } + return try lib.toSwiftString(kind) + } + + deinit { + lib.api.swiftscan_cached_output_dispose(ptr) + } +} + +public final class CacheReplayInstance { + let ptr: swiftscan_cache_replay_instance_t + private let lib: SwiftScan + + init(_ ptr: swiftscan_cache_replay_instance_t, lib: SwiftScan) { + self.ptr = ptr + self.lib = lib + } + + deinit { + lib.api.swiftscan_cache_replay_instance_dispose(ptr) + } +} + +public final class CacheReplayResult { + let ptr: swiftscan_cache_replay_result_t + private let lib: SwiftScan + + init(_ ptr: swiftscan_cache_replay_result_t, lib: SwiftScan) { + self.ptr = ptr + self.lib = lib + } + + public func getStdOut() throws -> String { + let str = lib.api.swiftscan_cache_replay_result_get_stdout(ptr) + return try lib.toSwiftString(str) + } + + public func getStdErr() throws -> String { + let str = lib.api.swiftscan_cache_replay_result_get_stderr(ptr) + return try lib.toSwiftString(str) + } + + deinit { + lib.api.swiftscan_cache_replay_result_dispose(ptr) + } +} + +public final class SwiftScanCAS { + let cas: swiftscan_cas_t + private let scanner: SwiftScan + deinit { + scanner.api.swiftscan_cas_dispose(cas) + } + + init(cas: swiftscan_cas_t, scanner: SwiftScan) { + self.cas = cas + self.scanner = scanner + } + + private func convert(compilation: swiftscan_cached_compilation_t?) -> CachedCompilation? { + return compilation?.convert(scanner) + } + private func convert(instance: swiftscan_cache_replay_instance_t?) -> CacheReplayInstance? { + return instance?.convert(scanner) + } + private func convert(result: swiftscan_cache_replay_result_t?) -> CacheReplayResult? { + return result?.convert(scanner) + } + + public func store(data: Data) throws -> String { + let bytes = UnsafeMutablePointer.allocate(capacity: data.count) + data.copyBytes(to: bytes, count: data.count) + let casid = try scanner.handleCASError { err_msg in + scanner.api.swiftscan_cas_store(cas, bytes, UInt32(data.count), &err_msg) + } + return try scanner.toSwiftString(casid) + } + + public func computeCacheKey(commandLine: [String], input: String) throws -> String { + let casid = try scanner.handleCASError { err_msg in + withArrayOfCStrings(commandLine) { commandArray in + scanner.api.swiftscan_cache_compute_key(cas, + Int32(commandLine.count), + commandArray, + input.cString(using: String.Encoding.utf8), + &err_msg) + } + } + return try scanner.toSwiftString(casid) + } + + public func createReplayInstance(commandLine: [String]) throws -> CacheReplayInstance { + let instance = try scanner.handleCASError { err_msg in + withArrayOfCStrings(commandLine) { commandArray in + scanner.api.swiftscan_cache_replay_instance_create(Int32(commandLine.count), + commandArray, + &err_msg) + } + } + // Never return nullptr when no error occurs. + guard let result = convert(instance: instance) else { + throw DependencyScanningError.casError("unexpected nil for replay instance") + } + return result + } + + public func queryCacheKey(_ key: String, globally: Bool) throws -> CachedCompilation? { + let result = try scanner.handleCASError { error in + scanner.api.swiftscan_cache_query(cas, key.cString(using: .utf8), globally, &error) + } + return convert(compilation: result) + } + + public func replayCompilation(instance: CacheReplayInstance, compilation: CachedCompilation) throws -> CacheReplayResult { + let result = try scanner.handleCASError { err_msg in + scanner.api.swiftscan_cache_replay_compilation(instance.ptr, compilation.ptr, &err_msg) + } + guard let res = convert(result: result) else { + throw DependencyScanningError.casError("unexpected nil for cache_replay_result") + } + return res + } +} + +extension swiftscan_cached_compilation_t { + func convert(_ lib: SwiftScan) -> CachedCompilation { + return CachedCompilation(self, lib: lib) + } +} + +extension swiftscan_cache_replay_instance_t { + func convert(_ lib: SwiftScan) -> CacheReplayInstance { + return CacheReplayInstance(self, lib: lib) + } +} + +extension swiftscan_cache_replay_result_t { + func convert(_ lib: SwiftScan) -> CacheReplayResult { + return CacheReplayResult(self, lib: lib) + } +} + +#if canImport(_Concurrency) +// Async API Vendor +extension CachedCompilation { + public func makeGlobal() async throws -> Bool { + class CallbackContext { + func retain() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + let continuation: CheckedContinuation + let comp: CachedCompilation + init(_ continuation: CheckedContinuation, compilation: CachedCompilation) { + self.continuation = continuation + self.comp = compilation + } + } + + func callbackFunc(_ context: UnsafeMutableRawPointer?, _ error: swiftscan_string_ref_t) { + let obj = Unmanaged.fromOpaque(context!).takeRetainedValue() + if error.length != 0 { + if let err = try? obj.comp.lib.toSwiftString(error) { + obj.continuation.resume(throwing: DependencyScanningError.casError(err)) + } else { + obj.continuation.resume(throwing: DependencyScanningError.casError("unknown makeGlobal error")) + } + } + obj.continuation.resume(returning: true) + } + + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let context = CallbackContext(continuation, compilation: self) + lib.api.swiftscan_cached_compilation_make_global_async(ptr, context.retain(), callbackFunc, nil) + } + } +} + +extension CachedOutput { + public func load() async throws -> Bool { + class CallbackContext { + func retain() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + let continuation: CheckedContinuation + let output: CachedOutput + init(_ continuation: CheckedContinuation, output: CachedOutput) { + self.continuation = continuation + self.output = output + } + } + + func callbackFunc(_ context: UnsafeMutableRawPointer?, _ success: Bool, _ error: swiftscan_string_ref_t) { + let obj = Unmanaged.fromOpaque(context!).takeRetainedValue() + if error.length != 0 { + if let err = try? obj.output.lib.toSwiftString(error) { + obj.continuation.resume(throwing: DependencyScanningError.casError(err)) + } else { + obj.continuation.resume(throwing: DependencyScanningError.casError("unknown output loading error")) + } + } + obj.continuation.resume(returning: success) + } + + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let context = CallbackContext(continuation, output: self) + lib.api.swiftscan_cached_output_load_async(ptr, context.retain(), callbackFunc, nil) + } + } +} + +extension SwiftScanCAS { + public func queryCacheKey(_ key: String, globally: Bool) async throws -> CachedCompilation? { + class CallbackContext { + func retain() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + let continuation: CheckedContinuation + let cas: SwiftScanCAS + init(_ continuation: CheckedContinuation, cas: SwiftScanCAS) { + self.continuation = continuation + self.cas = cas + } + } + + func callbackFunc(_ context: UnsafeMutableRawPointer?, _ comp: swiftscan_cached_compilation_t?, _ error: swiftscan_string_ref_t) { + let obj = Unmanaged.fromOpaque(context!).takeRetainedValue() + if error.length != 0 { + if let err = try? obj.cas.scanner.toSwiftString(error) { + obj.continuation.resume(throwing: DependencyScanningError.casError(err)) + } else { + obj.continuation.resume(throwing: DependencyScanningError.casError("unknown cache querying error")) + } + } + obj.continuation.resume(returning: obj.cas.convert(compilation: comp)) + } + + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let context = CallbackContext(continuation, cas: self) + scanner.api.swiftscan_cache_query_async(cas, key.cString(using: .utf8), globally, context.retain(), callbackFunc, nil) + } + } +} +#endif diff --git a/Sources/SwiftDriver/Utilities/FileType.swift b/Sources/SwiftDriver/Utilities/FileType.swift index 8acbd145e..b55943a1c 100644 --- a/Sources/SwiftDriver/Utilities/FileType.swift +++ b/Sources/SwiftDriver/Utilities/FileType.swift @@ -154,6 +154,15 @@ public enum FileType: String, Hashable, CaseIterable, Codable { /// API descriptor JSON case jsonAPIDescriptor + + /// Swift Module Summary + case moduleSummary = "swiftmodulesummary" + + /// Swift Module Semantic Info + case moduleSemanticInfo + + /// Cached Diagnostics + case cachedDiagnostics } extension FileType: CustomStringConvertible { @@ -241,6 +250,15 @@ extension FileType: CustomStringConvertible { case .jsonAPIDescriptor: return "api-descriptor-json" + + case .moduleSummary: + return "swift-module-summary" + + case .moduleSemanticInfo: + return "module-semantic-info" + + case .cachedDiagnostics: + return "cached-diagnostics" } } } @@ -260,7 +278,8 @@ extension FileType { .swiftInterface, .privateSwiftInterface, .swiftSourceInfoFile, .jsonDependencies, .clangModuleMap, .jsonTargetInfo, .jsonCompilerFeatures, .jsonSwiftArtifacts, .indexUnitOutputPath, .modDepCache, .jsonAPIBaseline, - .jsonABIBaseline, .swiftConstValues, .jsonAPIDescriptor: + .jsonABIBaseline, .swiftConstValues, .jsonAPIDescriptor, + .moduleSummary, .moduleSemanticInfo, .cachedDiagnostics: return false } } @@ -367,6 +386,12 @@ extension FileType { return "const-values" case .jsonAPIDescriptor: return "api-descriptor-json" + case .moduleSummary: + return "swiftmodulesummary" + case .moduleSemanticInfo: + return "module-semantic-info" + case .cachedDiagnostics: + return "cached-diagnostics" } } } @@ -379,7 +404,7 @@ extension FileType { .moduleTrace, .yamlOptimizationRecord, .swiftInterface, .privateSwiftInterface, .jsonDependencies, .clangModuleMap, .jsonCompilerFeatures, .jsonTargetInfo, .jsonSwiftArtifacts, .jsonAPIBaseline, .jsonABIBaseline, .swiftConstValues, - .jsonAPIDescriptor: + .jsonAPIDescriptor, .moduleSummary, .moduleSemanticInfo, .cachedDiagnostics: return true case .image, .object, .dSYM, .pch, .sib, .raw_sib, .swiftModule, .swiftDocumentation, .swiftSourceInfoFile, .llvmBitcode, .diagnostics, @@ -403,8 +428,30 @@ extension FileType { .modDepCache, .bitstreamOptimizationRecord, .pcm, .pch, .jsonDependencies, .clangModuleMap, .jsonCompilerFeatures, .jsonTargetInfo, .jsonSwiftArtifacts, .indexUnitOutputPath, .jsonAPIBaseline, .jsonABIBaseline, .swiftConstValues, - .jsonAPIDescriptor: + .jsonAPIDescriptor, .moduleSummary, .moduleSemanticInfo, .cachedDiagnostics: return false } } + + /// Returns true if the type can be cached as output. + var supportCaching: Bool { + switch self { + case .diagnostics, .emitModuleDiagnostics, // diagnostics are cached using cached diagnostics. + // Those are by-product from swift-driver and not considered outputs need caching. + .jsonSwiftArtifacts, .remap, .indexUnitOutputPath, .modDepCache, + // the remaining should not be an output from a caching swift job. + .swift, .image, .dSYM, .importedModules, .clangModuleMap, + .jsonCompilerFeatures, .jsonTargetInfo, .autolink: + return false + case .assembly, .llvmIR, .llvmBitcode, .object, .sil, .sib, .ast, + .dependencies, .emitModuleDependencies, .swiftModule, + .swiftDocumentation, .swiftInterface, .privateSwiftInterface, + .swiftSourceInfoFile, .raw_sil, .raw_sib, .objcHeader, .swiftDeps, .tbd, + .moduleTrace, .indexData, .yamlOptimizationRecord, + .bitstreamOptimizationRecord, .pcm, .pch, .jsonDependencies, + .jsonAPIBaseline, .jsonABIBaseline, .swiftConstValues, .jsonAPIDescriptor, + .moduleSummary, .moduleSemanticInfo, .cachedDiagnostics: + return true + } + } } diff --git a/Sources/SwiftDriver/Utilities/VirtualPath.swift b/Sources/SwiftDriver/Utilities/VirtualPath.swift index 12dda459c..665147850 100644 --- a/Sources/SwiftDriver/Utilities/VirtualPath.swift +++ b/Sources/SwiftDriver/Utilities/VirtualPath.swift @@ -331,6 +331,11 @@ extension VirtualPath { public static let standardOutput = Handle(-1) public static let standardInput = Handle(-2) +#if os(Windows) + public static let null = try! VirtualPath(path: "nul").intern() +#else + public static let null = try! VirtualPath(path: "/dev/null").intern() +#endif } /// An implementation of a concurrent path cache. @@ -710,7 +715,7 @@ extension TSCBasic.FileSystem { } } - func readFileContents(_ path: VirtualPath) throws -> ByteString { + @_spi(Testing) public func readFileContents(_ path: VirtualPath) throws -> ByteString { try resolvingVirtualPath(path, apply: readFileContents) } @@ -726,7 +731,7 @@ extension TSCBasic.FileSystem { } } - func removeFileTree(_ path: VirtualPath) throws { + @_spi(Testing) public func removeFileTree(_ path: VirtualPath) throws { try resolvingVirtualPath(path) { absolutePath in try self.removeFileTree(absolutePath) } diff --git a/Sources/SwiftOptions/Options.swift b/Sources/SwiftOptions/Options.swift index c234f47d4..c580479fc 100644 --- a/Sources/SwiftOptions/Options.swift +++ b/Sources/SwiftOptions/Options.swift @@ -33,6 +33,7 @@ extension Option { public static let analyzeRequirementMachine: Option = Option("-analyze-requirement-machine", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Print out requirement machine statistics at the end of the compilation job") public static let apiDiffDataDir: Option = Option("-api-diff-data-dir", .separate, attributes: [.frontend, .noInteractive, .doesNotAffectIncrementalBuild, .argumentIsPath], metaVar: "", helpText: "Load platform and version specific API migration data files from . Ignored if -api-diff-data-file is specified.") public static let apiDiffDataFile: Option = Option("-api-diff-data-file", .separate, attributes: [.frontend, .noInteractive, .doesNotAffectIncrementalBuild, .argumentIsPath], metaVar: "", helpText: "API migration data is from ") + public static let enableAppExtensionLibrary: Option = Option("-application-extension-library", .flag, attributes: [.frontend, .noInteractive], helpText: "Restrict code to those available for App Extension Libraries") public static let enableAppExtension: Option = Option("-application-extension", .flag, attributes: [.frontend, .noInteractive], helpText: "Restrict code to those available for App Extensions") public static let AssertConfig: Option = Option("-assert-config", .separate, attributes: [.frontend, .moduleInterface], helpText: "Specify the assert_configuration replacement. Possible values are Debug, Release, Unchecked, DisableReplacement.") public static let AssumeSingleThreaded: Option = Option("-assume-single-threaded", .flag, attributes: [.helpHidden, .frontend], helpText: "Assume that code will be executed in a single-threaded environment") @@ -79,7 +80,6 @@ extension Option { public static let clangHeaderExposeDecls: Option = Option("-clang-header-expose-decls=", .joined, attributes: [.helpHidden, .frontend, .noDriver], metaVar: "all-public|has-expose-attr", helpText: "Which declarations should be exposed in the generated clang header.") public static let clangHeaderExposeModule: Option = Option("-clang-header-expose-module", .separate, attributes: [.helpHidden, .frontend, .noDriver], metaVar: "=", helpText: "Allow the compiler to assume that APIs from the specified module are exposed to C/C++/Objective-C in another generated header, so that APIs in the current module that depend on declarations from the specified module can be exposed in the generated header.") public static let clangIncludeTreeRoot: Option = Option("-clang-include-tree-root", .separate, attributes: [.helpHidden, .frontend, .noDriver], metaVar: "", helpText: "Clang Include Tree CASID") - public static let clangIncludeTree: Option = Option("-clang-include-tree", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Use clang include tree") public static let clangScannerModuleCachePath: Option = Option("-clang-scanner-module-cache-path", .separate, attributes: [.frontend, .doesNotAffectIncrementalBuild, .argumentIsPath], helpText: "Specifies the Clang dependency scanner module cache path") public static let clangTarget: Option = Option("-clang-target", .separate, attributes: [.frontend], helpText: "Separately set the target we should use for internal Clang instance") public static let codeCompleteCallPatternHeuristics: Option = Option("-code-complete-call-pattern-heuristics", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Use heuristics to guess whether we want call pattern completions") @@ -114,7 +114,6 @@ extension Option { public static let debugForbidTypecheckPrefix: Option = Option("-debug-forbid-typecheck-prefix", .separate, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Triggers llvm fatal_error if typechecker tries to typecheck a decl with the provided prefix name") public static let debugGenericSignatures: Option = Option("-debug-generic-signatures", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Debug generic signatures") public static let debugInfoFormat: Option = Option("-debug-info-format=", .joined, attributes: [.frontend], helpText: "Specify the debug info format type to either 'dwarf' or 'codeview'") - public static let dwarfVersion: Option = Option("-dwarf-version=", .joined, attributes: [.frontend], helpText: "DWARF debug info version to produce if requested") public static let debugInfoStoreInvocation: Option = Option("-debug-info-store-invocation", .flag, attributes: [.frontend], helpText: "Emit the compiler invocation in the debug info.") public static let debugMapping: Option = Option("-debug-mapping", .flag, attributes: [.noDriver], helpText: "Dumping information for debug purposes") public static let debugMapping_: Option = Option("--debug-mapping", .flag, alias: Option.debugMapping, attributes: [.noDriver], helpText: "Dumping information for debug purposes") @@ -278,6 +277,7 @@ extension Option { public static let dumpTypeRefinementContexts: Option = Option("-dump-type-refinement-contexts", .flag, attributes: [.frontend, .noInteractive, .doesNotAffectIncrementalBuild], helpText: "Type-check input file(s) and dump type refinement contexts(s)", group: .modes) public static let dumpTypeWitnessSystems: Option = Option("-dump-type-witness-systems", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Enables dumping type witness systems from associated type inference") public static let dumpUsr: Option = Option("-dump-usr", .flag, attributes: [.frontend, .noInteractive], helpText: "Dump USR for each declaration reference") + public static let dwarfVersion: Option = Option("-dwarf-version=", .joined, attributes: [.frontend], metaVar: "", helpText: "DWARF debug info version to produce if requested") public static let D: Option = Option("-D", .joinedOrSeparate, attributes: [.frontend], helpText: "Marks a conditional compilation flag as true") public static let embedBitcodeMarker: Option = Option("-embed-bitcode-marker", .flag, attributes: [.frontend, .noInteractive], helpText: "Embed placeholder LLVM IR data as a marker") public static let embedBitcode: Option = Option("-embed-bitcode", .flag, attributes: [.frontend, .noInteractive], helpText: "Embed LLVM IR bitcode as data") @@ -584,6 +584,7 @@ extension Option { public static let module: Option = Option("-module", .separate, attributes: [.noDriver], metaVar: "", helpText: "Names of modules") public static let module_: Option = Option("--module", .separate, alias: Option.module, attributes: [.noDriver], metaVar: "", helpText: "Names of modules") public static let newDriverPath: Option = Option("-new-driver-path", .separate, attributes: [.helpHidden, .frontend, .noDriver], metaVar: "", helpText: "Path of the new driver to be used") + public static let noClangIncludeTree: Option = Option("-no-clang-include-tree", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Do not use clang include tree, fallback to use CAS filesystem to build clang modules") public static let noClangModuleBreadcrumbs: Option = Option("-no-clang-module-breadcrumbs", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Don't emit DWARF skeleton CUs for imported Clang modules. Use this when building a redistributable static archive.") public static let noColorDiagnostics: Option = Option("-no-color-diagnostics", .flag, attributes: [.frontend, .doesNotAffectIncrementalBuild], helpText: "Do not print diagnostics in color") public static let noEmitModuleSeparatelyWMO: Option = Option("-no-emit-module-separately-wmo", .flag, attributes: [.helpHidden], helpText: "Force emitting the swiftmodule in the same job in wmo builds") @@ -615,7 +616,7 @@ extension Option { public static let O: Option = Option("-O", .flag, attributes: [.frontend, .moduleInterface], helpText: "Compile with optimizations", group: .O) public static let o: Option = Option("-o", .joinedOrSeparate, attributes: [.frontend, .noInteractive, .autolinkExtract, .moduleWrap, .indent, .argumentIsPath, .cacheInvariant], metaVar: "", helpText: "Write output to ") public static let packageDescriptionVersion: Option = Option("-package-description-version", .separate, attributes: [.helpHidden, .frontend, .moduleInterface], metaVar: "", helpText: "The version number to be applied on the input for the PackageDescription availability kind") - public static let packageName: Option = Option("-package-name", .separate, attributes: [.frontend], helpText: "Name of the package the module belongs to") + public static let packageName: Option = Option("-package-name", .separate, attributes: [.frontend, .moduleInterface], helpText: "Name of the package the module belongs to") public static let parallelScan: Option = Option("-parallel-scan", .flag, attributes: [.frontend, .noDriver], helpText: "Perform dependency scanning in-parallel.") public static let parseAsLibrary: Option = Option("-parse-as-library", .flag, attributes: [.frontend, .noInteractive], helpText: "Parse the input file(s) as libraries, not scripts") public static let parseSil: Option = Option("-parse-sil", .flag, attributes: [.frontend, .noInteractive], helpText: "Parse the input file as SIL code, not Swift source") @@ -854,6 +855,7 @@ extension Option { Option.analyzeRequirementMachine, Option.apiDiffDataDir, Option.apiDiffDataFile, + Option.enableAppExtensionLibrary, Option.enableAppExtension, Option.AssertConfig, Option.AssumeSingleThreaded, @@ -900,7 +902,6 @@ extension Option { Option.clangHeaderExposeDecls, Option.clangHeaderExposeModule, Option.clangIncludeTreeRoot, - Option.clangIncludeTree, Option.clangScannerModuleCachePath, Option.clangTarget, Option.codeCompleteCallPatternHeuristics, @@ -935,7 +936,6 @@ extension Option { Option.debugForbidTypecheckPrefix, Option.debugGenericSignatures, Option.debugInfoFormat, - Option.dwarfVersion, Option.debugInfoStoreInvocation, Option.debugMapping, Option.debugMapping_, @@ -1099,6 +1099,7 @@ extension Option { Option.dumpTypeRefinementContexts, Option.dumpTypeWitnessSystems, Option.dumpUsr, + Option.dwarfVersion, Option.D, Option.embedBitcodeMarker, Option.embedBitcode, @@ -1405,6 +1406,7 @@ extension Option { Option.module, Option.module_, Option.newDriverPath, + Option.noClangIncludeTree, Option.noClangModuleBreadcrumbs, Option.noColorDiagnostics, Option.noEmitModuleSeparatelyWMO, diff --git a/Tests/SwiftDriverTests/CachingBuildTests.swift b/Tests/SwiftDriverTests/CachingBuildTests.swift index 0d5878def..dd0f34713 100644 --- a/Tests/SwiftDriverTests/CachingBuildTests.swift +++ b/Tests/SwiftDriverTests/CachingBuildTests.swift @@ -75,6 +75,63 @@ throws { dependencyGraph: dependencyGraph) } +/// Checks that the output keys are in the action cache and also the output +/// can be replayed from CAS and identicial to the original output. +private func checkCASForResults(jobs: [Job], cas: SwiftScanCAS, fs: FileSystem) throws { + let expectation = XCTestExpectation(description: "Check CAS for output") + @Sendable + func replayAndVerifyOutput(_ job: Job, _ compilations: [CachedCompilation]) async throws { + func hashFile(_ file: VirtualPath) throws -> String { + // store the content in the CAS as a hashing function. + return try fs.readFileContents(file).withData { + try cas.store(data: $0) + } + } + let outputHashes = try job.outputs.map { + let hash = try hashFile($0.file) + // remove the original output after hashing the file. + try fs.removeFileTree($0.file) + return hash + } + let resolver = try ArgsResolver(fileSystem: fs) + let arguments: [String] = try resolver.resolveArgumentList(for: job.commandLine) + let instance = try cas.createReplayInstance(commandLine: arguments) + for compilation in compilations { + let _ = try cas.replayCompilation(instance: instance, compilation: compilation) + } + let replayHashes = try job.outputs.map { + try hashFile($0.file) + } + XCTAssertEqual(outputHashes, replayHashes, "replayed output is not identical to original") + } + Task { + defer { + expectation.fulfill() + } + for job in jobs { + if !job.kind.supportCaching { + continue + } + var compilations = [CachedCompilation]() + for (_, key) in job.outputCacheKeys { + if let compilation = try await cas.queryCacheKey(key, globally: false) { + for output in compilation { + XCTAssertTrue(output.isMaterialized, "Cached output not founded in CAS") + let success = try await output.load() + XCTAssertTrue(success, "Cached output not founded in CAS") + } + compilations.append(compilation) + } else { + XCTFail("Cached entry not found") + } + } + try await replayAndVerifyOutput(job, compilations) + } + } + let result = XCTWaiter.wait(for: [expectation], timeout: 10.0) + XCTAssertEqual(result, .completed) +} + /// Checks that the build job for the specified module contains the required options and inputs /// to build all of its dependencies explicitly private func checkCachingBuildJobDependencies(job: Job, @@ -106,8 +163,6 @@ private func checkCachingBuildJobDependencies(job: Job, XCTAssertTrue(job.inputs.contains(clangDependencyModuleMapPath)) XCTAssertTrue(job.commandLine.contains( .flag(String("-fmodule-file=\(dependencyId.moduleName)=\(clangDependencyModulePathString)")))) - XCTAssertTrue(job.commandLine.contains( - .flag(String("-fmodule-map-file=\(clangDependencyDetails.moduleMapPath.path.description)")))) XCTAssertTrue(job.commandLine.contains( .flag(String("-fmodule-file-cache-key")))) let cacheKey = try XCTUnwrap(clangDependencyDetails.moduleCacheKey) @@ -168,16 +223,8 @@ final class CachingBuildTests: XCTestCase { "-cache-compile-job", "-cas-path", casPath.nativePathString(escaped: true), "-import-objc-header", bridgingHeaderpath.nativePathString(escaped: true), main.nativePathString(escaped: true)] + sdkArgumentsForTesting) - let dependencyOracle = InterModuleDependencyOracle() - let scanLibPath = try XCTUnwrap(driver.toolchain.lookupSwiftScanLib()) - guard try dependencyOracle - .verifyOrCreateScannerInstance(fileSystem: localFileSystem, - swiftScanLibPath: scanLibPath) else { - XCTFail("Dependency scanner library not found") - return - } - guard try dependencyOracle.supportsCaching() else { - throw XCTSkip("libSwiftScan does not support caching.") + guard driver.isFeatureSupported(.cache_compile_job) else { + throw XCTSkip("toolchain does not support caching.") } let jobs = try driver.planBuild() @@ -303,16 +350,8 @@ final class CachingBuildTests: XCTestCase { guard driver.supportExplicitModuleVerifyInterface() else { throw XCTSkip("-typecheck-module-from-interface doesn't support explicit build.") } - let dependencyOracle = InterModuleDependencyOracle() - let scanLibPath = try XCTUnwrap(driver.toolchain.lookupSwiftScanLib()) - guard try dependencyOracle - .verifyOrCreateScannerInstance(fileSystem: localFileSystem, - swiftScanLibPath: scanLibPath) else { - XCTFail("Dependency scanner library not found") - return - } - guard try dependencyOracle.supportsCaching() else { - throw XCTSkip("libSwiftScan does not support caching.") + guard driver.isFeatureSupported(.cache_compile_job) else { + throw XCTSkip("toolchain does not support caching.") } let jobs = try driver.planBuild() @@ -442,20 +481,17 @@ final class CachingBuildTests: XCTestCase { "-working-directory", path.nativePathString(escaped: true), main.nativePathString(escaped: true)] + sdkArgumentsForTesting, env: ProcessEnv.vars) - let dependencyOracle = InterModuleDependencyOracle() - let scanLibPath = try XCTUnwrap(driver.toolchain.lookupSwiftScanLib()) - guard try dependencyOracle - .verifyOrCreateScannerInstance(fileSystem: localFileSystem, - swiftScanLibPath: scanLibPath) else { - XCTFail("Dependency scanner library not found") - return - } - guard try dependencyOracle.supportsCaching() else { - throw XCTSkip("libSwiftScan does not support caching.") + guard driver.isFeatureSupported(.cache_compile_job) else { + throw XCTSkip("toolchain does not support caching.") } let jobs = try driver.planBuild() try driver.run(jobs: jobs) XCTAssertFalse(driver.diagnosticEngine.hasErrors) + guard let cas = driver.cas else { + XCTFail("CAS is not available") + return + } + try checkCASForResults(jobs: jobs, cas: cas, fs: driver.fileSystem) } } @@ -500,6 +536,9 @@ final class CachingBuildTests: XCTestCase { env: ProcessEnv.vars) // Ensure this tooling supports this functionality + guard fooBuildDriver.isFeatureSupported(.cache_compile_job) else { + throw XCTSkip("toolchain does not support caching.") + } let dependencyOracle = InterModuleDependencyOracle() let scanLibPath = try XCTUnwrap(fooBuildDriver.toolchain.lookupSwiftScanLib()) guard try dependencyOracle @@ -511,9 +550,6 @@ final class CachingBuildTests: XCTestCase { guard try dependencyOracle.supportsBinaryModuleHeaderDependencies() else { throw XCTSkip("libSwiftScan does not support binary module header dependencies.") } - guard try dependencyOracle.supportsCaching() else { - throw XCTSkip("libSwiftScan does not support caching.") - } let fooJobs = try fooBuildDriver.planBuild() try fooBuildDriver.run(jobs: fooJobs) @@ -561,6 +597,9 @@ final class CachingBuildTests: XCTestCase { "-disable-clang-target", main.nativePathString(escaped: true)] + sdkArgumentsForTesting, env: ProcessEnv.vars) + guard driver.isFeatureSupported(.cache_compile_job) else { + throw XCTSkip("toolchain does not support caching.") + } let dependencyOracle = InterModuleDependencyOracle() let scanLibPath = try XCTUnwrap(driver.toolchain.lookupSwiftScanLib()) guard try dependencyOracle @@ -569,9 +608,6 @@ final class CachingBuildTests: XCTestCase { XCTFail("Dependency scanner library not found") return } - guard try dependencyOracle.supportsCaching() else { - throw XCTSkip("libSwiftScan does not support caching.") - } let resolver = try ArgsResolver(fileSystem: localFileSystem) var scannerCommand = try driver.dependencyScannerInvocationCommand().1.map { try resolver.resolve($0) } // We generate full swiftc -frontend -scan-dependencies invocations in order to also be @@ -661,4 +697,68 @@ final class CachingBuildTests: XCTestCase { XCTAssertEqual(diags[0].message, "CAS error encountered: conflicting CAS options used in scanning service") } } + + func testDependencyScanningPathRemap() throws { + // Create a simple test case. + try withTemporaryDirectory { path in + let main = path.appending(component: "testDependencyScanning.swift") + try localFileSystem.writeFileContents(main) { + $0.send("import C;") + $0.send("import E;") + $0.send("import G;") + } + + let cHeadersPath: AbsolutePath = + try testInputsPath.appending(component: "ExplicitModuleBuilds") + .appending(component: "CHeaders") + let swiftModuleInterfacesPath: AbsolutePath = + try testInputsPath.appending(component: "ExplicitModuleBuilds") + .appending(component: "Swift") + let casPath = path.appending(component: "cas") + let sdkArgumentsForTesting = (try? Driver.sdkArgumentsForTesting()) ?? [] + var driver = try Driver(args: ["swiftc", + "-I", cHeadersPath.nativePathString(escaped: true), + "-I", swiftModuleInterfacesPath.nativePathString(escaped: true), + "/tmp/Foo.o", "-v", + "-explicit-module-build", + "-cache-compile-job", "-cas-path", casPath.nativePathString(escaped: true), + "-working-directory", path.nativePathString(escaped: true), + "-disable-clang-target", "-scanner-prefix-map-sdk", "/^sdk", + "-scanner-prefix-map-toolchain", "/^toolchain", + "-scanner-prefix-map", testInputsPath.description + "=/^src", + "-scanner-prefix-map", path.description + "=/^tmp", + main.nativePathString(escaped: true)] + sdkArgumentsForTesting, + env: ProcessEnv.vars) + guard driver.isFrontendArgSupported(.scannerPrefixMap) else { + throw XCTSkip("frontend doesn't support prefix map") + } + let dependencyOracle = InterModuleDependencyOracle() + let scanLibPath = try XCTUnwrap(driver.toolchain.lookupSwiftScanLib()) + guard try dependencyOracle + .verifyOrCreateScannerInstance(fileSystem: localFileSystem, + swiftScanLibPath: scanLibPath) else { + XCTFail("Dependency scanner library not found") + return + } + let resolver = try ArgsResolver(fileSystem: localFileSystem) + let scannerCommand = try driver.dependencyScannerInvocationCommand().1.map { try resolver.resolve($0) } + + XCTAssertTrue(scannerCommand.contains("-scanner-prefix-map")) + XCTAssertTrue(scannerCommand.contains(try testInputsPath.description + "=/^src")) + + let jobs = try driver.planBuild() + for job in jobs { + if !job.kind.supportCaching { + continue + } + let command = try job.commandLine.map { try resolver.resolve($0) } + // Check all the arguments that are in the temporary directory are remapped. + // The only one that is not remapped should be the `-cas-path` that points to + // `casPath`. + XCTAssertFalse(command.contains { + return $0.starts(with: path.description) && $0 != casPath.description + }) + } + } + } }