@@ -262,23 +262,36 @@ extension TibsBuilder {
262
262
}
263
263
264
264
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
+ """
265
284
stream. write ( """
266
285
rule swiftc_index
267
286
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)
275
288
depfile = $out.d
276
289
deps = gcc
277
290
restat = 1 # Swift doesn't rewrite modules that haven't changed
278
291
279
292
rule cc_index
280
293
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 )
282
295
depfile = $out.d
283
296
deps = gcc
284
297
""" )
@@ -310,9 +323,9 @@ extension TibsBuilder {
310
323
}
311
324
312
325
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: " " ) ) )
316
329
MODULE_NAME = \( module. name)
317
330
MODULE_PATH = \( module. emitModulePath)
318
331
IMPORT_PATHS = \( module. importPaths. map { " -I \( $0) " } . joined ( separator: " " ) )
@@ -328,8 +341,8 @@ extension TibsBuilder {
328
341
public func writeNinjaSnippet< Output: TextOutputStream > ( for tu: TibsResolvedTarget . ClangTU , to stream: inout Output ) {
329
342
330
343
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 ?? " " )
333
346
IMPORT_PATHS = \( tu. importPaths. map { " -I \( $0) " } . joined ( separator: " " ) )
334
347
OUTPUT_NAME = \( tu. outputPath)
335
348
EXTRA_ARGS = \( tu. extraArgs. joined ( separator: " " ) )
@@ -356,3 +369,80 @@ func xcrunSDKPath() -> String {
356
369
}
357
370
return path
358
371
}
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