Skip to content

Commit 90bc104

Browse files
authored
Merge pull request swiftlang#57 from owenv/response-files
Use response files when running subcommands if needed
2 parents 9d4cdd7 + ae3f312 commit 90bc104

File tree

12 files changed

+236
-62
lines changed

12 files changed

+236
-62
lines changed

Sources/SwiftDriver/Driver/Driver.swift

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -423,48 +423,61 @@ extension Driver {
423423
/// This method supports response files with:
424424
/// 1. Double slash comments at the beginning of a line.
425425
/// 2. Backslash escaping.
426-
/// 3. Space character (U+0020 SPACE).
426+
/// 3. Shell Quoting
427427
///
428-
/// - Returns: One line String ready to be used in the shell, if any.
428+
/// - Returns: An array of 0 or more command line arguments
429429
///
430430
/// - Complexity: O(*n*), where *n* is the length of the line.
431-
private static func tokenizeResponseFileLine<S: StringProtocol>(_ line: S) -> String? {
432-
if line.isEmpty { return nil }
433-
431+
private static func tokenizeResponseFileLine<S: StringProtocol>(_ line: S) -> [String] {
434432
// Support double dash comments only if they start at the beginning of a line.
435-
if line.hasPrefix("//") { return nil }
436-
437-
var result: String = ""
438-
/// Indicates if we just parsed an escaping backslash.
433+
if line.hasPrefix("//") { return [] }
434+
435+
var tokens: [String] = []
436+
var token: String = ""
437+
// Conservatively assume ~1 token per line.
438+
token.reserveCapacity(line.count)
439+
// Indicates if we just parsed an escaping backslash.
439440
var isEscaping = false
441+
// Indicates if we are currently parsing quoted text.
442+
var quoted = false
440443

441444
for char in line {
442-
if char.isNewline { return result }
443-
444445
// Backslash escapes to the next character.
445446
if char == #"\"#, !isEscaping {
446447
isEscaping = true
447448
continue
448449
} else if isEscaping {
449450
// Disable escaping and keep parsing.
450451
isEscaping = false
452+
} else if char.isShellQuote {
453+
// If an unescaped shell quote appears, begin or end quoting.
454+
quoted.toggle()
455+
continue
456+
} else if char.isWhitespace && !quoted {
457+
// This is unquoted, unescaped whitespace, start a new token.
458+
tokens.append(token)
459+
token = ""
460+
continue
451461
}
452-
453-
// Ignore spacing characters, except by the space character.
454-
if char.isWhitespace && char != " " { continue }
455-
456-
result.append(char)
462+
463+
token.append(char)
457464
}
458-
return result.isEmpty ? nil : result
465+
// Add the final token
466+
tokens.append(token)
467+
468+
// Filter any empty tokens that might be parsed if there's excessive whitespace.
469+
return tokens.filter { !$0.isEmpty }
459470
}
460471

461472
/// Tokenize each line of the response file, omitting empty lines.
462473
///
463474
/// - Parameter content: response file's content to be tokenized.
464475
private static func tokenizeResponseFile(_ content: String) -> [String] {
465-
return content
466-
.split(separator: "\n")
467-
.compactMap { tokenizeResponseFileLine($0) }
476+
#if !os(macOS) && !os(Linux)
477+
#warning("Response file tokenization unimplemented for platform; behavior may be incorrect")
478+
#endif
479+
return content.split { $0 == "\n" || $0 == "\r\n" }
480+
.flatMap { tokenizeResponseFileLine($0) }
468481
}
469482

470483
/// Recursively expands the response files.
@@ -574,10 +587,12 @@ extension Driver {
574587

575588
if jobs.isEmpty { return }
576589

590+
let forceResponseFiles = parsedOptions.contains(.driverForceResponseFiles)
591+
577592
// If we're only supposed to print the jobs, do so now.
578593
if parsedOptions.contains(.driverPrintJobs) {
579594
for job in jobs {
580-
print(job)
595+
try Self.printJob(job, resolver: resolver, forceResponseFiles: forceResponseFiles)
581596
}
582597
return
583598
}
@@ -591,7 +606,7 @@ extension Driver {
591606

592607
if jobs.contains(where: { $0.requiresInPlaceExecution }) {
593608
assert(jobs.count == 1, "Cannot execute in place for multi-job build plans")
594-
return try executeJobInPlace(jobs[0], resolver: resolver)
609+
return try executeJobInPlace(jobs[0], resolver: resolver, forceResponseFiles: forceResponseFiles)
595610
}
596611

597612
// Create and use the tool execution delegate if one is not provided explicitly.
@@ -602,7 +617,8 @@ extension Driver {
602617
jobs: jobs, resolver: resolver,
603618
executorDelegate: executorDelegate,
604619
numParallelJobs: numParallelJobs,
605-
processSet: processSet
620+
processSet: processSet,
621+
forceResponseFiles: forceResponseFiles
606622
)
607623
try jobExecutor.execute(env: env)
608624
}
@@ -621,16 +637,32 @@ extension Driver {
621637
}
622638

623639
/// Execute a single job in-place.
624-
private func executeJobInPlace(_ job: Job, resolver: ArgsResolver) throws {
625-
let tool = try resolver.resolve(.path(job.tool))
626-
let commandLine = try job.commandLine.map{ try resolver.resolve($0) }
627-
let arguments = [tool] + commandLine
640+
private func executeJobInPlace(_ job: Job, resolver: ArgsResolver, forceResponseFiles: Bool) throws {
641+
let arguments: [String] = try resolver.resolveArgumentList(for: job, forceResponseFiles: forceResponseFiles)
628642

629643
for (envVar, value) in job.extraEnvironment {
630644
try ProcessEnv.setVar(envVar, value: value)
631645
}
632646

633-
return try exec(path: tool, args: arguments)
647+
return try exec(path: arguments[0], args: arguments)
648+
}
649+
650+
public static func printJob(_ job: Job, resolver: ArgsResolver, forceResponseFiles: Bool) throws {
651+
let (args, usedResponseFile) = try resolver.resolveArgumentList(for: job, forceResponseFiles: forceResponseFiles)
652+
var result = args.joined(separator: " ")
653+
654+
if usedResponseFile {
655+
// Print the response file arguments as a comment.
656+
result += " # \(job.commandLine.joinedArguments)"
657+
}
658+
659+
if !job.extraEnvironment.isEmpty {
660+
result += " #"
661+
for (envVar, val) in job.extraEnvironment {
662+
result += " \(envVar)=\(val)"
663+
}
664+
}
665+
print(result)
634666
}
635667

636668
private func printVersion<S: OutputByteStream>(outputStream: inout S) throws {

Sources/SwiftDriver/Execution/JobExecutor.swift

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ public struct ArgsResolver {
3232
}
3333
}
3434

35+
public func resolveArgumentList(for job: Job, forceResponseFiles: Bool) throws -> [String] {
36+
let (arguments, _) = try resolveArgumentList(for: job, forceResponseFiles: forceResponseFiles)
37+
return arguments
38+
}
39+
40+
public func resolveArgumentList(for job: Job, forceResponseFiles: Bool) throws -> ([String], usingResponseFile: Bool) {
41+
let tool = try resolve(.path(job.tool))
42+
var arguments = [tool] + (try job.commandLine.map { try resolve($0) })
43+
let usingResponseFile = try createResponseFileIfNeeded(for: job, resolvedArguments: &arguments,
44+
forceResponseFiles: forceResponseFiles)
45+
return (arguments, usingResponseFile)
46+
}
47+
3548
/// Resolve the given argument.
3649
public func resolve(_ arg: Job.ArgTemplate) throws -> String {
3750
switch arg {
@@ -55,6 +68,22 @@ public struct ArgsResolver {
5568
}
5669
}
5770

71+
private func createResponseFileIfNeeded(for job: Job, resolvedArguments: inout [String], forceResponseFiles: Bool) throws -> Bool {
72+
if forceResponseFiles ||
73+
(job.supportsResponseFiles && !commandLineFitsWithinSystemLimits(path: resolvedArguments[0], args: resolvedArguments)) {
74+
assert(!forceResponseFiles || job.supportsResponseFiles,
75+
"Platform does not support response files for job: \(job)")
76+
// Match the integrated driver's behavior, which uses response file names of the form "arguments-[0-9a-zA-Z].resp".
77+
let responseFilePath = temporaryDirectory.appending(component: "arguments-\(abs(job.hashValue)).resp")
78+
try localFileSystem.writeFileContents(responseFilePath) {
79+
$0 <<< resolvedArguments[1...].map{ $0.spm_shellEscaped() }.joined(separator: "\n")
80+
}
81+
resolvedArguments = [resolvedArguments[0], "@\(responseFilePath.pathString)"]
82+
return true
83+
}
84+
return false
85+
}
86+
5887
/// Remove the temporary directory from disk.
5988
public func removeTemporaryDirectory() throws {
6089
_ = try FileManager.default.removeItem(atPath: temporaryDirectory.pathString)
@@ -106,20 +135,25 @@ public final class JobExecutor {
106135
/// The process set to use when launching new processes.
107136
let processSet: ProcessSet?
108137

138+
/// If true, always use response files to pass command line arguments.
139+
let forceResponseFiles: Bool
140+
109141
init(
110142
argsResolver: ArgsResolver,
111143
env: [String: String],
112144
producerMap: [VirtualPath: Job],
113145
executorDelegate: JobExecutorDelegate,
114146
jobQueue: OperationQueue,
115-
processSet: ProcessSet?
147+
processSet: ProcessSet?,
148+
forceResponseFiles: Bool
116149
) {
117150
self.producerMap = producerMap
118151
self.argsResolver = argsResolver
119152
self.env = env
120153
self.executorDelegate = executorDelegate
121154
self.jobQueue = jobQueue
122155
self.processSet = processSet
156+
self.forceResponseFiles = forceResponseFiles
123157
}
124158
}
125159

@@ -138,18 +172,23 @@ public final class JobExecutor {
138172
/// The process set to use when launching new processes.
139173
let processSet: ProcessSet?
140174

175+
/// If true, always use response files to pass command line arguments.
176+
let forceResponseFiles: Bool
177+
141178
public init(
142179
jobs: [Job],
143180
resolver: ArgsResolver,
144181
executorDelegate: JobExecutorDelegate,
145182
numParallelJobs: Int? = nil,
146-
processSet: ProcessSet? = nil
183+
processSet: ProcessSet? = nil,
184+
forceResponseFiles: Bool = false
147185
) {
148186
self.jobs = jobs
149187
self.argsResolver = resolver
150188
self.executorDelegate = executorDelegate
151189
self.numParallelJobs = numParallelJobs ?? 1
152190
self.processSet = processSet
191+
self.forceResponseFiles = forceResponseFiles
153192
}
154193

155194
/// Execute all jobs.
@@ -187,7 +226,8 @@ public final class JobExecutor {
187226
producerMap: producerMap,
188227
executorDelegate: executorDelegate,
189228
jobQueue: jobQueue,
190-
processSet: processSet
229+
processSet: processSet,
230+
forceResponseFiles: forceResponseFiles
191231
)
192232
}
193233
}
@@ -339,9 +379,8 @@ class ExecuteJobRule: LLBuildRule {
339379
let value: DriverBuildValue
340380
var pid = 0
341381
do {
342-
let tool = try resolver.resolve(.path(job.tool))
343-
let commandLine = try job.commandLine.map{ try resolver.resolve($0) }
344-
let arguments = [tool] + commandLine
382+
let arguments: [String] = try resolver.resolveArgumentList(for: job,
383+
forceResponseFiles: context.forceResponseFiles)
345384

346385
let process = try context.executorDelegate.launchProcess(
347386
for: job, arguments: arguments, env: env

Sources/SwiftDriver/Jobs/AutolinkExtractJob.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ extension Driver {
3333
tool: .absolute(try toolchain.getToolPath(.swiftAutolinkExtract)),
3434
commandLine: commandLine,
3535
inputs: inputs,
36-
outputs: [.init(file: output, type: .autolink)]
36+
outputs: [.init(file: output, type: .autolink)],
37+
supportsResponseFiles: true
3738
)
3839
}
3940
}

Sources/SwiftDriver/Jobs/CompileJob.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ extension Driver {
158158
commandLine: commandLine,
159159
displayInputs: primaryInputs,
160160
inputs: inputs,
161-
outputs: outputs
161+
outputs: outputs,
162+
supportsResponseFiles: true
162163
)
163164
}
164165
}

Sources/SwiftDriver/Jobs/GeneratePCHJob.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ extension Driver {
6767
commandLine: commandLine,
6868
displayInputs: [],
6969
inputs: inputs,
70-
outputs: outputs
70+
outputs: outputs,
71+
supportsResponseFiles: true
7172
)
7273
}
7374
}

Sources/SwiftDriver/Jobs/InterpretJob.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ extension Driver {
4747
inputs:inputs,
4848
outputs: [],
4949
extraEnvironment: extraEnvironment,
50-
requiresInPlaceExecution: true
50+
requiresInPlaceExecution: true,
51+
supportsResponseFiles: true
5152
)
5253
}
5354
}

