Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Sources/SWBCore/Settings/BuiltinMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,7 @@ public final class BuiltinMacros {
public static let _WRAPPER_PARENT_PATH = BuiltinMacros.declareStringMacro("_WRAPPER_PARENT_PATH")
public static let _WRAPPER_RESOURCES_DIR = BuiltinMacros.declareStringMacro("_WRAPPER_RESOURCES_DIR")
public static let __INPUT_FILE_LIST_PATH__ = BuiltinMacros.declarePathMacro("__INPUT_FILE_LIST_PATH__")
public static let LINKER_FILE_LIST_FORMAT = BuiltinMacros.declareEnumMacro("LINKER_FILE_LIST_FORMAT") as EnumMacroDeclaration<LinkerFileListFormat>
public static let SWIFT_RESPONSE_FILE_PATH = BuiltinMacros.declarePathMacro("SWIFT_RESPONSE_FILE_PATH")
public static let __ARCHS__ = BuiltinMacros.declareStringListMacro("__ARCHS__")

Expand Down Expand Up @@ -2425,6 +2426,7 @@ public final class BuiltinMacros {
_WRAPPER_PARENT_PATH,
_WRAPPER_RESOURCES_DIR,
__INPUT_FILE_LIST_PATH__,
LINKER_FILE_LIST_FORMAT,
__ARCHS__,
__SWIFT_MODULE_ONLY_ARCHS__,
arch,
Expand Down Expand Up @@ -2870,6 +2872,14 @@ public enum StripStyle: String, Equatable, Hashable, EnumerationMacroType {
case debugging
}

public enum LinkerFileListFormat: String, Equatable, Hashable, EnumerationMacroType {
public static let defaultValue = Self.unescapedNewlineSeparated

case unescapedNewlineSeparated
case unixShellQuotedNewlineSeparated
case windowsShellQuotedNewlineSeparated
}

public enum MergedBinaryType: String, Equatable, Hashable, EnumerationMacroType {
public static let defaultValue = Self.none

Expand Down
17 changes: 17 additions & 0 deletions Sources/SWBCore/SpecImplementations/LinkerSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,23 @@ open class LinkerSpec : CommandLineToolSpec, @unchecked Sendable {
return ruleInfo
}

public func inputFileListContents(_ cbc: CommandBuildContext) -> ByteString {
let contents = OutputByteStream()
for input in cbc.inputs {
switch cbc.scope.evaluate(BuiltinMacros.LINKER_FILE_LIST_FORMAT) {
case .unescapedNewlineSeparated:
contents <<< input.absolutePath.strWithPosixSlashes <<< "\n"
case .unixShellQuotedNewlineSeparated:
let escaper = UNIXShellCommandCodec(encodingStrategy: .singleQuotes, encodingBehavior: .argumentsOnly)
contents <<< escaper.encode([input.absolutePath.strWithPosixSlashes]) <<< "\n"
case .windowsShellQuotedNewlineSeparated:
let escaper = WindowsProcessArgumentsCodec()
contents <<< escaper.encode([input.absolutePath.strWithPosixSlashes]) <<< "\n"
}
}
return contents.bytes
}

open override func constructTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async {
// FIXME: We should ensure this cannot happen.
fatalError("unexpected direct invocation")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ private final class EnumBuildOptionType : BuildOptionType {
return try namespace.declareEnumMacro(name) as EnumMacroDeclaration<LinkerDriverChoice>
case "SWIFT_API_DIGESTER_MODE":
return try namespace.declareEnumMacro(name) as EnumMacroDeclaration<SwiftAPIDigesterMode>
case "LINKER_FILE_LIST_FORMAT":
return try namespace.declareEnumMacro(name) as EnumMacroDeclaration<LinkerFileListFormat>
default:
return try namespace.declareStringMacro(name)
}
Expand Down
10 changes: 2 additions & 8 deletions Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift
Original file line number Diff line number Diff line change
Expand Up @@ -382,13 +382,8 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
// Define the linker file list.
let fileListPath = cbc.scope.evaluate(BuiltinMacros.__INPUT_FILE_LIST_PATH__, lookup: linkerDriverLookup)
if !fileListPath.isEmpty {
let contents = OutputByteStream()
for input in cbc.inputs {
// ld64 reads lines from the file using fgets, without doing any other processing.
contents <<< input.absolutePath.strWithPosixSlashes <<< "\n"
}
let fileListPath = fileListPath
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])
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])
inputPaths.append(fileListPath)
}

