Skip to content

Commit d72c6bf

Browse files
committed
[Dependency Scanning] Unify path-escaping handling using TSC's spm_shellEscaped
Command lines generated for libSwiftScan (either for actual dependency scanning queries, or for target-info queries) do not go through the shell, and are instead tokenized at the entry-points (inside libSwiftScan). In order for them to be correclty tokenized, we must ensure that all the various filepaths are escaped in case they contain whitespace characters, to ensure they get treated as a whole path, rather than multiple strings. The driver previously relied on a separate mechanism to do this for libSwiftScan command lines, by getting the 'ArgsResolver' to always escape '.path' arguments. This turned out to be insufficient, because it happens to miss a whole class of paths specified on the command-line that the driver cannot/doesn't recognize as paths: arguments forwarded to tools, such as '-Xcc' or '-Xfrontend' or '-Xclang-linker'. As a result, it is possible for such paths to end-up unescaped and fail to get tokenized correctly by libSwiftScan. Instead, this change switches the formulation of these command-lines to use the existing 'spm_shellEscaped' mechanism from TSC which is a robust way to achieve exactly the desired behavior: produce shell-escaped command-line arguments by detecting flags/arguments that contain characters that would prevent correct tokenization: whitespaces, etc, and only quote-escaping them, and doing so in a platform-appropriate manner (e.g. using '"' instead of "'" on Windows) Resolves rdar://108971395
1 parent 661d59f commit d72c6bf

File tree

3 files changed

+47
-28
lines changed

3 files changed

+47
-28
lines changed

Sources/SwiftDriver/Execution/ArgsResolver.swift

+20-22
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,16 @@ public final class ArgsResolver {
5757
}
5858
}
5959