Sources/SwiftDriver/Jobs/Job.swift

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import TSCBasic
1313

1414
/// A job represents an individual subprocess that should be invoked during compilation.
15-
public struct Job: Codable, Equatable {
15+
public struct Job: Codable, Equatable, Hashable {
1616
public enum Kind: String, Codable {
1717
case compile
1818
case mergeModule = "merge-module"
@@ -31,7 +31,7 @@ public struct Job: Codable, Equatable {
3131
case versionRequest = "version-request"
3232
}
3333

34-
public enum ArgTemplate: Equatable {
34+
public enum ArgTemplate: Equatable, Hashable {
3535
/// Represents a command-line flag that is substitued as-is.
3636
case flag(String)
3737

@@ -45,6 +45,9 @@ public struct Job: Codable, Equatable {
4545
/// The command-line arguments of the job.
4646
public var commandLine: [ArgTemplate]
4747

48+
/// Whether or not the job supports using response files to pass command line arguments.
49+
public var supportsResponseFiles: Bool
50+
4851
/// The list of inputs to use for displaying purposes.
4952
public var displayInputs: [TypedVirtualPath]
5053

@@ -71,7 +74,8 @@ public struct Job: Codable, Equatable {
7174
inputs: [TypedVirtualPath],
7275
outputs: [TypedVirtualPath],
7376
extraEnvironment: [String: String] = [:],
74-
requiresInPlaceExecution: Bool = false
77+
requiresInPlaceExecution: Bool = false,
78+
supportsResponseFiles: Bool = false
7579
) {
7680
self.kind = kind
7781
self.tool = tool
@@ -81,21 +85,7 @@ public struct Job: Codable, Equatable {
8185
self.outputs = outputs
8286
self.extraEnvironment = extraEnvironment
8387
self.requiresInPlaceExecution = requiresInPlaceExecution
84-
}
85-
}
86-
87-
extension Job: CustomStringConvertible {
88-
public var description: String {
89-
var result: String = "\(tool.name) \(commandLine.joinedArguments)"
90-
91-
if !self.extraEnvironment.isEmpty {
92-
result += " #"
93-
for (envVar, val) in extraEnvironment {
94-
result += " \(envVar)=\(val)"
95-
}
96-
}
97-
98-
return result
88+
self.supportsResponseFiles = supportsResponseFiles
9989
}
10090
}
10191

Sources/SwiftDriver/Jobs/LinkJob.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ extension Driver {
5151
targetTriple: targetTriple
5252
)
5353

54+
// TODO: some, but not all, linkers support response files.
5455
return Job(
5556
kind: .link,
5657
tool: .absolute(toolPath),

Sources/SwiftDriver/Jobs/MergeModuleJob.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ extension Driver {
5454
tool: .absolute(try toolchain.getToolPath(.swiftCompiler)),
5555
commandLine: commandLine,
5656
inputs: inputs,
57-
outputs: outputs
57+
outputs: outputs,
58+
supportsResponseFiles: true
5859
)
5960
}
6061
}

Sources/SwiftDriver/Utilities/StringAdditions.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,9 @@ extension Unicode.Scalar {
133133
}
134134
}
135135
}
136+
137+
extension Character {
138+
var isShellQuote: Bool {
139+
return self == "\"" || self == "'"
140+
}
141+
}

0 commit comments

Comments
 (0)