From 3af215f8b955255ac8d3edbdac9299a86dab14a7 Mon Sep 17 00:00:00 2001 From: Artem Chikin Date: Fri, 12 Sep 2025 15:57:42 -0700 Subject: [PATCH 1/2] Use UUID to unique response file names, instead of content hash We've been having issues with content hash causing collisions as well as issues with underlying Foundation atomic APIs. For now, attempt to simply make the files unique with a uuid. --- Sources/SwiftDriver/Execution/ArgsResolver.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftDriver/Execution/ArgsResolver.swift b/Sources/SwiftDriver/Execution/ArgsResolver.swift index 0e5470988..5f6b963fd 100644 --- a/Sources/SwiftDriver/Execution/ArgsResolver.swift +++ b/Sources/SwiftDriver/Execution/ArgsResolver.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import class Foundation.NSLock +import struct Foundation.UUID import func TSCBasic.withTemporaryDirectory import protocol TSCBasic.FileSystem @@ -183,14 +184,13 @@ public final class ArgsResolver { (job.supportsResponseFiles && !commandLineFitsWithinSystemLimits(path: resolvedArguments[0], args: resolvedArguments)) { assert(!forceResponseFiles || job.supportsResponseFiles, "Platform does not support response files for job: \(job)") - // Match the integrated driver's behavior, which uses response file names of the form "arguments-[0-9a-zA-Z].resp". - let hash = SHA256().hash(resolvedArguments.joined(separator: " ")).hexadecimalRepresentation - let responseFilePath = temporaryDirectory.appending(component: "arguments-\(hash).resp") + let uuid = UUID().uuidString + let responseFilePath = temporaryDirectory.appending(component: "arguments-\(uuid).resp") // FIXME: Need a way to support this for distributed build systems... if let absPath = responseFilePath.absolutePath { let argumentBytes = ByteString(resolvedArguments[2...].map { $0.spm_shellEscaped() }.joined(separator: "\n").utf8) - try fileSystem.writeFileContents(absPath, bytes: argumentBytes, atomically: true) + try fileSystem.writeFileContents(absPath, bytes: argumentBytes) resolvedArguments = [resolvedArguments[0], resolvedArguments[1], "@\(absPath.pathString)"] } From d18dfc40a3468a5a896881688a61966e3efe2e39 Mon Sep 17 00:00:00 2001 From: Artem Chikin Date: Fri, 12 Sep 2025 16:29:21 -0700 Subject: [PATCH 2/2] Add 'resolveArgumentList' API which contains original command-line if a response file is used --- .../SwiftDriver/Execution/ArgsResolver.swift | 17 ++++++++ Tests/SwiftDriverTests/SwiftDriverTests.swift | 43 ++++++++++--------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/Sources/SwiftDriver/Execution/ArgsResolver.swift b/Sources/SwiftDriver/Execution/ArgsResolver.swift index 5f6b963fd..e8086dbd8 100644 --- a/Sources/SwiftDriver/Execution/ArgsResolver.swift +++ b/Sources/SwiftDriver/Execution/ArgsResolver.swift @@ -26,6 +26,11 @@ public enum ResponseFileHandling { case heuristic } +public enum ResolvedCommandLine { + case plain([String]) + case usingResponseFile(resolved: [String], responseFileContents: [String]) +} + /// Resolver for a job's argument template. public final class ArgsResolver { /// The map of virtual path to the actual path. @@ -76,6 +81,18 @@ public final class ArgsResolver { return (arguments, usingResponseFile) } + public func resolveArgumentList(for job: Job, useResponseFiles: ResponseFileHandling = .heuristic) + throws -> ResolvedCommandLine { + let tool = try resolve(.path(job.tool)) + let resolvedArguments = [tool] + (try resolveArgumentList(for: job.commandLine)) + var actualArguments = resolvedArguments + let usingResponseFile = try createResponseFileIfNeeded(for: job, resolvedArguments: &actualArguments, + useResponseFiles: useResponseFiles) + return usingResponseFile ? .usingResponseFile(resolved: actualArguments, + responseFileContents: resolvedArguments) + : .plain(actualArguments) + } + public func resolveArgumentList(for commandLine: [Job.ArgTemplate]) throws -> [String] { return try commandLine.map { try resolve($0) } } diff --git a/Tests/SwiftDriverTests/SwiftDriverTests.swift b/Tests/SwiftDriverTests/SwiftDriverTests.swift index 8b46640f7..ec7427571 100644 --- a/Tests/SwiftDriverTests/SwiftDriverTests.swift +++ b/Tests/SwiftDriverTests/SwiftDriverTests.swift @@ -1823,37 +1823,38 @@ final class SwiftDriverTests: XCTestCase { } } - // No response file + // Response file query with full command-line API do { - var driver = try Driver(args: ["swift"] + ["foo.swift"]) + let source = try AbsolutePath(validating: "/foo.swift") + var driver = try Driver(args: ["swift"] + [source.nativePathString(escaped: false)]) let jobs = try driver.planBuild() XCTAssertEqual(jobs.count, 1) XCTAssertEqual(jobs[0].kind, .interpret) let interpretJob = jobs[0] let resolver = try ArgsResolver(fileSystem: localFileSystem) - let resolvedArgs: [String] = try resolver.resolveArgumentList(for: interpretJob) - XCTAssertFalse(resolvedArgs.contains { $0.hasPrefix("@") }) + let resolved: ResolvedCommandLine = try resolver.resolveArgumentList(for: interpretJob, useResponseFiles: .forced) + guard case .usingResponseFile(resolved: let resolvedArgs, responseFileContents: let contents) = resolved else { + XCTFail("Argument wasn't a response file") + return + } + XCTAssertEqual(resolvedArgs.count, 3) + XCTAssertEqual(resolvedArgs[1], "-frontend") + XCTAssertEqual(resolvedArgs[2].first, "@") + + XCTAssertTrue(contents.contains(subsequence: ["-frontend", "-interpret"])) + XCTAssertTrue(contents.contains(subsequence: ["-module-name", "foo"])) } - } - func testResponseFileDeterministicNaming() throws { -#if !os(macOS) - try XCTSkipIf(true, "Test assumes macOS response file quoting behavior") -#endif + // No response file do { - let testJob = Job(moduleName: "Foo", - kind: .compile, - tool: .init(path: try AbsolutePath(validating: "/swiftc"), supportsResponseFiles: true), - commandLine: (1...20000).map { .flag("-DTEST_\($0)") }, - inputs: [], - primaryInputs: [], - outputs: []) + var driver = try Driver(args: ["swift"] + ["foo.swift"]) + let jobs = try driver.planBuild() + XCTAssertEqual(jobs.count, 1) + XCTAssertEqual(jobs[0].kind, .interpret) + let interpretJob = jobs[0] let resolver = try ArgsResolver(fileSystem: localFileSystem) - let resolvedArgs: [String] = try resolver.resolveArgumentList(for: testJob) - XCTAssertEqual(resolvedArgs.count, 3) - XCTAssertEqual(resolvedArgs[2].first, "@") - let responseFilePath = try AbsolutePath(validating: String(resolvedArgs[2].dropFirst())) - XCTAssertEqual(responseFilePath.basename, "arguments-847d15e70d97df7c18033735497ca8dcc4441f461d5a9c2b764b127004524e81.resp") + let resolvedArgs: [String] = try resolver.resolveArgumentList(for: interpretJob) + XCTAssertFalse(resolvedArgs.contains { $0.hasPrefix("@") }) } }