60-
public func resolveArgumentList(for job: Job, useResponseFiles: ResponseFileHandling = .heuristic,
61-
quotePaths: Bool = false) throws -> [String] {
62-
let (arguments, _) = try resolveArgumentList(for: job, useResponseFiles: useResponseFiles,
63-
quotePaths: quotePaths)
60+
public func resolveArgumentList(for job: Job, useResponseFiles: ResponseFileHandling = .heuristic)
61+
throws -> [String] {
62+
let (arguments, _) = try resolveArgumentList(for: job, useResponseFiles: useResponseFiles)
6463
return arguments
6564
}
6665

67-
public func resolveArgumentList(for job: Job, useResponseFiles: ResponseFileHandling = .heuristic,
68-
quotePaths: Bool = false) throws -> ([String], usingResponseFile: Bool) {
69-
let tool = try resolve(.path(job.tool), quotePaths: quotePaths)
70-
var arguments = [tool] + (try job.commandLine.map { try resolve($0, quotePaths: quotePaths) })
66+
public func resolveArgumentList(for job: Job, useResponseFiles: ResponseFileHandling = .heuristic)
67+
throws -> ([String], usingResponseFile: Bool) {
68+
let tool = try resolve(.path(job.tool))
69+
var arguments = [tool] + (try job.commandLine.map { try resolve($0) })
7170
let usingResponseFile = try createResponseFileIfNeeded(for: job, resolvedArguments: &arguments,
7271
useResponseFiles: useResponseFiles)
7372
return (arguments, usingResponseFile)
@@ -77,46 +76,45 @@ public final class ArgsResolver {
7776
public func resolveArgumentList(for job: Job, forceResponseFiles: Bool,
7877
quotePaths: Bool = false) throws -> [String] {
7978
let useResponseFiles: ResponseFileHandling = forceResponseFiles ? .forced : .heuristic
80-
return try resolveArgumentList(for: job, useResponseFiles: useResponseFiles, quotePaths: quotePaths)
79+
return try resolveArgumentList(for: job, useResponseFiles: useResponseFiles)
8180
}
8281

8382
@available(*, deprecated, message: "use resolveArgumentList(for:,useResponseFiles:,quotePaths:)")
8483
public func resolveArgumentList(for job: Job, forceResponseFiles: Bool,
8584
quotePaths: Bool = false) throws -> ([String], usingResponseFile: Bool) {
8685
let useResponseFiles: ResponseFileHandling = forceResponseFiles ? .forced : .heuristic
87-
return try resolveArgumentList(for: job, useResponseFiles: useResponseFiles, quotePaths: quotePaths)
86+
return try resolveArgumentList(for: job, useResponseFiles: useResponseFiles)
8887
}
8988

9089
/// Resolve the given argument.
91-
public func resolve(_ arg: Job.ArgTemplate,
92-
quotePaths: Bool = false) throws -> String {
90+
public func resolve(_ arg: Job.ArgTemplate) throws -> String {
9391
switch arg {
9492
case .flag(let flag):
9593
return flag
9694

9795
case .path(let path):
9896
return try lock.withLock {
99-
return try unsafeResolve(path: path, quotePaths: quotePaths)
97+
return try unsafeResolve(path: path)
10098
}
10199

102100
case .responseFilePath(let path):
103-
return "@\(try resolve(.path(path), quotePaths: quotePaths))"
101+
return "@\(try resolve(.path(path)))"
104102

105103
case let .joinedOptionAndPath(option, path):
106-
return option + (try resolve(.path(path), quotePaths: quotePaths))
104+
return option + (try resolve(.path(path)))
107105

108106
case let .squashedArgumentList(option: option, args: args):
109107
return try option + args.map {
110-
try resolve($0, quotePaths: quotePaths)
108+
try resolve($0)
111109
}.joined(separator: " ")
112110
}
113111
}
114112

115113
/// Needs to be done inside of `lock`. Marked unsafe to make that more obvious.
116-
private func unsafeResolve(path: VirtualPath, quotePaths: Bool) throws -> String {
114+
private func unsafeResolve(path: VirtualPath) throws -> String {
117115
// If there was a path mapping, use it.
118116
if let actualPath = pathMapping[path] {
119-
return quotePaths ? quoteArgument(actualPath) : actualPath
117+
return actualPath
120118
}
121119

122120
// Return the path from the temporary directory if this is a temporary file.
@@ -140,21 +138,21 @@ public final class ArgsResolver {
140138

141139
let result = actualPath.name
142140
pathMapping[path] = result
143-
return quotePaths ? "'\(result)'" : result
141+
return result
144142
}
145143

146144
// Otherwise, return the path.
147145
let result = path.name
148146
pathMapping[path] = result
149-
return quotePaths ? quoteArgument(result) : result
147+
return result
150148
}
151149

152150
private func createFileList(path: VirtualPath, contents: [VirtualPath]) throws {
153151
// FIXME: Need a way to support this for distributed build systems...
154152
if let absPath = path.absolutePath {
155153
try fileSystem.writeFileContents(absPath) { out in
156154
for path in contents {
157-
try! out <<< unsafeResolve(path: path, quotePaths: false) <<< "\n"
155+
try! out <<< unsafeResolve(path: path) <<< "\n"
158156
}
159157
}
160158
}
@@ -184,7 +182,7 @@ public final class ArgsResolver {
184182
}
185183

186184
private func quoteAndEscape(path: VirtualPath) -> String {
187-
let inputNode = Node.scalar(Node.Scalar(try! unsafeResolve(path: path, quotePaths: false),
185+
let inputNode = Node.scalar(Node.Scalar(try! unsafeResolve(path: path),
188186
Tag(.str), .doubleQuoted))
189187
// Width parameter of -1 sets preferred line-width to unlimited so that no extraneous
190188
// line-breaks will be inserted during serialization.

Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift

+5-6
Original file line numberDiff line numberDiff line change
@@ -415,12 +415,11 @@ public extension Driver {
415415

416416
static func itemizedJobCommand(of job: Job, useResponseFiles: ResponseFileHandling,
417417
using resolver: ArgsResolver) throws -> [String] {
418-
// FIXME: this is to walkaround rdar://108769167
419-
let quotePaths = job.kind != .scanDependencies
420-
let (args, _) = try resolver.resolveArgumentList(for: job,
421-
useResponseFiles: useResponseFiles,
422-
quotePaths: quotePaths)
423-
return args
418+
// Because the command-line passed to libSwiftScan does not go through the shell
419+
// we must ensure that we generate a shell-escaped string for all arguments/flags that may
420+
// potentially need it.
421+
return try resolver.resolveArgumentList(for: job,
422+
useResponseFiles: useResponseFiles).0.map { $0.spm_shellEscaped() }
424423
}
425424

426425
static func getRootPath(of toolchain: Toolchain, env: [String: String])

Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift

+22
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,28 @@ final class ExplicitModuleBuildTests: XCTestCase {
15581558
}
15591559
}
15601560

1561+
func testDependencyScanCommandLineEscape() throws {
1562+
#if os(Windows)
1563+
let quoteCharacter: Character = "\""
1564+
#else
1565+
let quoteCharacter: Character = "'"
1566+
#endif
1567+
let swiftInputWithSpace = "/tmp/input example/test.swift"
1568+
let swiftInputWithoutSpace = "/tmp/baz.swift"
1569+
let clangInputWithSpace = "/tmp/input example/bar.o"
1570+
var driver = try Driver(args: ["swiftc", "-explicit-module-build",
1571+
"-module-name", "testDependencyScanning",
1572+
swiftInputWithSpace, swiftInputWithoutSpace,
1573+
"-Xcc", clangInputWithSpace])
1574+
let scanJob = try driver.dependencyScanningJob()
1575+
let scanJobCommand = try Driver.itemizedJobCommand(of: scanJob,
1576+
useResponseFiles: .disabled,
1577+
using: ArgsResolver(fileSystem: InMemoryFileSystem()))
1578+
XCTAssertTrue(scanJobCommand.contains(String(quoteCharacter) + swiftInputWithSpace + String(quoteCharacter)))
1579+
XCTAssertTrue(scanJobCommand.contains(String(quoteCharacter) + clangInputWithSpace + String(quoteCharacter)))
1580+
XCTAssertTrue(scanJobCommand.contains(swiftInputWithoutSpace))
1581+
}
1582+
15611583
func testExplicitSwiftModuleMap() throws {
15621584
let jsonExample : String = """
15631585
[

0 commit comments

Comments
 (0)