Skip to content

Commit 33c3975

Browse files
committed
Properly quote linker file list contents on Linux and Windows
1 parent d9c8077 commit 33c3975

File tree

11 files changed

+234
-8
lines changed

11 files changed

+234
-8
lines changed

Sources/SWBCore/Settings/BuiltinMacros.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,7 @@ public final class BuiltinMacros {
11811181
public static let _WRAPPER_PARENT_PATH = BuiltinMacros.declareStringMacro("_WRAPPER_PARENT_PATH")
11821182
public static let _WRAPPER_RESOURCES_DIR = BuiltinMacros.declareStringMacro("_WRAPPER_RESOURCES_DIR")
11831183
public static let __INPUT_FILE_LIST_PATH__ = BuiltinMacros.declarePathMacro("__INPUT_FILE_LIST_PATH__")
1184+
public static let LINKER_FILE_LIST_FORMAT = BuiltinMacros.declareEnumMacro("LINKER_FILE_LIST_FORMAT") as EnumMacroDeclaration<LinkerFileListFormat>
11841185
public static let SWIFT_RESPONSE_FILE_PATH = BuiltinMacros.declarePathMacro("SWIFT_RESPONSE_FILE_PATH")
11851186
public static let __ARCHS__ = BuiltinMacros.declareStringListMacro("__ARCHS__")
11861187

@@ -2425,6 +2426,7 @@ public final class BuiltinMacros {
24252426
_WRAPPER_PARENT_PATH,
24262427
_WRAPPER_RESOURCES_DIR,
24272428
__INPUT_FILE_LIST_PATH__,
2429+
LINKER_FILE_LIST_FORMAT,
24282430
__ARCHS__,
24292431
__SWIFT_MODULE_ONLY_ARCHS__,
24302432
arch,
@@ -2870,6 +2872,14 @@ public enum StripStyle: String, Equatable, Hashable, EnumerationMacroType {
28702872
case debugging
28712873
}
28722874

2875+
public enum LinkerFileListFormat: String, Equatable, Hashable, EnumerationMacroType {
2876+
public static let defaultValue = Self.unescapedNewlineSeparated
2877+
2878+
case unescapedNewlineSeparated
2879+
case unixShellQuotedNewlineSeparated
2880+
case windowsShellQuotedNewlineSeparated
2881+
}
2882+
28732883
public enum MergedBinaryType: String, Equatable, Hashable, EnumerationMacroType {
28742884
public static let defaultValue = Self.none
28752885

Sources/SWBCore/SpecImplementations/LinkerSpec.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,23 @@ open class LinkerSpec : CommandLineToolSpec, @unchecked Sendable {
138138
return ruleInfo
139139
}
140140

141+
public func inputFileListContents(_ cbc: CommandBuildContext) -> ByteString {
142+
let contents = OutputByteStream()
143+
for input in cbc.inputs {
144+
switch cbc.scope.evaluate(BuiltinMacros.LINKER_FILE_LIST_FORMAT) {
145+
case .unescapedNewlineSeparated:
146+
contents <<< input.absolutePath.strWithPosixSlashes <<< "\n"
147+
case .unixShellQuotedNewlineSeparated:
148+
let escaper = UNIXShellCommandCodec(encodingStrategy: .singleQuotes, encodingBehavior: .argumentsOnly)
149+
contents <<< escaper.encode([input.absolutePath.strWithPosixSlashes]) <<< "\n"
150+
case .windowsShellQuotedNewlineSeparated:
151+
let escaper = WindowsProcessArgumentsCodec()
152+
contents <<< escaper.encode([input.absolutePath.strWithPosixSlashes]) <<< "\n"
153+
}
154+
}
155+
return contents.bytes
156+
}
157+
141158
open override func constructTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async {
142159
// FIXME: We should ensure this cannot happen.
143160
fatalError("unexpected direct invocation")

Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ private final class EnumBuildOptionType : BuildOptionType {
116116
return try namespace.declareEnumMacro(name) as EnumMacroDeclaration<LinkerDriverChoice>
117117
case "SWIFT_API_DIGESTER_MODE":
118118
return try namespace.declareEnumMacro(name) as EnumMacroDeclaration<SwiftAPIDigesterMode>
119+
case "LINKER_FILE_LIST_FORMAT":
120+
return try namespace.declareEnumMacro(name) as EnumMacroDeclaration<LinkerFileListFormat>
119121
default:
120122
return try namespace.declareStringMacro(name)
121123
}

Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -382,13 +382,8 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
382382
// Define the linker file list.
383383
let fileListPath = cbc.scope.evaluate(BuiltinMacros.__INPUT_FILE_LIST_PATH__, lookup: linkerDriverLookup)
384384
if !fileListPath.isEmpty {
385-
let contents = OutputByteStream()
386-
for input in cbc.inputs {
387-
// ld64 reads lines from the file using fgets, without doing any other processing.
388-
contents <<< input.absolutePath.strWithPosixSlashes <<< "\n"
389-
}
390385
let fileListPath = fileListPath
391-
cbc.producer.writeFileSpec.constructFileTasks(CommandBuildContext(producer: cbc.producer, scope: cbc.scope, inputs: [], output: fileListPath), delegate, contents: contents.bytes, permissions: nil, preparesForIndexing: false, additionalTaskOrderingOptions: [.immediate, .ignorePhaseOrdering])
386+
cbc.producer.writeFileSpec.constructFileTasks(CommandBuildContext(producer: cbc.producer, scope: cbc.scope, inputs: [], output: fileListPath), delegate, contents: inputFileListContents(cbc), permissions: nil, preparesForIndexing: false, additionalTaskOrderingOptions: [.immediate, .ignorePhaseOrdering])
392387
inputPaths.append(fileListPath)
393388
}
394389

@@ -1600,8 +1595,7 @@ public final class LibtoolLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @u
16001595
// Define the linker file list.
16011596
let fileListPath = cbc.scope.evaluate(BuiltinMacros.__INPUT_FILE_LIST_PATH__)
16021597
if !fileListPath.isEmpty {
1603-
let contents = cbc.inputs.map({ return $0.absolutePath.strWithPosixSlashes + "\n" }).joined(separator: "")
1604-
cbc.producer.writeFileSpec.constructFileTasks(CommandBuildContext(producer: cbc.producer, scope: cbc.scope, inputs: [], output: fileListPath), delegate, contents: ByteString(encodingAsUTF8: contents), permissions: nil, preparesForIndexing: false, additionalTaskOrderingOptions: [.immediate, .ignorePhaseOrdering])
1598+
cbc.producer.writeFileSpec.constructFileTasks(CommandBuildContext(producer: cbc.producer, scope: cbc.scope, inputs: [], output: fileListPath), delegate, contents: inputFileListContents(cbc), permissions: nil, preparesForIndexing: false, additionalTaskOrderingOptions: [.immediate, .ignorePhaseOrdering])
16051599
inputPaths.append(fileListPath)
16061600
}
16071601
} else {

Sources/SWBGenericUnixPlatform/Specs/UnixLd.xcspec

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,16 @@
180180
Name = "SWIFTC__INPUT_FILE_LIST_PATH__";
181181
Type = Path;
182182
Condition = "NO";
183+
},
184+
{
185+
Name = "LINKER_FILE_LIST_FORMAT";
186+
Type = Enumeration;
187+
Values = (
188+
unescapedNewlineSeparated,
189+
unixShellQuotedNewlineSeparated,
190+
windowsShellQuotedNewlineSeparated,
191+
);
192+
DefaultValue = unixShellQuotedNewlineSeparated;
183193
}
184194
);
185195
},

Sources/SWBGenericUnixPlatform/Specs/UnixLibtool.xcspec

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@
6868
Type = Boolean;
6969
DefaultValue = YES;
7070
},
71+
{
72+
Name = "LINKER_FILE_LIST_FORMAT";
73+
Type = Enumeration;
74+
Values = (
75+
unescapedNewlineSeparated,
76+
unixShellQuotedNewlineSeparated,
77+
windowsShellQuotedNewlineSeparated,
78+
);
79+
DefaultValue = unixShellQuotedNewlineSeparated;
80+
}
7181
);
7282
},
7383
)

