diff --git a/Sources/SWBCore/PlannedTaskAction.swift b/Sources/SWBCore/PlannedTaskAction.swift index 4ffc6484..ab6e2419 100644 --- a/Sources/SWBCore/PlannedTaskAction.swift +++ b/Sources/SWBCore/PlannedTaskAction.swift @@ -349,7 +349,7 @@ public protocol TaskActionCreationDelegate func createProcessSDKImportsTaskAction() -> any PlannedTaskAction func createValidateDependenciesTaskAction() -> any PlannedTaskAction func createObjectLibraryAssemblerTaskAction() -> any PlannedTaskAction - func createLinkerTaskAction(expandResponseFiles: Bool) -> any PlannedTaskAction + func createLinkerTaskAction(expandResponseFiles: Bool, responseFileFormat: ResponseFileFormat) -> any PlannedTaskAction } extension TaskActionCreationDelegate { diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index 7570b70b..50cc3505 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -1188,7 +1188,8 @@ 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 + public static let LINKER_FILE_LIST_FORMAT = BuiltinMacros.declareEnumMacro("LINKER_FILE_LIST_FORMAT") as EnumMacroDeclaration + public static let LINKER_RESPONSE_FILE_FORMAT = BuiltinMacros.declareEnumMacro("LINKER_RESPONSE_FILE_FORMAT") as EnumMacroDeclaration public static let SWIFT_RESPONSE_FILE_PATH = BuiltinMacros.declarePathMacro("SWIFT_RESPONSE_FILE_PATH") public static let __ARCHS__ = BuiltinMacros.declareStringListMacro("__ARCHS__") @@ -2441,6 +2442,7 @@ public final class BuiltinMacros { _WRAPPER_RESOURCES_DIR, __INPUT_FILE_LIST_PATH__, LINKER_FILE_LIST_FORMAT, + LINKER_RESPONSE_FILE_FORMAT, __ARCHS__, __SWIFT_MODULE_ONLY_ARCHS__, arch, @@ -2898,12 +2900,8 @@ 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 +extension ResponseFileFormat: EnumerationMacroType { + public static let defaultValue = ResponseFileFormat.unixShellQuotedNewlineSeparated } public enum MergedBinaryType: String, Equatable, Hashable, EnumerationMacroType { diff --git a/Sources/SWBCore/SpecImplementations/LinkerSpec.swift b/Sources/SWBCore/SpecImplementations/LinkerSpec.swift index 6160a31e..5d728619 100644 --- a/Sources/SWBCore/SpecImplementations/LinkerSpec.swift +++ b/Sources/SWBCore/SpecImplementations/LinkerSpec.swift @@ -139,20 +139,7 @@ open class LinkerSpec : CommandLineToolSpec, @unchecked Sendable { } 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 + return ByteString(encodingAsUTF8: ResponseFiles.responseFileContents(args: cbc.inputs.map { $0.absolutePath.strWithPosixSlashes }, format: cbc.scope.evaluate(BuiltinMacros.LINKER_FILE_LIST_FORMAT))) } open override func constructTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async { diff --git a/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift b/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift index bfbff393..b8fa1092 100644 --- a/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift +++ b/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift @@ -117,7 +117,9 @@ private final class EnumBuildOptionType : BuildOptionType { case "SWIFT_API_DIGESTER_MODE": return try namespace.declareEnumMacro(name) as EnumMacroDeclaration case "LINKER_FILE_LIST_FORMAT": - return try namespace.declareEnumMacro(name) as EnumMacroDeclaration + return try namespace.declareEnumMacro(name) as EnumMacroDeclaration + case "LINKER_RESPONSE_FILE_FORMAT": + return try namespace.declareEnumMacro(name) as EnumMacroDeclaration case "DOCC_MINIMUM_ACCESS_LEVEL": return try namespace.declareEnumMacro(name) as EnumMacroDeclaration default: diff --git a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift index f5856959..537358d5 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift @@ -235,7 +235,7 @@ public struct ClangSourceFileIndexingInfo: SourceFileIndexingInfo { // Skip } else if arg.starts(with: ByteString(unicodeScalarLiteral: "@")), let attachmentPath = responseFileMapping[Path(arg.asString.dropFirst())], - let responseFileArgs = try? ResponseFiles.expandResponseFiles(["@\(attachmentPath.str)"], fileSystem: localFS, relativeTo: workingDir) { + let responseFileArgs = try? ResponseFiles.expandResponseFiles(["@\(attachmentPath.str)"], fileSystem: localFS, relativeTo: workingDir, format: .unixShellQuotedSpaceSeparated) { result.append(contentsOf: responseFileArgs.map { ByteString(encodingAsUTF8: $0) }) } else { result.append(arg) @@ -719,7 +719,7 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible ctx.add(string: self.identifier) let responseFilePath = scope.evaluate(BuiltinMacros.PER_ARCH_OBJECT_FILE_DIR).join("\(ctx.signature.asString)-common-args.resp") - let attachmentPath = producer.writeFileSpec.constructFileTasks(CommandBuildContext(producer: producer, scope: scope, inputs: [], output: responseFilePath), delegate, contents: ByteString(encodingAsUTF8: ResponseFiles.responseFileContents(args: responseFileCommandLine)), permissions: nil, logContents: true, preparesForIndexing: true, additionalTaskOrderingOptions: [.immediate, .ignorePhaseOrdering]) + let attachmentPath = producer.writeFileSpec.constructFileTasks(CommandBuildContext(producer: producer, scope: scope, inputs: [], output: responseFilePath), delegate, contents: ByteString(encodingAsUTF8: ResponseFiles.responseFileContents(args: responseFileCommandLine, format: .unixShellQuotedSpaceSeparated)), permissions: nil, logContents: true, preparesForIndexing: true, additionalTaskOrderingOptions: [.immediate, .ignorePhaseOrdering]) return ConstantFlags(flags: regularCommandLine + ["@\(responseFilePath.str)"], headerSearchPaths: headerSearchPaths, inputs: [responseFilePath], responseFileMapping: [responseFilePath: attachmentPath]) } else { diff --git a/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift b/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift index 002478c9..bd2acc90 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift @@ -1428,7 +1428,8 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec override public func createTaskAction(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) -> (any PlannedTaskAction)? { let useResponseFile = cbc.scope.evaluate(BuiltinMacros.CLANG_USE_RESPONSE_FILE) - return delegate.taskActionCreationDelegate.createLinkerTaskAction(expandResponseFiles: !useResponseFile) + let responseFileFormat = cbc.scope.evaluate(BuiltinMacros.LINKER_RESPONSE_FILE_FORMAT) + return delegate.taskActionCreationDelegate.createLinkerTaskAction(expandResponseFiles: !useResponseFile, responseFileFormat: responseFileFormat) } override public func discoveredCommandLineToolSpecInfo(_ producer: any CommandProducer, _ scope: MacroEvaluationScope, _ delegate: any CoreClientTargetDiagnosticProducingDelegate) async -> (any DiscoveredCommandLineToolSpecInfo)? { @@ -1645,7 +1646,8 @@ public final class LibtoolLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @u override public func createTaskAction(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) -> (any PlannedTaskAction)? { let useResponseFile = cbc.scope.evaluate(BuiltinMacros.LIBTOOL_USE_RESPONSE_FILE) - return delegate.taskActionCreationDelegate.createLinkerTaskAction(expandResponseFiles: !useResponseFile) + let responseFileFormat = cbc.scope.evaluate(BuiltinMacros.LINKER_RESPONSE_FILE_FORMAT) + return delegate.taskActionCreationDelegate.createLinkerTaskAction(expandResponseFiles: !useResponseFile, responseFileFormat: responseFileFormat) } override public func constructLinkerTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, libraries: [LibrarySpecifier], usedTools: [CommandLineToolSpec: Set]) async { diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift index 14d1030c..07215597 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift @@ -373,8 +373,9 @@ public struct SwiftDriverPayload: Serializable, TaskPayload, Encodable { public let linkerResponseFilePath: Path? public let dependencyFilteringRootPath: Path? public let verifyScannerDependencies: Bool + public let linkerResponseFileFormat: ResponseFileFormat - internal init(uniqueID: String, compilerLocation: LibSwiftDriver.CompilerLocation, moduleName: String, outputPrefix: String, tempDirPath: Path, explicitModulesTempDirPath: Path, variant: String, architecture: String, eagerCompilationEnabled: Bool, explicitModulesEnabled: Bool, commandLine: [String], ruleInfo: [String], isUsingWholeModuleOptimization: Bool, casOptions: CASOptions?, reportRequiredTargetDependencies: BooleanWarningLevel, linkerResponseFilePath: Path?, dependencyFilteringRootPath: Path?, verifyScannerDependencies: Bool) { + internal init(uniqueID: String, compilerLocation: LibSwiftDriver.CompilerLocation, moduleName: String, outputPrefix: String, tempDirPath: Path, explicitModulesTempDirPath: Path, variant: String, architecture: String, eagerCompilationEnabled: Bool, explicitModulesEnabled: Bool, commandLine: [String], ruleInfo: [String], isUsingWholeModuleOptimization: Bool, casOptions: CASOptions?, reportRequiredTargetDependencies: BooleanWarningLevel, linkerResponseFilePath: Path?, linkerResponseFileFormat: ResponseFileFormat, dependencyFilteringRootPath: Path?, verifyScannerDependencies: Bool) { self.uniqueID = uniqueID self.compilerLocation = compilerLocation self.moduleName = moduleName @@ -391,12 +392,13 @@ public struct SwiftDriverPayload: Serializable, TaskPayload, Encodable { self.casOptions = casOptions self.reportRequiredTargetDependencies = reportRequiredTargetDependencies self.linkerResponseFilePath = linkerResponseFilePath + self.linkerResponseFileFormat = linkerResponseFileFormat self.dependencyFilteringRootPath = dependencyFilteringRootPath self.verifyScannerDependencies = verifyScannerDependencies } public init(from deserializer: any Deserializer) throws { - try deserializer.beginAggregate(18) + try deserializer.beginAggregate(19) self.uniqueID = try deserializer.deserialize() self.compilerLocation = try deserializer.deserialize() self.moduleName = try deserializer.deserialize() @@ -413,12 +415,13 @@ public struct SwiftDriverPayload: Serializable, TaskPayload, Encodable { self.casOptions = try deserializer.deserialize() self.reportRequiredTargetDependencies = try deserializer.deserialize() self.linkerResponseFilePath = try deserializer.deserialize() + self.linkerResponseFileFormat = try deserializer.deserialize() self.dependencyFilteringRootPath = try deserializer.deserialize() self.verifyScannerDependencies = try deserializer.deserialize() } public func serialize(to serializer: T) where T : Serializer { - serializer.serializeAggregate(18) { + serializer.serializeAggregate(19) { serializer.serialize(self.uniqueID) serializer.serialize(self.compilerLocation) serializer.serialize(self.moduleName) @@ -435,6 +438,7 @@ public struct SwiftDriverPayload: Serializable, TaskPayload, Encodable { serializer.serialize(self.casOptions) serializer.serialize(self.reportRequiredTargetDependencies) serializer.serialize(self.linkerResponseFilePath) + serializer.serialize(self.linkerResponseFileFormat) serializer.serialize(self.dependencyFilteringRootPath) serializer.serialize(self.verifyScannerDependencies) } @@ -2560,7 +2564,7 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi let explicitModuleBuildEnabled = await swiftExplicitModuleBuildEnabled(cbc.producer, cbc.scope, delegate) let verifyScannerDependencies = explicitModuleBuildEnabled && cbc.scope.evaluate(BuiltinMacros.SWIFT_DEPENDENCY_REGISTRATION_MODE) == .verifySwiftDependencyScanner - return SwiftDriverPayload(uniqueID: uniqueID, compilerLocation: compilerLocation, moduleName: scope.evaluate(BuiltinMacros.SWIFT_MODULE_NAME), outputPrefix: scope.evaluate(BuiltinMacros.TARGET_NAME) + compilationMode.moduleBaseNameSuffix, tempDirPath: tempDirPath, explicitModulesTempDirPath: explicitModulesTempDirPath, variant: variant, architecture: arch, eagerCompilationEnabled: eagerCompilationEnabled(args: args, scope: scope, compilationMode: compilationMode, isUsingWholeModuleOptimization: isUsingWholeModuleOptimization), explicitModulesEnabled: explicitModuleBuildEnabled, commandLine: commandLine, ruleInfo: ruleInfo, isUsingWholeModuleOptimization: isUsingWholeModuleOptimization, casOptions: casOptions, reportRequiredTargetDependencies: scope.evaluate(BuiltinMacros.DIAGNOSE_MISSING_TARGET_DEPENDENCIES), linkerResponseFilePath: linkerResponseFilePath, dependencyFilteringRootPath: cbc.producer.sdk?.path, verifyScannerDependencies: verifyScannerDependencies) + return SwiftDriverPayload(uniqueID: uniqueID, compilerLocation: compilerLocation, moduleName: scope.evaluate(BuiltinMacros.SWIFT_MODULE_NAME), outputPrefix: scope.evaluate(BuiltinMacros.TARGET_NAME) + compilationMode.moduleBaseNameSuffix, tempDirPath: tempDirPath, explicitModulesTempDirPath: explicitModulesTempDirPath, variant: variant, architecture: arch, eagerCompilationEnabled: eagerCompilationEnabled(args: args, scope: scope, compilationMode: compilationMode, isUsingWholeModuleOptimization: isUsingWholeModuleOptimization), explicitModulesEnabled: explicitModuleBuildEnabled, commandLine: commandLine, ruleInfo: ruleInfo, isUsingWholeModuleOptimization: isUsingWholeModuleOptimization, casOptions: casOptions, reportRequiredTargetDependencies: scope.evaluate(BuiltinMacros.DIAGNOSE_MISSING_TARGET_DEPENDENCIES), linkerResponseFilePath: linkerResponseFilePath, linkerResponseFileFormat: cbc.scope.evaluate(BuiltinMacros.LINKER_RESPONSE_FILE_FORMAT), dependencyFilteringRootPath: cbc.producer.sdk?.path, verifyScannerDependencies: verifyScannerDependencies) } func constructSwiftResponseFileTask(path: Path) { diff --git a/Sources/SWBGenericUnixPlatform/Specs/UnixLd.xcspec b/Sources/SWBGenericUnixPlatform/Specs/UnixLd.xcspec index 5efcd8f5..b408bd89 100644 --- a/Sources/SWBGenericUnixPlatform/Specs/UnixLd.xcspec +++ b/Sources/SWBGenericUnixPlatform/Specs/UnixLd.xcspec @@ -186,10 +186,22 @@ Type = Enumeration; Values = ( unescapedNewlineSeparated, + unixShellQuotedSpaceSeparated, unixShellQuotedNewlineSeparated, windowsShellQuotedNewlineSeparated, ); DefaultValue = unixShellQuotedNewlineSeparated; + }, + { + Name = "LINKER_RESPONSE_FILE_FORMAT"; + Type = Enumeration; + Values = ( + unescapedNewlineSeparated, + unixShellQuotedSpaceSeparated, + unixShellQuotedNewlineSeparated, + windowsShellQuotedNewlineSeparated, + ); + DefaultValue = unixShellQuotedSpaceSeparated; } ); }, diff --git a/Sources/SWBGenericUnixPlatform/Specs/UnixLibtool.xcspec b/Sources/SWBGenericUnixPlatform/Specs/UnixLibtool.xcspec index b46d97f0..7d08dcb2 100644 --- a/Sources/SWBGenericUnixPlatform/Specs/UnixLibtool.xcspec +++ b/Sources/SWBGenericUnixPlatform/Specs/UnixLibtool.xcspec @@ -77,6 +77,17 @@ windowsShellQuotedNewlineSeparated, ); DefaultValue = unixShellQuotedNewlineSeparated; + }, + { + Name = "LINKER_RESPONSE_FILE_FORMAT"; + Type = Enumeration; + Values = ( + unescapedNewlineSeparated, + unixShellQuotedSpaceSeparated, + unixShellQuotedNewlineSeparated, + windowsShellQuotedNewlineSeparated, + ); + DefaultValue = unixShellQuotedSpaceSeparated; } ); }, diff --git a/Sources/SWBTaskExecution/BuildDescriptionManager.swift b/Sources/SWBTaskExecution/BuildDescriptionManager.swift index ecdfbbbd..6bd51a61 100644 --- a/Sources/SWBTaskExecution/BuildDescriptionManager.swift +++ b/Sources/SWBTaskExecution/BuildDescriptionManager.swift @@ -904,8 +904,8 @@ extension BuildSystemTaskPlanningDelegate: TaskActionCreationDelegate { return ObjectLibraryAssemblerTaskAction() } - func createLinkerTaskAction(expandResponseFiles: Bool) -> any PlannedTaskAction { - return LinkerTaskAction(expandResponseFiles: expandResponseFiles) + func createLinkerTaskAction(expandResponseFiles: Bool, responseFileFormat: ResponseFileFormat) -> any PlannedTaskAction { + return LinkerTaskAction(expandResponseFiles: expandResponseFiles, responseFileFormat: responseFileFormat) } } diff --git a/Sources/SWBTaskExecution/CMakeLists.txt b/Sources/SWBTaskExecution/CMakeLists.txt index 6ae2d684..b8c7f116 100644 --- a/Sources/SWBTaskExecution/CMakeLists.txt +++ b/Sources/SWBTaskExecution/CMakeLists.txt @@ -28,6 +28,7 @@ add_library(SWBTaskExecution DynamicTaskSpecs/SwiftDriverJobDynamicTaskSpec.swift DynamicTaskSpecs/SwiftDriverPlanningDynamicTaskSpec.swift ProjectPlanner.swift + ResponseFileFormat+ExpressibleByArgument.swift Task.swift TaskActionExtensionPoint.swift TaskActions/AuxiliaryFileTaskAction.swift diff --git a/Sources/SWBTaskExecution/ResponseFileFormat+ExpressibleByArgument.swift b/Sources/SWBTaskExecution/ResponseFileFormat+ExpressibleByArgument.swift new file mode 100644 index 00000000..4b9abae0 --- /dev/null +++ b/Sources/SWBTaskExecution/ResponseFileFormat+ExpressibleByArgument.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +public import ArgumentParser +import SWBUtil + +extension ResponseFileFormat: ExpressibleByArgument {} diff --git a/Sources/SWBTaskExecution/TaskActions/ClangScanTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ClangScanTaskAction.swift index d9d7e053..0712b6ff 100644 --- a/Sources/SWBTaskExecution/TaskActions/ClangScanTaskAction.swift +++ b/Sources/SWBTaskExecution/TaskActions/ClangScanTaskAction.swift @@ -74,7 +74,7 @@ public final class ClangScanTaskAction: TaskAction, BuildValueValidatingTaskActi self.scanningOutput = parsedOutput if expandResponseFiles { do { - self.commandLine = try ResponseFiles.expandResponseFiles(cliArguments, fileSystem: executionDelegate.fs, relativeTo: workingDirectory) + self.commandLine = try ResponseFiles.expandResponseFiles(cliArguments, fileSystem: executionDelegate.fs, relativeTo: workingDirectory, format: .unixShellQuotedSpaceSeparated) } catch { outputDelegate.error(error.localizedDescription) return nil diff --git a/Sources/SWBTaskExecution/TaskActions/LinkerTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/LinkerTaskAction.swift index fedbdede..be739b13 100644 --- a/Sources/SWBTaskExecution/TaskActions/LinkerTaskAction.swift +++ b/Sources/SWBTaskExecution/TaskActions/LinkerTaskAction.swift @@ -22,22 +22,26 @@ public final class LinkerTaskAction: TaskAction { /// Whether response files should be expanded before invoking the linker. private let expandResponseFiles: Bool - - public init(expandResponseFiles: Bool) { + private let responseFileFormat: ResponseFileFormat + + public init(expandResponseFiles: Bool, responseFileFormat: ResponseFileFormat) { self.expandResponseFiles = expandResponseFiles + self.responseFileFormat = responseFileFormat super.init() } public override func serialize(to serializer: T) { - serializer.beginAggregate(2) + serializer.beginAggregate(3) serializer.serialize(expandResponseFiles) + serializer.serialize(responseFileFormat) super.serialize(to: serializer) serializer.endAggregate() } public required init(from deserializer: any Deserializer) throws { - try deserializer.beginAggregate(2) + try deserializer.beginAggregate(3) self.expandResponseFiles = try deserializer.deserialize() + self.responseFileFormat = try deserializer.deserialize() try super.init(from: deserializer) } @@ -55,7 +59,8 @@ public final class LinkerTaskAction: TaskAction { commandLine = try ResponseFiles.expandResponseFiles( commandLine, fileSystem: executionDelegate.fs, - relativeTo: task.workingDirectory + relativeTo: task.workingDirectory, + format: responseFileFormat ) } catch { outputDelegate.emitError("Failed to expand response files: \(error.localizedDescription)") diff --git a/Sources/SWBTaskExecution/TaskActions/ObjectLibraryAssemblerTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ObjectLibraryAssemblerTaskAction.swift index 82176362..31b5487f 100644 --- a/Sources/SWBTaskExecution/TaskActions/ObjectLibraryAssemblerTaskAction.swift +++ b/Sources/SWBTaskExecution/TaskActions/ObjectLibraryAssemblerTaskAction.swift @@ -22,6 +22,7 @@ public final class ObjectLibraryAssemblerTaskAction: TaskAction { private struct Options: ParsableArguments { @Argument var inputs: [Path] @Option var output: Path + @Option var linkerResponseFileFormat: ResponseFileFormat } override public func performTaskAction( @@ -39,7 +40,7 @@ public final class ObjectLibraryAssemblerTaskAction: TaskAction { try executionDelegate.fs.copy(input, to: options.output.join(input.basename)) } let args = options.inputs.map { $0.strWithPosixSlashes } - try executionDelegate.fs.write(options.output.join("args.resp"), contents: ByteString(encodingAsUTF8: ResponseFiles.responseFileContents(args: args))) + try executionDelegate.fs.write(options.output.join("args.resp"), contents: ByteString(encodingAsUTF8: ResponseFiles.responseFileContents(args: args, format: options.linkerResponseFileFormat))) return .succeeded } catch { outputDelegate.emitError("\(error)") diff --git a/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift index 85f7a182..8b4e72ed 100644 --- a/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift +++ b/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift @@ -141,7 +141,7 @@ final public class SwiftDriverTaskAction: TaskAction, BuildValueValidatingTaskAc responseFileCommandLine.append(contentsOf: ["-Xlinker", "-add_ast_path", "-Xlinker", "\(swiftmodulePath)"]) } } - let contents = ByteString(encodingAsUTF8: ResponseFiles.responseFileContents(args: responseFileCommandLine)) + let contents = ByteString(encodingAsUTF8: ResponseFiles.responseFileContents(args: responseFileCommandLine, format: driverPayload.linkerResponseFileFormat)) try executionDelegate.fs.write(linkerResponseFilePath, contents: contents, atomically: true) } diff --git a/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift b/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift index 91f6937a..2779938d 100644 --- a/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift +++ b/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift @@ -252,7 +252,7 @@ extension CapturingTaskGenerationDelegate: TaskActionCreationDelegate { return ObjectLibraryAssemblerTaskAction() } - package func createLinkerTaskAction(expandResponseFiles: Bool) -> any PlannedTaskAction { - return LinkerTaskAction(expandResponseFiles: expandResponseFiles) + package func createLinkerTaskAction(expandResponseFiles: Bool, responseFileFormat: ResponseFileFormat) -> any PlannedTaskAction { + return LinkerTaskAction(expandResponseFiles: expandResponseFiles, responseFileFormat: responseFileFormat) } } diff --git a/Sources/SWBTestSupport/TaskPlanningTestSupport.swift b/Sources/SWBTestSupport/TaskPlanningTestSupport.swift index 9ba7c3d9..2b0f251a 100644 --- a/Sources/SWBTestSupport/TaskPlanningTestSupport.swift +++ b/Sources/SWBTestSupport/TaskPlanningTestSupport.swift @@ -480,8 +480,8 @@ extension TestTaskPlanningDelegate: TaskActionCreationDelegate { return ObjectLibraryAssemblerTaskAction() } - package func createLinkerTaskAction(expandResponseFiles: Bool) -> any PlannedTaskAction { - return LinkerTaskAction(expandResponseFiles: expandResponseFiles) + package func createLinkerTaskAction(expandResponseFiles: Bool, responseFileFormat: ResponseFileFormat) -> any PlannedTaskAction { + return LinkerTaskAction(expandResponseFiles: expandResponseFiles, responseFileFormat: responseFileFormat) } } diff --git a/Sources/SWBUniversalPlatform/Specs/Ld.xcspec b/Sources/SWBUniversalPlatform/Specs/Ld.xcspec index 2f07a375..47ffa803 100644 --- a/Sources/SWBUniversalPlatform/Specs/Ld.xcspec +++ b/Sources/SWBUniversalPlatform/Specs/Ld.xcspec @@ -807,6 +807,28 @@ ); }; }, + { + Name = "LINKER_FILE_LIST_FORMAT"; + Type = Enumeration; + Values = ( + unescapedNewlineSeparated, + unixShellQuotedSpaceSeparated, + unixShellQuotedNewlineSeparated, + windowsShellQuotedNewlineSeparated, + ); + DefaultValue = unescapedNewlineSeparated; + }, + { + Name = "LINKER_RESPONSE_FILE_FORMAT"; + Type = Enumeration; + Values = ( + unescapedNewlineSeparated, + unixShellQuotedSpaceSeparated, + unixShellQuotedNewlineSeparated, + windowsShellQuotedNewlineSeparated, + ); + DefaultValue = unixShellQuotedSpaceSeparated; + } ); } ) diff --git a/Sources/SWBUniversalPlatform/Specs/ObjectLibraryAssembler.xcspec b/Sources/SWBUniversalPlatform/Specs/ObjectLibraryAssembler.xcspec index 0a3d4758..651ca29f 100644 --- a/Sources/SWBUniversalPlatform/Specs/ObjectLibraryAssembler.xcspec +++ b/Sources/SWBUniversalPlatform/Specs/ObjectLibraryAssembler.xcspec @@ -32,7 +32,11 @@ ); CommandOutputParser = "XCGenericCommandOutputParser"; Options = ( - + { Name = OBJECT_FILE_ASSEMBLER_LINKER_RESPONSE_FILE_FORMAT; + Type = String; + DefaultValue = "$(LINKER_RESPONSE_FILE_FORMAT)"; + CommandLineFlag = "--linker-response-file-format"; + }, ); } ) diff --git a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift index 17075967..d5f8ba5f 100644 --- a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift +++ b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift @@ -28,8 +28,7 @@ class TestEntryPointGenerationTaskAction: TaskAction { if options.discoverTests { var objects: [Path] = [] for linkerFilelist in options.linkerFilelist { - let filelistContents = String(String(decoding: try executionDelegate.fs.read(linkerFilelist), as: UTF8.self)) - let entries = filelistContents.split(separator: "\n", omittingEmptySubsequences: true).map { Path($0) }.map { + let entries = try ResponseFiles.expandResponseFiles(["@\(linkerFilelist.str)"], fileSystem: executionDelegate.fs, relativeTo: linkerFilelist.dirname, format: options.linkerFileListFormat).map { Path($0) }.map { for indexUnitBasePath in options.indexUnitBasePath { if let remappedPath = generateIndexOutputPath(from: $0, basePath: indexUnitBasePath) { return remappedPath @@ -129,6 +128,7 @@ class TestEntryPointGenerationTaskAction: TaskAction { @Option() var linkerFilelist: [Path] = [] @Option var indexStore: [Path] = [] @Option var indexUnitBasePath: [Path] = [] + @Option var linkerFileListFormat: ResponseFileFormat = ResponseFileFormat.defaultValue @Flag var enableExperimentalTestOutput: Bool = false @Flag var discoverTests: Bool = false } diff --git a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift index ec9dbfb7..dbe42335 100644 --- a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift +++ b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift @@ -21,6 +21,10 @@ final class TestEntryPointGenerationToolSpec: GenericCommandLineToolSpec, SpecId var args = await super.commandLineFromTemplate(cbc, delegate, optionContext: optionContext, specialArgs: specialArgs, lookup: lookup) if cbc.scope.evaluate(BuiltinMacros.GENERATED_TEST_ENTRY_POINT_INCLUDE_DISCOVERED_TESTS) { args.append("--discover-tests") + + let format = cbc.scope.evaluate(BuiltinMacros.LINKER_FILE_LIST_FORMAT) + args.append(contentsOf: ["--linker-file-list-format", .literal(.init(encodingAsUTF8: format.rawValue))]) + for toolchainLibrarySearchPath in cbc.producer.toolchains.map({ $0.librarySearchPaths }) { if let path = toolchainLibrarySearchPath.findLibrary(operatingSystem: cbc.producer.hostOperatingSystem, basename: "IndexStore") { args.append(contentsOf: ["--index-store-library-path", .path(path)]) diff --git a/Sources/SWBUtil/ResponseFiles.swift b/Sources/SWBUtil/ResponseFiles.swift index 1115fd81..9b4bc741 100644 --- a/Sources/SWBUtil/ResponseFiles.swift +++ b/Sources/SWBUtil/ResponseFiles.swift @@ -10,18 +10,36 @@ // //===----------------------------------------------------------------------===// +public enum ResponseFileFormat: String, Equatable, Hashable, CaseIterable, Sendable, Codable, Serializable { + case unescapedNewlineSeparated + case unixShellQuotedNewlineSeparated + case unixShellQuotedSpaceSeparated + case windowsShellQuotedNewlineSeparated +} + public enum ResponseFiles: Sendable { - public static func responseFileContents(args: [String]) -> String { - UNIXShellCommandCodec(encodingStrategy: .singleQuotes, encodingBehavior: .argumentsOnly).encode(args) + public static func responseFileContents(args: [String], format: ResponseFileFormat) -> String { + switch format { + case .unescapedNewlineSeparated: + return args.map { $0 + "\n" }.joined() + case .unixShellQuotedNewlineSeparated: + let escaper = UNIXShellCommandCodec(encodingStrategy: .singleQuotes, encodingBehavior: .argumentsOnly) + return args.map { escaper.encode([$0]) + "\n" }.joined() + case .unixShellQuotedSpaceSeparated: + return UNIXShellCommandCodec(encodingStrategy: .singleQuotes, encodingBehavior: .argumentsOnly).encode(args) + case .windowsShellQuotedNewlineSeparated: + let escaper = WindowsProcessArgumentsCodec() + return args.map { escaper.encode([$0]) + "\r\n" }.joined() + } } // Adapted from SwiftDriver's response file support. - public static func expandResponseFiles(_ args: [String], fileSystem: any FSProxy, relativeTo basePath: Path) throws -> [String] { + public static func expandResponseFiles(_ args: [String], fileSystem: any FSProxy, relativeTo basePath: Path, format: ResponseFileFormat) throws -> [String] { var visited: Set = [] - return try expandResponseFiles(args, fileSystem: fileSystem, relativeTo: basePath, visitedResponseFiles: &visited) + return try expandResponseFiles(args, fileSystem: fileSystem, relativeTo: basePath, format: format, visitedResponseFiles: &visited) } - private static func expandResponseFiles(_ args: [String], fileSystem: any FSProxy, relativeTo basePath: Path, visitedResponseFiles: inout Set) throws -> [String] { + private static func expandResponseFiles(_ args: [String], fileSystem: any FSProxy, relativeTo basePath: Path, format: ResponseFileFormat, visitedResponseFiles: inout Set) throws -> [String] { var result: [String] = [] for arg in args { if arg.first == "@" { @@ -35,8 +53,8 @@ public enum ResponseFiles: Sendable { } let contents = try fileSystem.read(responseFile).asString - let lines = tokenizeResponseFile(contents) - result.append(contentsOf: try expandResponseFiles(lines, fileSystem: fileSystem, relativeTo: basePath, visitedResponseFiles: &visitedResponseFiles)) + let tokens = tokenizeResponseFile(contents, format: format) + result.append(contentsOf: try expandResponseFiles(tokens, fileSystem: fileSystem, relativeTo: basePath, format: format, visitedResponseFiles: &visitedResponseFiles)) } else { result.append(arg) } @@ -45,17 +63,25 @@ public enum ResponseFiles: Sendable { return result } - private static func tokenizeResponseFile(_ content: String) -> [String] { - return content.split { $0 == "\n" || $0 == "\r\n" } - .flatMap { tokenizeResponseFileLine($0) } + private static func tokenizeResponseFile(_ content: String, format: ResponseFileFormat) -> [String] { + switch format { + case .unescapedNewlineSeparated: + return content.split { $0 == "\n" || $0 == "\r\n" }.map { String($0) } + case .unixShellQuotedNewlineSeparated, .unixShellQuotedSpaceSeparated: + return content.split { $0 == "\n" || $0 == "\r\n" } + .flatMap { tokenizeUnixShellQuotedResponseFileLine($0) } + case .windowsShellQuotedNewlineSeparated: + return content.split { $0 == "\n" || $0 == "\r\n" } + .map { tokenizeWindowsShellQuotedResponseFileArg($0) } + } } - private enum TokenState { + private enum UnixTokenState { case normal, escaping, quoted } /// Tokenizes a response file line generated by `UNIXShellCommandCodec` using the `.singleQuotes` strategy. - private static func tokenizeResponseFileLine(_ line: S) -> [String] { + private static func tokenizeUnixShellQuotedResponseFileLine(_ line: S) -> [String] { // Support double dash comments only if they start at the beginning of a line. if line.hasPrefix("//") { return [] } @@ -64,7 +90,7 @@ public enum ResponseFiles: Sendable { // Conservatively assume ~1 token per line. token.reserveCapacity(line.count) - var state: TokenState = .normal + var state: UnixTokenState = .normal for char in line { if char == #"\"# && state == .normal { @@ -109,4 +135,30 @@ public enum ResponseFiles: Sendable { return tokens } + + private enum WindowsTokenState { + case normal, escaping + } + + private static func tokenizeWindowsShellQuotedResponseFileArg(_ arg: S) -> String { + guard arg.first == "\"" && arg.last == "\"" else { + return String(arg) + } + var result = "" + var state = WindowsTokenState.normal + for char in arg.dropFirst().dropLast() { + switch state { + case .normal: + if char == "\\" { + state = .escaping + } else { + result.append(char) + } + case .escaping: + result.append(char) + state = .normal + } + } + return result + } } diff --git a/Sources/SWBWindowsPlatform/Specs/WindowsLd.xcspec b/Sources/SWBWindowsPlatform/Specs/WindowsLd.xcspec index 4c1fbcaf..b4ef6fbc 100644 --- a/Sources/SWBWindowsPlatform/Specs/WindowsLd.xcspec +++ b/Sources/SWBWindowsPlatform/Specs/WindowsLd.xcspec @@ -201,6 +201,18 @@ Type = Enumeration; Values = ( unescapedNewlineSeparated, + unixShellQuotedSpaceSeparated, + unixShellQuotedNewlineSeparated, + windowsShellQuotedNewlineSeparated, + ); + DefaultValue = windowsShellQuotedNewlineSeparated; + }, + { + Name = "LINKER_RESPONSE_FILE_FORMAT"; + Type = Enumeration; + Values = ( + unescapedNewlineSeparated, + unixShellQuotedSpaceSeparated, unixShellQuotedNewlineSeparated, windowsShellQuotedNewlineSeparated, ); diff --git a/Sources/SWBWindowsPlatform/Specs/WindowsLibtool.xcspec b/Sources/SWBWindowsPlatform/Specs/WindowsLibtool.xcspec index 7885caf7..c5521e0d 100644 --- a/Sources/SWBWindowsPlatform/Specs/WindowsLibtool.xcspec +++ b/Sources/SWBWindowsPlatform/Specs/WindowsLibtool.xcspec @@ -68,11 +68,23 @@ Type = Enumeration; Values = ( unescapedNewlineSeparated, + unixShellQuotedSpaceSeparated, unixShellQuotedNewlineSeparated, windowsShellQuotedNewlineSeparated, ); DefaultValue = windowsShellQuotedNewlineSeparated; }, + { + Name = "LINKER_RESPONSE_FILE_FORMAT"; + Type = Enumeration; + Values = ( + unescapedNewlineSeparated, + unixShellQuotedSpaceSeparated, + unixShellQuotedNewlineSeparated, + windowsShellQuotedNewlineSeparated, + ); + DefaultValue = windowsShellQuotedNewlineSeparated; + } ); }, ) diff --git a/Tests/SWBBuildSystemTests/BuildOperationTests.swift b/Tests/SWBBuildSystemTests/BuildOperationTests.swift index aa178223..150e503a 100644 --- a/Tests/SWBBuildSystemTests/BuildOperationTests.swift +++ b/Tests/SWBBuildSystemTests/BuildOperationTests.swift @@ -330,40 +330,16 @@ fileprivate struct BuildOperationTests: CoreBasedTests { try await tester.checkBuild(runDestination: destination, persistent: true, signableTargets: Set(provisioningInputs.keys), signableTargetInputs: provisioningInputs) { results in results.checkNoErrors() if core.hostOperatingSystem.imageFormat.requiresSwiftModulewrap { - try results.checkTask(.matchTargetName("tool"), .matchRulePattern(["WriteAuxiliaryFile", .suffix("LinkFileList")])) { task in - let auxFileAction = try #require(task.action as? AuxiliaryFileTaskAction) - let contents = try tester.fs.read(auxFileAction.context.input).asString - let files = contents.components(separatedBy: "\n").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty } - #expect(files.count == 2) - #expect(files[0].hasSuffix("tool.o")) - #expect(files[1].hasSuffix("main.o")) - } let toolWrap = try #require(results.getTask(.matchTargetName("tool"), .matchRuleType("SwiftModuleWrap"))) try results.checkTask(.matchTargetName("tool"), .matchRuleType("Ld")) { task in try results.checkTaskFollows(task, toolWrap) } - try results.checkTask(.matchTargetName("dynamiclib"), .matchRulePattern(["WriteAuxiliaryFile", .suffix("LinkFileList")])) { task in - let auxFileAction = try #require(task.action as? AuxiliaryFileTaskAction) - let contents = try tester.fs.read(auxFileAction.context.input).asString - let files = contents.components(separatedBy: "\n").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty } - #expect(files.count == 2) - #expect(files[0].hasSuffix("dynamiclib.o")) - #expect(files[1].hasSuffix("dynamic.o")) - } let dylibWrap = try #require(results.getTask(.matchTargetName("dynamiclib"), .matchRuleType("SwiftModuleWrap"))) try results.checkTask(.matchTargetName("dynamiclib"), .matchRuleType("Ld")) { task in try results.checkTaskFollows(task, dylibWrap) } - try results.checkTask(.matchTargetName("staticlib"), .matchRulePattern(["WriteAuxiliaryFile", .suffix("LinkFileList")])) { task in - let auxFileAction = try #require(task.action as? AuxiliaryFileTaskAction) - let contents = try tester.fs.read(auxFileAction.context.input).asString - let files = contents.components(separatedBy: "\n").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty } - #expect(files.count == 2) - #expect(files[0].hasSuffix("staticlib.o")) - #expect(files[1].hasSuffix("static.o")) - } let staticWrap = try #require(results.getTask(.matchTargetName("staticlib"), .matchRuleType("SwiftModuleWrap"))) try results.checkTask(.matchTargetName("staticlib"), .matchRuleType("Libtool")) { task in try results.checkTaskFollows(task, staticWrap) diff --git a/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift b/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift index a9e2004d..a1753962 100644 --- a/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift +++ b/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift @@ -246,8 +246,8 @@ extension CapturingTaskGenerationDelegate: TaskActionCreationDelegate { return ObjectLibraryAssemblerTaskAction() } - func createLinkerTaskAction(expandResponseFiles: Bool) -> any PlannedTaskAction { - return LinkerTaskAction(expandResponseFiles: expandResponseFiles) + func createLinkerTaskAction(expandResponseFiles: Bool, responseFileFormat: ResponseFileFormat) -> any PlannedTaskAction { + return LinkerTaskAction(expandResponseFiles: expandResponseFiles, responseFileFormat: responseFileFormat) } } diff --git a/Tests/SWBTaskConstructionTests/ObjectLibraryTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/ObjectLibraryTaskConstructionTests.swift index e6d0d958..4eff840a 100644 --- a/Tests/SWBTaskConstructionTests/ObjectLibraryTaskConstructionTests.swift +++ b/Tests/SWBTaskConstructionTests/ObjectLibraryTaskConstructionTests.swift @@ -56,6 +56,8 @@ fileprivate struct ObjectLibraryTaskConstructionTests: CoreBasedTests { results.checkTask(.matchRuleType("AssembleObjectLibrary")) { task in task.checkCommandLineMatches([ "builtin-ObjectLibraryAssembler", + "--linker-response-file-format", + .any, .suffix("a.o"), .suffix("b.o"), "--output", diff --git a/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift index cd27640e..20cf4808 100644 --- a/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift +++ b/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift @@ -366,7 +366,7 @@ fileprivate struct UnitTestTaskConstructionTests: CoreBasedTests { await tester.checkBuild(runDestination: .linux, fs: fs) { results in results.checkTarget("UnitTestRunner") { target in results.checkTask(.matchTarget(target), .matchRuleType("GenerateTestEntryPoint")) { task in - task.checkCommandLineMatches([.suffix("builtin-generateTestEntryPoint"), "--output", .suffix("test_entry_point.swift"), "--discover-tests", "--index-store-library-path", .suffix("libIndexStore.so"), "--linker-filelist", .suffix("UnitTestTarget.LinkFileList"), "--index-store", "/index", "--index-unit-base-path", "/tmp/Test/aProject/build"]) + task.checkCommandLineMatches([.suffix("builtin-generateTestEntryPoint"), "--output", .suffix("test_entry_point.swift"), "--discover-tests", "--linker-file-list-format", .any, "--index-store-library-path", .suffix("libIndexStore.so"), "--linker-filelist", .suffix("UnitTestTarget.LinkFileList"), "--index-store", "/index", "--index-unit-base-path", "/tmp/Test/aProject/build"]) task.checkInputs([ .pathPattern(.suffix("UnitTestTarget.LinkFileList")), .pathPattern(.suffix("UnitTestTarget.so")), diff --git a/Tests/SWBUtilTests/ResponseFileTests.swift b/Tests/SWBUtilTests/ResponseFileTests.swift index 64b14064..9dfe569b 100644 --- a/Tests/SWBUtilTests/ResponseFileTests.swift +++ b/Tests/SWBUtilTests/ResponseFileTests.swift @@ -16,18 +16,40 @@ import SWBTestSupport import SWBUtil @Suite fileprivate struct ResponseFileTests { + @Test(arguments: ResponseFileFormat.allCases) + func responseFileRoundTripping(format: ResponseFileFormat) throws { + try assertRoundTrip(args: ["foo", "bar"], format: format) + try assertRoundTrip(args: ["foo bar"], format: format) + try assertRoundTrip(args: ["foo bar", "baz"], format: format) + try assertRoundTrip(args: ["/this/is/a/path", "-bar"], format: format) + + try assertRoundTrip(args: [#"'"#], format: format) + try assertRoundTrip(args: [#"""#], format: format) + try assertRoundTrip(args: [#" a b '"#], format: format) + try assertRoundTrip(args: [#"' a b "#], format: format) + try assertRoundTrip(args: [#"start foo "bar baz's slash\" end"#], format: format) + } + @Test - func responseFileContent() throws { - try assertRoundTrip(args: ["foo", "bar"], "foo bar") - try assertRoundTrip(args: ["foo bar"], "'foo bar'") - try assertRoundTrip(args: ["foo bar", "baz"], "'foo bar' baz") - try assertRoundTrip(args: ["/this/is/a/path", "-bar"], "/this/is/a/path -bar") - - try assertRoundTrip(args: [#"'"#], #"\'"#) - try assertRoundTrip(args: [#"""#], #"'"'"#) - try assertRoundTrip(args: [#" a b '"#], #"' a b '\'"#) - try assertRoundTrip(args: [#"' a b "#], #"\'' a b '"#) - try assertRoundTrip(args: [#"start foo "bar baz's slash\" end"#], #"'start foo "bar baz'\''s slash\" end'"#) + func responseFileContentUnix() throws { + try assertContents(args: ["foo", "bar"], "foo bar", format: .unixShellQuotedSpaceSeparated) + try assertContents(args: ["foo bar"], "'foo bar'", format: .unixShellQuotedSpaceSeparated) + try assertContents(args: ["foo bar", "baz"], "'foo bar' baz", format: .unixShellQuotedSpaceSeparated) + try assertContents(args: ["/this/is/a/path", "-bar"], "/this/is/a/path -bar", format: .unixShellQuotedSpaceSeparated) + + try assertContents(args: [#"'"#], #"\'"#, format: .unixShellQuotedSpaceSeparated) + try assertContents(args: [#"""#], #"'"'"#, format: .unixShellQuotedSpaceSeparated) + try assertContents(args: [#" a b '"#], #"' a b '\'"#, format: .unixShellQuotedSpaceSeparated) + try assertContents(args: [#"' a b "#], #"\'' a b '"#, format: .unixShellQuotedSpaceSeparated) + try assertContents(args: [#"start foo "bar baz's slash\" end"#], #"'start foo "bar baz'\''s slash\" end'"#, format: .unixShellQuotedSpaceSeparated) + } + + @Test + func responseFileContentWindows() throws { + try assertContents(args: ["foo", "bar"], "foo\r\nbar\r\n", format: .windowsShellQuotedNewlineSeparated) + try assertContents(args: ["foo bar"], "\"foo bar\"\r\n", format: .windowsShellQuotedNewlineSeparated) + try assertContents(args: ["foo bar", "baz"], "\"foo bar\"\r\nbaz\r\n", format: .windowsShellQuotedNewlineSeparated) + try assertContents(args: ["/this/is/a/path", "-bar"], "/this/is/a/path\r\n-bar\r\n", format: .windowsShellQuotedNewlineSeparated) } @Test @@ -42,21 +64,25 @@ import SWBUtil // comment two -path '/path/with space' @b.resp """) - try #expect(ResponseFiles.expandResponseFiles(["-first", "@a.resp", "-last"], fileSystem: localFS, relativeTo: tmpDir) == ["-first", "-foo", "-bar", "-baz", "-last"]) - try #expect(ResponseFiles.expandResponseFiles(["-first", "@b.resp", "-last"], fileSystem: localFS, relativeTo: tmpDir) == ["-first", "-b", "-foo", "-bar", "-baz", "-last"]) + try #expect(ResponseFiles.expandResponseFiles(["-first", "@a.resp", "-last"], fileSystem: localFS, relativeTo: tmpDir, format: .unixShellQuotedSpaceSeparated) == ["-first", "-foo", "-bar", "-baz", "-last"]) + try #expect(ResponseFiles.expandResponseFiles(["-first", "@b.resp", "-last"], fileSystem: localFS, relativeTo: tmpDir, format: .unixShellQuotedSpaceSeparated) == ["-first", "-b", "-foo", "-bar", "-baz", "-last"]) #expect(throws: (any Error).self) { - try ResponseFiles.expandResponseFiles(["-first", "@c.resp", "-last"], fileSystem: localFS, relativeTo: tmpDir) + try ResponseFiles.expandResponseFiles(["-first", "@c.resp", "-last"], fileSystem: localFS, relativeTo: tmpDir, format: .unixShellQuotedSpaceSeparated) } - try #expect(ResponseFiles.expandResponseFiles(["-first", "@d.resp", "-last"], fileSystem: localFS, relativeTo: tmpDir) == ["-first", "-foo", "-bar", "-baz", "-path", "/path/with space", "-b", "-foo", "-bar", "-baz", "-last"]) + try #expect(ResponseFiles.expandResponseFiles(["-first", "@d.resp", "-last"], fileSystem: localFS, relativeTo: tmpDir, format: .unixShellQuotedSpaceSeparated) == ["-first", "-foo", "-bar", "-baz", "-path", "/path/with space", "-b", "-foo", "-bar", "-baz", "-last"]) } } - private func assertRoundTrip(args: [String], _ contents: String, sourceLocation: SourceLocation = #_sourceLocation) throws { - let actualContents = ResponseFiles.responseFileContents(args: args) - #expect(actualContents == contents, sourceLocation: sourceLocation) + private func assertRoundTrip(args: [String], format: ResponseFileFormat, sourceLocation: SourceLocation = #_sourceLocation) throws { + let actualContents = ResponseFiles.responseFileContents(args: args, format: format) let fs = PseudoFS() try fs.write(Path.root.join("a.resp"), contents: ByteString(encodingAsUTF8: actualContents)) - try #expect(ResponseFiles.expandResponseFiles(["@a.resp"], fileSystem: fs, relativeTo: .root) == args, sourceLocation: sourceLocation) + try #expect(ResponseFiles.expandResponseFiles(["@a.resp"], fileSystem: fs, relativeTo: .root, format: format) == args, sourceLocation: sourceLocation) + } + + private func assertContents(args: [String], _ contents: String, format: ResponseFileFormat, sourceLocation: SourceLocation = #_sourceLocation) throws { + let actualContents = ResponseFiles.responseFileContents(args: args, format: format) + #expect(actualContents == contents, sourceLocation: sourceLocation) } }