diff --git a/Sources/SwiftDriver/Execution/ArgsResolver.swift b/Sources/SwiftDriver/Execution/ArgsResolver.swift index c8315849c..055e37de5 100644 --- a/Sources/SwiftDriver/Execution/ArgsResolver.swift +++ b/Sources/SwiftDriver/Execution/ArgsResolver.swift @@ -128,6 +128,11 @@ public final class ArgsResolver { case let .joinedOptionAndPath(option, path): return option + (try resolve(.path(path))) + case let .commaJoinedOptionAndPaths(option, paths): + return try option + paths.map { + try resolve(.path($0)) + }.joined(separator: ",") + case let .squashedArgumentList(option: option, args: args): return try option + args.map { try resolve($0) diff --git a/Sources/SwiftDriver/Jobs/CommandLineArguments.swift b/Sources/SwiftDriver/Jobs/CommandLineArguments.swift index 821191251..000e59a3a 100644 --- a/Sources/SwiftDriver/Jobs/CommandLineArguments.swift +++ b/Sources/SwiftDriver/Jobs/CommandLineArguments.swift @@ -190,6 +190,8 @@ extension Array where Element == Job.ArgTemplate { return "@\(path.name.spm_shellEscaped())" case let .joinedOptionAndPath(option, path): return option.spm_shellEscaped() + path.name.spm_shellEscaped() + case let .commaJoinedOptionAndPaths(option, paths): + return (option + paths.map(\.name).joined(separator: ",")).spm_shellEscaped() case let .squashedArgumentList(option, args): return (option + args.joinedUnresolvedArguments).spm_shellEscaped() } @@ -207,6 +209,8 @@ extension Array where Element == Job.ArgTemplate { return "@\(path.name)" case let .joinedOptionAndPath(option, path): return option + path.name + case let .commaJoinedOptionAndPaths(option, paths): + return option + paths.map(\.name).joined(separator: ",") case let .squashedArgumentList(option, args): return option + args.joinedUnresolvedArguments } diff --git a/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift b/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift index 0c57ae11f..3a96363dc 100644 --- a/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift +++ b/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift @@ -243,11 +243,11 @@ extension Driver { try commandLine.appendLast(.RpassMissedEQ, from: &parsedOptions) try commandLine.appendLast(.suppressWarnings, from: &parsedOptions) try commandLine.appendLast(.profileGenerate, from: &parsedOptions) - try commandLine.appendLast(.profileUse, from: &parsedOptions) + try addLastArgumentWithPath(.profileUse, to: &commandLine, remap: jobNeedPathRemap) try commandLine.appendLast(.profileCoverageMapping, from: &parsedOptions) try commandLine.appendLast(.debugInfoForProfiling, from: &parsedOptions) if parsedOptions.hasArgument(.profileSampleUse) { - try commandLine.appendLast(.profileSampleUse, from: &parsedOptions) + try addLastArgumentWithPath(.profileSampleUse, to: &commandLine, remap: jobNeedPathRemap) // Use LLVM's "profi" to infer missing sample data from the profile. commandLine.appendFlag(.Xllvm) commandLine.appendFlag("-sample-profile-use-profi") @@ -1249,15 +1249,23 @@ extension Driver { } 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) + if option.option.kind == .commaJoined || option.option.kind == .multiArg { + let paths = try option.argument.asMultiple.map { try VirtualPath(path: $0) } + try addPathOptions(option: option.option, paths: paths, to: &commandLine, remap: remap) + } else { + let path = try VirtualPath(path: option.argument.asSingle) + try addPathOption(option: option.option, path: path, to: &commandLine, remap: remap) + } + } + + private mutating func needsPathRemapping(for option: Option, remap: Bool) -> Bool { + remap && isCachingEnabled && option.attributes.contains(.argumentIsPath) && + !option.attributes.contains(.cacheInvariant) } public mutating func addPathOption(option: Option, path: VirtualPath, to commandLine: inout [Job.ArgTemplate], remap: Bool = true) throws { assert(option.kind != .commaJoined && option.kind != .multiArg) - let needRemap = remap && isCachingEnabled && option.attributes.contains(.argumentIsPath) && - !option.attributes.contains(.cacheInvariant) - let commandPath = needRemap ? remapPath(path) : path + let commandPath = needsPathRemapping(for: option, remap: remap) ? remapPath(path) : path if option.kind == .joined { commandLine.append(.joinedOptionAndPath(option.spelling, commandPath)) } else { @@ -1269,12 +1277,17 @@ extension Driver { public mutating func addPathOptions(option: Option, paths: [VirtualPath], to commandLine: inout [Job.ArgTemplate], remap: Bool = true) throws { assert(option.kind == .commaJoined || option.kind == .multiArg) - let commandPaths = paths.map { - let needRemap = remap && isCachingEnabled && option.attributes.contains(.argumentIsPath) && - !option.attributes.contains(.cacheInvariant) - return needRemap ? remapPath($0).name : $0.name + let needRemap = needsPathRemapping(for: option, remap: remap) + if option.kind == .commaJoined { + let commandPaths = paths.map { needRemap ? remapPath($0) : $0 } + commandLine.append(.commaJoinedOptionAndPaths(option.spelling, commandPaths)) + } else { + commandLine.appendFlag(option) + for path in paths { + let commandPath = needRemap ? remapPath(path) : path + commandLine.appendPath(commandPath) + } } - commandLine.appendFlag(option.spelling + commandPaths.joined(separator: ",")) } /// Helper function to add last argument with path to command-line. diff --git a/Sources/SwiftDriver/Jobs/Job.swift b/Sources/SwiftDriver/Jobs/Job.swift index 0f0c52c07..ae507f0ce 100644 --- a/Sources/SwiftDriver/Jobs/Job.swift +++ b/Sources/SwiftDriver/Jobs/Job.swift @@ -60,6 +60,9 @@ public struct Job: Codable, Equatable, Hashable { /// Represents a joined option+path combo. case joinedOptionAndPath(String, VirtualPath) + /// Represents a comma-joined option with multiple paths (e.g., `-option/path1,/path2`). + case commaJoinedOptionAndPaths(String, [VirtualPath]) + /// Represents a list of arguments squashed together and passed as a single argument. case squashedArgumentList(option: String, args: [ArgTemplate]) } @@ -321,12 +324,16 @@ extension Job.Kind { extension Job.ArgTemplate: Codable { private enum CodingKeys: String, CodingKey { - case flag, path, responseFilePath, joinedOptionAndPath, squashedArgumentList + case flag, path, responseFilePath, joinedOptionAndPath, commaJoinedOptionAndPaths, squashedArgumentList enum JoinedOptionAndPathCodingKeys: String, CodingKey { case option, path } + enum CommaJoinedOptionAndPathsCodingKeys: String, CodingKey { + case option, paths + } + enum SquashedArgumentListCodingKeys: String, CodingKey { case option, args } @@ -350,6 +357,12 @@ extension Job.ArgTemplate: Codable { forKey: .joinedOptionAndPath) try keyedContainer.encode(option, forKey: .option) try keyedContainer.encode(path, forKey: .path) + case let .commaJoinedOptionAndPaths(option, paths): + var keyedContainer = container.nestedContainer( + keyedBy: CodingKeys.CommaJoinedOptionAndPathsCodingKeys.self, + forKey: .commaJoinedOptionAndPaths) + try keyedContainer.encode(option, forKey: .option) + try keyedContainer.encode(paths, forKey: .paths) case .squashedArgumentList(option: let option, args: let args): var keyedContainer = container.nestedContainer( keyedBy: CodingKeys.SquashedArgumentListCodingKeys.self, @@ -383,6 +396,12 @@ extension Job.ArgTemplate: Codable { forKey: .joinedOptionAndPath) self = .joinedOptionAndPath(try keyedValues.decode(String.self, forKey: .option), try keyedValues.decode(VirtualPath.self, forKey: .path)) + case .commaJoinedOptionAndPaths: + let keyedValues = try values.nestedContainer( + keyedBy: CodingKeys.CommaJoinedOptionAndPathsCodingKeys.self, + forKey: .commaJoinedOptionAndPaths) + self = .commaJoinedOptionAndPaths(try keyedValues.decode(String.self, forKey: .option), + try keyedValues.decode([VirtualPath].self, forKey: .paths)) case .squashedArgumentList: let keyedValues = try values.nestedContainer( keyedBy: CodingKeys.SquashedArgumentListCodingKeys.self, diff --git a/Tests/SwiftDriverTests/CachingBuildTests.swift b/Tests/SwiftDriverTests/CachingBuildTests.swift index 6a6b17c2d..559af5cb0 100644 --- a/Tests/SwiftDriverTests/CachingBuildTests.swift +++ b/Tests/SwiftDriverTests/CachingBuildTests.swift @@ -861,6 +861,79 @@ final class CachingBuildTests: XCTestCase { } } + func testCommaJoinedPathRemapping() throws { + #if os(Windows) + try XCTSkipIf(true, "Skipping due to improper path mapping handling.") + #endif + + try withTemporaryDirectory { path in + let main = path.appending(component: "testCommaJoinedPathRemapping.swift") + try localFileSystem.writeFileContents(main) { + $0.send("import C;") + $0.send("import E;") + $0.send("import G;") + } + + // Create dummy profdata files so the driver doesn't emit missing data errors. + let profdata1 = path.appending(component: "prof1.profdata") + let profdata2 = path.appending(component: "prof2.profdata") + try localFileSystem.writeFileContents(profdata1, bytes: .init()) + try localFileSystem.writeFileContents(profdata2, bytes: .init()) + + 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()) ?? [] + let dependencyOracle = InterModuleDependencyOracle() + var driver = try Driver(args: ["swiftc", + "-I", cHeadersPath.nativePathString(escaped: false), + "-I", swiftModuleInterfacesPath.nativePathString(escaped: false), + "-g", "-explicit-module-build", + "-cache-compile-job", "-cas-path", casPath.nativePathString(escaped: false), + "-working-directory", path.nativePathString(escaped: false), + "-disable-clang-target", + "-scanner-prefix-map-paths", path.nativePathString(escaped: false), "/^tmp", + "-profile-use=" + profdata1.nativePathString(escaped: false) + "," + profdata2.nativePathString(escaped: false), + main.nativePathString(escaped: false)] + sdkArgumentsForTesting, + interModuleDependencyOracle: dependencyOracle) + guard driver.isFrontendArgSupported(.scannerPrefixMapPaths) else { + throw XCTSkip("frontend doesn't support prefix map") + } + let scanLibPath = try XCTUnwrap(driver.getSwiftScanLibPath()) + try dependencyOracle.verifyOrCreateScannerInstance(swiftScanLibPath: scanLibPath) + let resolver = try ArgsResolver(fileSystem: localFileSystem) + + 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 that -profile-use= paths are remapped and don't contain the original temp path. + for arg in command { + if arg.hasPrefix("-profile-use=") { + let paths = String(arg.dropFirst("-profile-use=".count)) + for profilePath in paths.split(separator: ",") { + XCTAssertTrue(profilePath.starts(with: "/^tmp"), + "Expected remapped profile path, got: \(profilePath)") + } + } + } + // Verify no unremapped temp paths appear (except -cas-path). + for i in 0..= 2 && command[i - 2] == "-cache-replay-prefix-map" { continue } + XCTAssertFalse(command[i] != casPath.description && command[i].starts(with: path.description), + "Found unremapped path: \(command[i])") + } + } + XCTAssertFalse(driver.diagnosticEngine.hasErrors) + } + } + func testCacheIncrementalBuildPlan() throws { try withTemporaryDirectory { path in try localFileSystem.changeCurrentWorkingDirectory(to: path) diff --git a/Tests/SwiftDriverTests/SwiftDriverTests.swift b/Tests/SwiftDriverTests/SwiftDriverTests.swift index bb087af4a..2c73770ed 100644 --- a/Tests/SwiftDriverTests/SwiftDriverTests.swift +++ b/Tests/SwiftDriverTests/SwiftDriverTests.swift @@ -9467,7 +9467,7 @@ private extension Array where Element == Job.ArgTemplate { switch $0 { case let .path(path): return path.basename == basename - case .flag, .responseFilePath, .joinedOptionAndPath, .squashedArgumentList: + case .flag, .responseFilePath, .joinedOptionAndPath, .commaJoinedOptionAndPaths, .squashedArgumentList: return false } }