Expand Down Expand Up @@ -1600,8 +1595,7 @@ public final class LibtoolLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @u
// Define the linker file list.
let fileListPath = cbc.scope.evaluate(BuiltinMacros.__INPUT_FILE_LIST_PATH__)
if !fileListPath.isEmpty {
let contents = cbc.inputs.map({ return $0.absolutePath.strWithPosixSlashes + "\n" }).joined(separator: "")
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])
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])
inputPaths.append(fileListPath)
}
} else {
Expand Down
10 changes: 10 additions & 0 deletions Sources/SWBGenericUnixPlatform/Specs/UnixLd.xcspec
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,16 @@
Name = "SWIFTC__INPUT_FILE_LIST_PATH__";
Type = Path;
Condition = "NO";
},
{
Name = "LINKER_FILE_LIST_FORMAT";
Type = Enumeration;
Values = (
unescapedNewlineSeparated,
unixShellQuotedNewlineSeparated,
windowsShellQuotedNewlineSeparated,
);
DefaultValue = unixShellQuotedNewlineSeparated;
}
);
},
Expand Down
10 changes: 10 additions & 0 deletions Sources/SWBGenericUnixPlatform/Specs/UnixLibtool.xcspec
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@
Type = Boolean;
DefaultValue = YES;
},
{
Name = "LINKER_FILE_LIST_FORMAT";
Type = Enumeration;
Values = (
unescapedNewlineSeparated,
unixShellQuotedNewlineSeparated,
windowsShellQuotedNewlineSeparated,
);
DefaultValue = unixShellQuotedNewlineSeparated;
}
);
},
)
36 changes: 36 additions & 0 deletions Sources/SWBUtil/ArgumentSplitting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,39 @@ public final class LLVMStyleCommandCodec: CommandSequenceEncodable, CommandSeque
private static let space = Character(" ")
private static let dollarSign = Character("$")
}

/// Suitable for escaping Windows process arguments, but NOT command lines which will be interpreted by a shell
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent call out :)

public final class WindowsProcessArgumentsCodec: CommandSequenceEncodable, Sendable {
public init() {}

// Adapted from swift-testing's process spawning code.
public func encode(_ sequence: [String]) -> String {
return sequence.lazy
.map { arg in
if !arg.contains(where: {" \t\n\"".contains($0)}) {
return arg
}

var quoted = "\""
var unquoted = arg.unicodeScalars
while !unquoted.isEmpty {
guard let firstNonBackslash = unquoted.firstIndex(where: { $0 != "\\" }) else {
let backslashCount = unquoted.count
quoted.append(String(repeating: "\\", count: backslashCount * 2))
break
}
let backslashCount = unquoted.distance(from: unquoted.startIndex, to: firstNonBackslash)
if (unquoted[firstNonBackslash] == "\"") {
quoted.append(String(repeating: "\\", count: backslashCount * 2 + 1))
quoted.append(String(unquoted[firstNonBackslash]))
} else {
quoted.append(String(repeating: "\\", count: backslashCount))
quoted.append(String(unquoted[firstNonBackslash]))
}
unquoted.removeFirst(backslashCount + 1)
}
quoted.append("\"")
return quoted
}.joined(separator: " ")
}
}
10 changes: 10 additions & 0 deletions Sources/SWBWindowsPlatform/Specs/WindowsLd.xcspec
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,16 @@
Type = Path;
Condition = "NO";
},
{
Name = "LINKER_FILE_LIST_FORMAT";
Type = Enumeration;
Values = (
unescapedNewlineSeparated,
unixShellQuotedNewlineSeparated,
windowsShellQuotedNewlineSeparated,
);
DefaultValue = windowsShellQuotedNewlineSeparated;
},
{
Name = "ALTERNATE_LINKER";
Type = String;
Expand Down
10 changes: 10 additions & 0 deletions Sources/SWBWindowsPlatform/Specs/WindowsLibtool.xcspec
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@
);
IsInputDependency = Yes;
},
{
Name = "LINKER_FILE_LIST_FORMAT";
Type = Enumeration;
Values = (
unescapedNewlineSeparated,
unixShellQuotedNewlineSeparated,
windowsShellQuotedNewlineSeparated,
);
DefaultValue = windowsShellQuotedNewlineSeparated;
},
);
},
)
118 changes: 118 additions & 0 deletions Tests/SWBBuildSystemTests/BuildOperationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,124 @@ fileprivate struct BuildOperationTests: CoreBasedTests {
}
}