Sources/SWBUtil/ArgumentSplitting.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,39 @@ public final class LLVMStyleCommandCodec: CommandSequenceEncodable, CommandSeque
280280
private static let space = Character(" ")
281281
private static let dollarSign = Character("$")
282282
}
283+
284+
/// Suitable for escaping Windows process arguments, but NOT command lines which will be interpreted by a shell
285+
public final class WindowsProcessArgumentsCodec: CommandSequenceEncodable, Sendable {
286+
public init() {}
287+
288+
// Adapted from swift-testing's process spawning code.
289+
public func encode(_ sequence: [String]) -> String {
290+
return sequence.lazy
291+
.map { arg in
292+
if !arg.contains(where: {" \t\n\"".contains($0)}) {
293+
return arg
294+
}
295+
296+
var quoted = "\""
297+
var unquoted = arg.unicodeScalars
298+
while !unquoted.isEmpty {
299+
guard let firstNonBackslash = unquoted.firstIndex(where: { $0 != "\\" }) else {
300+
let backslashCount = unquoted.count
301+
quoted.append(String(repeating: "\\", count: backslashCount * 2))
302+
break
303+
}
304+
let backslashCount = unquoted.distance(from: unquoted.startIndex, to: firstNonBackslash)
305+
if (unquoted[firstNonBackslash] == "\"") {
306+
quoted.append(String(repeating: "\\", count: backslashCount * 2 + 1))
307+
quoted.append(String(unquoted[firstNonBackslash]))
308+
} else {
309+
quoted.append(String(repeating: "\\", count: backslashCount))
310+
quoted.append(String(unquoted[firstNonBackslash]))
311+
}
312+
unquoted.removeFirst(backslashCount + 1)
313+
}
314+
quoted.append("\"")
315+
return quoted
316+
}.joined(separator: " ")
317+
}
318+
}

