From 051e0a32350a76b8869ddfa3dfe34e20e9f3aae3 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 653108201..ae1e6a18e 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 @@ -189,14 +190,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 e01d70677392fc260d79eb88e3bb241b4b4baec4 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 ae1e6a18e..c8315849c 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 4b9b1e549..fdbce98bf 100644 --- a/Tests/SwiftDriverTests/SwiftDriverTests.swift +++ b/Tests/SwiftDriverTests/SwiftDriverTests.swift @@ -1806,37 +1806,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("@") }) } }