Skip to content

Commit 1e3e71d

Browse files
authored
Merge pull request #50 from gmittert/WindowsPathsMakeMeSad
Fix Ninja Output for Windows
2 parents 97ec3b5 + 0a02e5d commit 1e3e71d

File tree

1 file changed

+103
-13
lines changed

1 file changed

+103
-13
lines changed

Sources/ISDBTibs/TibsBuilder.swift

Lines changed: 103 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -262,23 +262,36 @@ extension TibsBuilder {
262262
}
263263

264264
public func writeNinjaRules<Output: TextOutputStream>(to stream: inout Output) {
265+
#if os(Windows)
266+
let callCmd = "cmd.exe /C "
267+
#else
268+
let callCmd = ""
269+
#endif
270+
let swiftIndexCommand = callCmd + """
271+
\(escapeCommand([toolchain.swiftc.path])) $in $IMPORT_PATHS -module-name $MODULE_NAME \
272+
-index-store-path index -index-ignore-system-modules \
273+
-output-file-map $OUTPUT_FILE_MAP \
274+
-emit-module -emit-module-path $MODULE_PATH -emit-dependencies \
275+
-pch-output-dir pch -module-cache-path ModuleCache \
276+
$EMIT_HEADER $BRIDGING_HEADER $SDK $EXTRA_ARGS \
277+
&& \(toolchain.tibs.path) swift-deps-merge $out $DEP_FILES > $out.d
278+
"""
279+
let ccIndexCommand = callCmd + """
280+
\(escapeCommand([toolchain.clang.path])) -fsyntax-only $in $IMPORT_PATHS -index-store-path index \
281+
-index-ignore-system-symbols -fmodules -fmodules-cache-path=ModuleCache \
282+
-MMD -MF $OUTPUT_NAME.d -o $out $EXTRA_ARGS && touch $out
283+
"""
265284
stream.write("""
266285
rule swiftc_index
267286
description = Indexing Swift Module $MODULE_NAME
268-
command = \(toolchain.swiftc.path) $in $IMPORT_PATHS -module-name $MODULE_NAME \
269-
-index-store-path index -index-ignore-system-modules \
270-
-output-file-map $OUTPUT_FILE_MAP \
271-
-emit-module -emit-module-path $MODULE_PATH -emit-dependencies \
272-
-pch-output-dir pch -module-cache-path ModuleCache \
273-
$EMIT_HEADER $BRIDGING_HEADER $SDK $EXTRA_ARGS \
274-
&& \(toolchain.tibs.path) swift-deps-merge $out $DEP_FILES > $out.d
287+
command = \(swiftIndexCommand)
275288
depfile = $out.d
276289
deps = gcc
277290
restat = 1 # Swift doesn't rewrite modules that haven't changed
278291
279292
rule cc_index
280293
description = Indexing $in
281-
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
294+
command = \(ccIndexCommand)
282295
depfile = $out.d
283296
deps = gcc
284297
""")
@@ -310,9 +323,9 @@ extension TibsBuilder {
310323
}
311324

312325
stream.write("""
313-
build \(outputs.joined(separator: " ")) : \
314-
swiftc_index \(module.sources.map { $0.path }.joined(separator: " ")) \
315-
| \(deps.joined(separator: " "))
326+
build \(escapePath(path: outputs.joined(separator: " "))) : \
327+
swiftc_index \(module.sources.map { escapePath(path: $0.path) }.joined(separator: " ")) \
328+
| \(escapePath(path: deps.joined(separator: " ")))
316329
MODULE_NAME = \(module.name)
317330
MODULE_PATH = \(module.emitModulePath)
318331
IMPORT_PATHS = \(module.importPaths.map { "-I \($0)" }.joined(separator: " "))
@@ -328,8 +341,8 @@ extension TibsBuilder {
328341
public func writeNinjaSnippet<Output: TextOutputStream>(for tu: TibsResolvedTarget.ClangTU, to stream: inout Output) {
329342

330343
stream.write("""
331-
build \(tu.outputPath): \
332-
cc_index \(tu.source.path) | \(toolchain.clang.path) \(tu.generatedHeaderDep ?? "")
344+
build \(escapePath(path: tu.outputPath)): \
345+
cc_index \(escapePath(path: tu.source.path)) | \(escapePath(path: toolchain.clang.path)) \(tu.generatedHeaderDep ?? "")
333346
IMPORT_PATHS = \(tu.importPaths.map { "-I \($0)" }.joined(separator: " "))
334347
OUTPUT_NAME = \(tu.outputPath)
335348
EXTRA_ARGS = \(tu.extraArgs.joined(separator: " "))
@@ -356,3 +369,80 @@ func xcrunSDKPath() -> String {
356369
}
357370
return path
358371
}
372+
373+
#if os(Windows)
374+
func quoteWindowsCommandLine(_ commandLine: [String]) -> String {
375+
func quoteWindowsCommandArg(arg: String) -> String {
376+
// Windows escaping, adapted from Daniel Colascione's "Everyone quotes
377+
// command line arguments the wrong way" - Microsoft Developer Blog
378+
if !arg.contains(where: {" \t\n\"".contains($0)}) {
379+
return arg
380+
}
381+
382+
// To escape the command line, we surround the argument with quotes. However
383+
// the complication comes due to how the Windows command line parser treats
384+
// backslashes (\) and quotes (")
385+
//
386+
// - \ is normally treated as a literal backslash
387+
// - e.g. foo\bar\baz => foo\bar\baz
388+
// - However, the sequence \" is treated as a literal "
389+
// - e.g. foo\"bar => foo"bar
390+
//
391+
// But then what if we are given a path that ends with a \? Surrounding
392+
// foo\bar\ with " would be "foo\bar\" which would be an unterminated string
393+
394+
// since it ends on a literal quote. To allow this case the parser treats:
395+
//
396+
// - \\" as \ followed by the " metachar
397+
// - \\\" as \ followed by a literal "
398+
// - In general:
399+
// - 2n \ followed by " => n \ followed by the " metachar
400+
// - 2n+1 \ followed by " => n \ followed by a literal "
401+
var quoted = "\""
402+
var unquoted = arg.unicodeScalars
403+
404+
while !unquoted.isEmpty {
405+
guard let firstNonBackslash = unquoted.firstIndex(where: { $0 != "\\" }) else {
406+
// String ends with a backslash e.g. foo\bar\, escape all the backslashes
407+
// then add the metachar " below
408+
let backslashCount = unquoted.count
409+
quoted.append(String(repeating: "\\", count: backslashCount * 2))
410+
break
411+
}
412+
let backslashCount = unquoted.distance(from: unquoted.startIndex, to: firstNonBackslash)
413+
if (unquoted[firstNonBackslash] == "\"") {
414+
// This is a string of \ followed by a " e.g. foo\"bar. Escape the
415+
// backslashes and the quote
416+
quoted.append(String(repeating: "\\", count: backslashCount * 2 + 1))
417+
quoted.append(String(unquoted[firstNonBackslash]))
418+
} else {
419+
// These are just literal backslashes
420+
quoted.append(String(repeating: "\\", count: backslashCount))
421+
quoted.append(String(unquoted[firstNonBackslash]))
422+
}
423+
// Drop the backslashes and the following character
424+
unquoted.removeFirst(backslashCount + 1)
425+
}
426+
quoted.append("\"")
427+
return quoted
428+
}
429+
return commandLine.map(quoteWindowsCommandArg).joined(separator: " ")
430+
}
431+
#endif
432+
433+
func escapeCommand(_ args: [String]) -> String {
434+
let escaped: String
435+
#if os(Windows)
436+
escaped = quoteWindowsCommandLine(args)
437+
#else
438+
escaped = args.joined(separator: " ")
439+
#endif
440+
return escapePath(path: escaped)
441+
}
442+
443+
func escapePath(path: String) -> String {
444+
// Ninja escapes using $, this only matters during build lines
445+
// since those are terminated by a :
446+
return path.replacingOccurrences(of: ":", with: "$:")
447+
}
448+

0 commit comments

Comments
 (0)