Sources/SWBWindowsPlatform/Specs/WindowsLd.xcspec

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,16 @@
196196
Type = Path;
197197
Condition = "NO";
198198
},
199+
{
200+
Name = "LINKER_FILE_LIST_FORMAT";
201+
Type = Enumeration;
202+
Values = (
203+
unescapedNewlineSeparated,
204+
unixShellQuotedNewlineSeparated,
205+
windowsShellQuotedNewlineSeparated,
206+
);
207+
DefaultValue = windowsShellQuotedNewlineSeparated;
208+
},
199209
{
200210
Name = "ALTERNATE_LINKER";
201211
Type = String;

Sources/SWBWindowsPlatform/Specs/WindowsLibtool.xcspec

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@
6363
);
6464
IsInputDependency = Yes;
6565
},
66+
{
67+
Name = "LINKER_FILE_LIST_FORMAT";
68+
Type = Enumeration;
69+
Values = (
70+
unescapedNewlineSeparated,
71+
unixShellQuotedNewlineSeparated,
72+
windowsShellQuotedNewlineSeparated,
73+
);
74+
DefaultValue = windowsShellQuotedNewlineSeparated;
75+
},
6676
);
6777
},
6878
)

Tests/SWBBuildSystemTests/BuildOperationTests.swift

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,124 @@ fileprivate struct BuildOperationTests: CoreBasedTests {
382382
}
383383
}
384384