@Test(.requireSDKs(.host))
func commandLineTool_whitespaceEscaping() async throws {
try await withTemporaryDirectory { (tmpDir: Path) in
let tmpDir = tmpDir.join("has whitespace")
let testProject = try await TestProject(
"TestProject",
sourceRoot: tmpDir,
groupTree: TestGroup(
"SomeFiles",
children: [
TestFile("main.swift"),
TestFile("dynamic.swift"),
TestFile("static.swift"),
]),
buildConfigurations: [
TestBuildConfiguration("Debug", buildSettings: [
"ARCHS": "$(ARCHS_STANDARD)",
"CODE_SIGNING_ALLOWED": ProcessInfo.processInfo.hostOperatingSystem() == .macOS ? "YES" : "NO",
"CODE_SIGN_IDENTITY": "-",
"CODE_SIGN_ENTITLEMENTS": "Entitlements.plist",
"DEFINES_MODULE": "YES",
"PRODUCT_NAME": "$(TARGET_NAME)",
"SDKROOT": "$(HOST_PLATFORM)",
"SUPPORTED_PLATFORMS": "$(HOST_PLATFORM)",
"SWIFT_VERSION": swiftVersion,
"GCC_GENERATE_DEBUGGING_SYMBOLS": "YES",
])
],
targets: [
TestStandardTarget(
"tool",
type: .commandLineTool,
buildConfigurations: [
TestBuildConfiguration("Debug", buildSettings: [
"LD_RUNPATH_SEARCH_PATHS": "@loader_path/",
])
],
buildPhases: [
TestSourcesBuildPhase(["main.swift"]),
TestFrameworksBuildPhase([
TestBuildFile(.target("dynamiclib")),
TestBuildFile(.target("staticlib")),
])
],
dependencies: [
"dynamiclib",
"staticlib",
]
),
TestStandardTarget(
"dynamiclib",
type: .dynamicLibrary,
buildConfigurations: [
TestBuildConfiguration("Debug", buildSettings: [
"DYLIB_INSTALL_NAME_BASE": "$ORIGIN",
"DYLIB_INSTALL_NAME_BASE[sdk=macosx*]": "@rpath",

// FIXME: Find a way to make these default
"EXECUTABLE_PREFIX": "lib",
"EXECUTABLE_PREFIX[sdk=windows*]": "",
])
],
buildPhases: [
TestSourcesBuildPhase(["dynamic.swift"]),
]
),
TestStandardTarget(
"staticlib",
type: .staticLibrary,
buildConfigurations: [
TestBuildConfiguration("Debug", buildSettings: [
// FIXME: Find a way to make these default
"EXECUTABLE_PREFIX": "lib",
"EXECUTABLE_PREFIX[sdk=windows*]": "",
])
],
buildPhases: [
TestSourcesBuildPhase(["static.swift"]),
]
),
])
let core = try await getCore()
let tester = try await BuildOperationTester(core, testProject, simulated: false)

let projectDir = tester.workspace.projects[0].sourceRoot

try await tester.fs.writeFileContents(projectDir.join("main.swift")) { stream in
stream <<< "import dynamiclib\n"
stream <<< "import staticlib\n"
stream <<< "dynamicLib()\n"
stream <<< "dynamicLib()\n"
stream <<< "staticLib()\n"
stream <<< "print(\"Hello world\")\n"
}

try await tester.fs.writeFileContents(projectDir.join("dynamic.swift")) { stream in
stream <<< "public func dynamicLib() { }"
}

try await tester.fs.writeFileContents(projectDir.join("static.swift")) { stream in
stream <<< "public func staticLib() { }"
}

try await tester.fs.writePlist(projectDir.join("Entitlements.plist"), .plDict([:]))

let provisioningInputs = [
"dynamiclib": ProvisioningTaskInputs(identityHash: "-", signedEntitlements: .plDict([:]), simulatedEntitlements: .plDict([:])),
"staticlib": ProvisioningTaskInputs(identityHash: "-", signedEntitlements: .plDict([:]), simulatedEntitlements: .plDict([:])),
"tool": ProvisioningTaskInputs(identityHash: "-", signedEntitlements: .plDict([:]), simulatedEntitlements: .plDict([:]))
]

let destination: RunDestinationInfo = .host
try await tester.checkBuild(runDestination: destination, persistent: true, signableTargets: Set(provisioningInputs.keys), signableTargetInputs: provisioningInputs) { results in
results.checkNoErrors()
}
}
}

@Test(.requireSDKs(.macOS))
func unitTestWithGeneratedEntryPointViaMacOSOverride() async throws {
try await withTemporaryDirectory(removeTreeOnDeinit: false) { (tmpDir: Path) in
Expand Down
9 changes: 9 additions & 0 deletions Tests/SWBUtilTests/ArgumentSplittingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,13 @@ import Testing
#expect(UNIXShellCommandCodec(encodingStrategy: .singleQuotes, joinSequence: " ", encodingBehavior: .fullCommandLine)?.encode(["This", "is", "typical"]) == "This is typical")
#expect(UNIXShellCommandCodec(encodingStrategy: .singleQuotes, joinSequence: " ", encodingBehavior: .fullCommandLine)?.encode(["This", "is", "apparently", "more", "readable"]) == "This is apparently more readable")
}

@Test
func windowsProcessArgsEncoding() {
#expect(WindowsProcessArgumentsCodec().encode([#"normalarg"#]) == #"normalarg"#)
#expect(WindowsProcessArgumentsCodec().encode([#"argument with spaces"#]) == #""argument with spaces""#)
#expect(WindowsProcessArgumentsCodec().encode([#"\windows\path\ending\in\backslash\"#]) == #"\windows\path\ending\in\backslash\"#)
#expect(WindowsProcessArgumentsCodec().encode([#"\windows\path\with space\ending\in\backslash\"#]) == #""\windows\path\with space\ending\in\backslash\\""#)
#expect(WindowsProcessArgumentsCodec().encode([#"has"quote"#]) == #""has\"quote""#)
}
}