Skip to content

Commit 02e2a52

Browse files
committed
Fix Ninja Output for Windows
- Ninja requires that `:`s in filenames be escaped as `$:` - Explicitly call cmd.exe /C on Windows - In addition, escape spaces for the command that we pass to cmd.exe
1 parent c045a1f commit 02e2a52

File tree

1 file changed

+102
-13
lines changed

1 file changed

+102
-13
lines changed

Sources/ISDBTibs/TibsBuilder.swift

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,82 @@ extension TibsBuilder {
257257
return result
258258
}
259259

260+
#if os(Windows)
261+
private func quoteWindowsCommandLine(_ commandLine: [String]) -> String {
262+
func quoteWindowsCommandArg(arg: String) -> String {
263+
// Windows escaping, adapted from Daniel Colascione's "Everyone quotes
264+
// command line arguments the wrong way" - Microsoft Developer Blog
265+
if !arg.contains(where: {" \t\n\"".contains($0)}) {
266+
return arg
267+
}
268+
269+
// To escape the command line, we surround the argument with quotes. However
270+
// the complication comes due to how the Windows command line parser treats
271+
// backslashes (\) and quotes (")
272+
//
273+
// - \ is normally treated as a literal backslash
274+
// - e.g. foo\bar\baz => foo\bar\baz
275+
// - However, the sequence \" is treated as a literal "
276+
// - e.g. foo\"bar => foo"bar
277+
//
278+
// But then what if we are given a path that ends with a \? Surrounding
279+
// foo\bar\ with " would be "foo\bar\" which would be an unterminated string
280+
281+
// since it ends on a literal quote. To allow this case the parser treats:
282+
//
283+
// - \\" as \ followed by the " metachar
284+
// - \\\" as \ followed by a literal "
285+
// - In general:
286+
// - 2n \ followed by " => n \ followed by the " metachar
287+
// - 2n+1 \ followed by " => n \ followed by a literal "
288+
var quoted = "\""
289+
var unquoted = arg.unicodeScalars
290+
291+
while !unquoted.isEmpty {
292+
guard let firstNonBackslash = unquoted.firstIndex(where: { $0 != "\\" }) else {
293+
// String ends with a backslash e.g. foo\bar\, escape all the backslashes
294+
// then add the metachar " below
295+
let backslashCount = unquoted.count
296+
quoted.append(String(repeating: "\\", count: backslashCount * 2))
297+
break
298+
}
299+
let backslashCount = unquoted.distance(from: unquoted.startIndex, to: firstNonBackslash)
300+
if (unquoted[firstNonBackslash] == "\"") {
301+
// This is a string of \ followed by a " e.g. foo\"bar. Escape the
302+
// backslashes and the quote
303+
quoted.append(String(repeating: "\\", count: backslashCount * 2 + 1))
304+
quoted.append(String(unquoted[firstNonBackslash]))
305+
} else {
306+
// These are just literal backslashes
307+
quoted.append(String(repeating: "\\", count: backslashCount))
308+
quoted.append(String(unquoted[firstNonBackslash]))
309+
}
310+
// Drop the backslashes and the following character
311+
unquoted.removeFirst(backslashCount + 1)
312+
}
313+
quoted.append("\"")
314+
return quoted
315+
}
316+
return commandLine.map(quoteWindowsCommandArg).joined(separator: " ")
317+
}
318+
#endif
319+
320+
private func escapeCommand(_ args: [String]) -> String {
321+
let escaped: String
322+
#if os(Windows)
323+
escaped = quoteWindowsCommandLine(args)
324+
#else
325+
escaped = args.joined(separator: " ")
326+
#endif
327+
return escapePath(path: escaped)
328+
}
329+
330+
private func escapePath(path: String) -> String {
331+
// Ninja escapes using $, this only matters during build lines
332+
// since those are terminated by a :
333+
return path.replacingOccurrences(of: ":", with: "$:")
334+
}
335+
260336
public func writeNinja<Output: TextOutputStream>(to stream: inout Output) {
261337
writeNinjaHeader(to: &stream)
262338
stream.write("\n\n")
@@ -276,23 +352,36 @@ extension TibsBuilder {
276352
}
277353

278354
public func writeNinjaRules<Output: TextOutputStream>(to stream: inout Output) {
355+
#if os(Windows)
356+
let callCmd = "cmd.exe /C "
357+
#else
358+
let callCmd = ""
359+
#endif
360+
let swiftIndexCommand = callCmd + """
361+
\(escapeCommand([toolchain.swiftc.path])) $in $IMPORT_PATHS -module-name $MODULE_NAME \
362+
-index-store-path index -index-ignore-system-modules \
363+
-output-file-map $OUTPUT_FILE_MAP \
364+
-emit-module -emit-module-path $MODULE_PATH -emit-dependencies \
365+
-pch-output-dir pch -module-cache-path ModuleCache \
366+
$EMIT_HEADER $BRIDGING_HEADER $EXTRA_ARGS \
367+
&& \(toolchain.tibs.path) swift-deps-merge $out $DEP_FILES > $out.d
368+
"""
369+
let ccIndexCommand = callCmd + """
370+
\(escapeCommand([toolchain.clang.path])) -fsyntax-only $in $IMPORT_PATHS -index-store-path index \
371+
-index-ignore-system-symbols -fmodules -fmodules-cache-path=ModuleCache \
372+
-MMD -MF $OUTPUT_NAME.d -o $out $EXTRA_ARGS && touch $out
373+
"""
279374
stream.write("""
280375
rule swiftc_index
281376
description = Indexing Swift Module $MODULE_NAME
282-
command = \(toolchain.swiftc.path) $in $IMPORT_PATHS -module-name $MODULE_NAME \
283-
-index-store-path index -index-ignore-system-modules \
284-
-output-file-map $OUTPUT_FILE_MAP \
285-
-emit-module -emit-module-path $MODULE_PATH -emit-dependencies \
286-
-pch-output-dir pch -module-cache-path ModuleCache \
287-
$EMIT_HEADER $BRIDGING_HEADER $EXTRA_ARGS \
288-
&& \(toolchain.tibs.path) swift-deps-merge $out $DEP_FILES > $out.d
377+
command = \(swiftIndexCommand)
289378
depfile = $out.d
290379
deps = gcc
291380
restat = 1 # Swift doesn't rewrite modules that haven't changed
292381
293382
rule cc_index
294383
description = Indexing $in
295-
command = \(toolchain.clang.path) -fsyntax-only $in $IMPORT_PATHS -index-store-path index -index-ignore-system-symbols -fmodules -fmodules-cache-path=ModuleCache -MMD -MF $OUTPUT_NAME.d -o $out $EXTRA_ARGS && touch $out
384+
command = \(ccIndexCommand)
296385
depfile = $out.d
297386
deps = gcc
298387
""")
@@ -324,9 +413,9 @@ extension TibsBuilder {
324413
}
325414

326415
stream.write("""
327-
build \(outputs.joined(separator: " ")) : \
328-
swiftc_index \(module.sources.map { $0.path }.joined(separator: " ")) \
329-
| \(deps.joined(separator: " "))
416+
build \(escapePath(path: outputs.joined(separator: " "))) : \
417+
swiftc_index \(module.sources.map { escapePath(path: $0.path) }.joined(separator: " ")) \
418+
| \(escapePath(path: deps.joined(separator: " ")))
330419
MODULE_NAME = \(module.name)
331420
MODULE_PATH = \(module.emitModulePath)
332421
IMPORT_PATHS = \(module.importPaths.map { "-I \($0)" }.joined(separator: " "))
@@ -341,8 +430,8 @@ extension TibsBuilder {
341430
public func writeNinjaSnippet<Output: TextOutputStream>(for tu: TibsResolvedTarget.ClangTU, to stream: inout Output) {
342431

343432
stream.write("""
344-
build \(tu.outputPath): \
345-
cc_index \(tu.source.path) | \(toolchain.clang.path) \(tu.generatedHeaderDep ?? "")
433+
build \(escapePath(path: tu.outputPath)): \
434+
cc_index \(escapePath(path: tu.source.path)) | \(escapePath(path: toolchain.clang.path)) \(tu.generatedHeaderDep ?? "")
346435
IMPORT_PATHS = \(tu.importPaths.map { "-I \($0)" }.joined(separator: " "))
347436
OUTPUT_NAME = \(tu.outputPath)
348437
EXTRA_ARGS = \(tu.extraArgs.joined(separator: " "))

0 commit comments

Comments
 (0)