385+
@Test(.requireSDKs(.host))
386+
func commandLineTool_whitespaceEscaping() async throws {
387+
try await withTemporaryDirectory { (tmpDir: Path) in
388+
let tmpDir = tmpDir.join("has whitespace")
389+
let testProject = try await TestProject(
390+
"TestProject",
391+
sourceRoot: tmpDir,
392+
groupTree: TestGroup(
393+
"SomeFiles",
394+
children: [
395+
TestFile("main.swift"),
396+
TestFile("dynamic.swift"),
397+
TestFile("static.swift"),
398+
]),
399+
buildConfigurations: [
400+
TestBuildConfiguration("Debug", buildSettings: [
401+
"ARCHS": "$(ARCHS_STANDARD)",
402+
"CODE_SIGNING_ALLOWED": ProcessInfo.processInfo.hostOperatingSystem() == .macOS ? "YES" : "NO",
403+
"CODE_SIGN_IDENTITY": "-",
404+
"CODE_SIGN_ENTITLEMENTS": "Entitlements.plist",
405+
"DEFINES_MODULE": "YES",
406+
"PRODUCT_NAME": "$(TARGET_NAME)",
407+
"SDKROOT": "$(HOST_PLATFORM)",
408+
"SUPPORTED_PLATFORMS": "$(HOST_PLATFORM)",
409+
"SWIFT_VERSION": swiftVersion,
410+
"GCC_GENERATE_DEBUGGING_SYMBOLS": "YES",
411+
])
412+
],
413+
targets: [
414+
TestStandardTarget(
415+
"tool",
416+
type: .commandLineTool,
417+
buildConfigurations: [
418+
TestBuildConfiguration("Debug", buildSettings: [
419+
"LD_RUNPATH_SEARCH_PATHS": "@loader_path/",
420+
])
421+
],
422+
buildPhases: [
423+
TestSourcesBuildPhase(["main.swift"]),
424+
TestFrameworksBuildPhase([
425+
TestBuildFile(.target("dynamiclib")),
426+
TestBuildFile(.target("staticlib")),
427+
])
428+
],
429+
dependencies: [
430+
"dynamiclib",
431+
"staticlib",
432+
]
433+
),
434+
TestStandardTarget(
435+
"dynamiclib",
436+
type: .dynamicLibrary,
437+
buildConfigurations: [
438+
TestBuildConfiguration("Debug", buildSettings: [
439+
"DYLIB_INSTALL_NAME_BASE": "$ORIGIN",
440+
"DYLIB_INSTALL_NAME_BASE[sdk=macosx*]": "@rpath",
441+
442+
// FIXME: Find a way to make these default
443+
"EXECUTABLE_PREFIX": "lib",
444+
"EXECUTABLE_PREFIX[sdk=windows*]": "",
445+
])
446+
],
447+
buildPhases: [
448+
TestSourcesBuildPhase(["dynamic.swift"]),
449+
]
450+
),
451+
TestStandardTarget(
452+
"staticlib",
453+
type: .staticLibrary,
454+
buildConfigurations: [
455+
TestBuildConfiguration("Debug", buildSettings: [
456+
// FIXME: Find a way to make these default
457+
"EXECUTABLE_PREFIX": "lib",
458+
"EXECUTABLE_PREFIX[sdk=windows*]": "",
459+
])
460+
],
461+
buildPhases: [
462+
TestSourcesBuildPhase(["static.swift"]),
463+
]
464+
),
465+
])
466+
let core = try await getCore()
467+
let tester = try await BuildOperationTester(core, testProject, simulated: false)
468+
469+
let projectDir = tester.workspace.projects[0].sourceRoot
470+
471+
try await tester.fs.writeFileContents(projectDir.join("main.swift")) { stream in
472+
stream <<< "import dynamiclib\n"
473+
stream <<< "import staticlib\n"
474+
stream <<< "dynamicLib()\n"
475+
stream <<< "dynamicLib()\n"
476+
stream <<< "staticLib()\n"
477+
stream <<< "print(\"Hello world\")\n"
478+
}
479+
480+
try await tester.fs.writeFileContents(projectDir.join("dynamic.swift")) { stream in
481+
stream <<< "public func dynamicLib() { }"
482+
}
483+
484+
try await tester.fs.writeFileContents(projectDir.join("static.swift")) { stream in
485+
stream <<< "public func staticLib() { }"
486+
}
487+
488+
try await tester.fs.writePlist(projectDir.join("Entitlements.plist"), .plDict([:]))
489+
490+
let provisioningInputs = [
491+
"dynamiclib": ProvisioningTaskInputs(identityHash: "-", signedEntitlements: .plDict([:]), simulatedEntitlements: .plDict([:])),
492+
"staticlib": ProvisioningTaskInputs(identityHash: "-", signedEntitlements: .plDict([:]), simulatedEntitlements: .plDict([:])),
493+
"tool": ProvisioningTaskInputs(identityHash: "-", signedEntitlements: .plDict([:]), simulatedEntitlements: .plDict([:]))
494+
]
495+
496+
let destination: RunDestinationInfo = .host
497+
try await tester.checkBuild(runDestination: destination, persistent: true, signableTargets: Set(provisioningInputs.keys), signableTargetInputs: provisioningInputs) { results in
498+
results.checkNoErrors()
499+
}
500+
}
501+
}
502+
385503
@Test(.requireSDKs(.macOS))
386504
func unitTestWithGeneratedEntryPointViaMacOSOverride() async throws {
387505
try await withTemporaryDirectory(removeTreeOnDeinit: false) { (tmpDir: Path) in

0 commit comments

Comments
 (0)