From 7f8505d472dc31493dae3313ad0df722322da2c1 Mon Sep 17 00:00:00 2001 From: Dave Inglis Date: Mon, 3 Nov 2025 10:34:12 -0500 Subject: [PATCH 1/2] Change how libraries are specified to the linker when using searched libs - remove the platform specifics from computeLibraryArgs (we cannot assume that all libraries have a lib prefix and what there suffix is.) So we now use the FileType prefix and remove any suffix when using searchPathFlagsForLD, moving this into the LinkerSpec.LibrarySpecifier extension, this allows for proper searching of libraries, and linking of dynamic libraries (especially on Windows). --- .../SpecImplementations/LinkerSpec.swift | 9 +- .../SWBCore/SpecImplementations/Specs.swift | 6 +- .../Tools/LinkerTools.swift | 89 ++++++++----------- .../SWBGenericUnixPlatform/Specs/Unix.xcspec | 10 +++ .../SourcesTaskProducer.swift | 40 ++++++--- .../RunDestinationTestSupport.swift | 7 +- .../Specs/StandardFileTypes.xcspec | 4 +- Sources/SWBWindowsPlatform/Plugin.swift | 2 +- .../SWBWindowsPlatform/Specs/Windows.xcspec | 23 +++++ .../BuildOperationTests.swift | 56 +++++++----- .../CustomTaskBuildOperationTests.swift | 2 +- Tests/SWBBuildSystemTests/LinkerTests.swift | 3 +- Tests/SWBCoreTests/CommandLineSpecTests.swift | 9 +- .../UnitTestTaskConstructionTests.swift | 13 +-- 14 files changed, 173 insertions(+), 100 deletions(-) diff --git a/Sources/SWBCore/SpecImplementations/LinkerSpec.swift b/Sources/SWBCore/SpecImplementations/LinkerSpec.swift index 6149d4c4..a8d7df3f 100644 --- a/Sources/SWBCore/SpecImplementations/LinkerSpec.swift +++ b/Sources/SWBCore/SpecImplementations/LinkerSpec.swift @@ -89,11 +89,12 @@ open class LinkerSpec : CommandLineToolSpec, @unchecked Sendable { /// The path to the privacy file, if one exists. public let privacyFile: Path? - public init(kind: Kind, path: Path, mode: Mode, useSearchPaths: Bool, swiftModulePaths: [String: Path], swiftModuleAdditionalLinkerArgResponseFilePaths: [String: Path], explicitDependencies: [Path] = [], topLevelItemPath: Path? = nil, dsymPath: Path? = nil, xcframeworkSourcePath: Path? = nil, privacyFile: Path? = nil) { + public let libPrefix: String? + + public init(kind: Kind, path: Path, mode: Mode, useSearchPaths: Bool, swiftModulePaths: [String: Path], swiftModuleAdditionalLinkerArgResponseFilePaths: [String: Path], prefix: String? = nil, explicitDependencies: [Path] = [], topLevelItemPath: Path? = nil, dsymPath: Path? = nil, xcframeworkSourcePath: Path? = nil, privacyFile: Path? = nil) { self.kind = kind self.path = path self.mode = mode - self.useSearchPaths = useSearchPaths self.swiftModulePaths = swiftModulePaths self.swiftModuleAdditionalLinkerArgResponseFilePaths = swiftModuleAdditionalLinkerArgResponseFilePaths self.explicitDependencies = explicitDependencies @@ -101,6 +102,10 @@ open class LinkerSpec : CommandLineToolSpec, @unchecked Sendable { self.dsymPath = dsymPath self.xcframeworkSourcePath = xcframeworkSourcePath self.privacyFile = privacyFile + self.libPrefix = prefix + // Only use search paths when no prefix is required or when the prefix matches + let hasValidPrefix = libPrefix.map { path.basename.hasPrefix($0) } ?? true + self.useSearchPaths = hasValidPrefix && useSearchPaths } } diff --git a/Sources/SWBCore/SpecImplementations/Specs.swift b/Sources/SWBCore/SpecImplementations/Specs.swift index cb8cf844..8223ce77 100644 --- a/Sources/SWBCore/SpecImplementations/Specs.swift +++ b/Sources/SWBCore/SpecImplementations/Specs.swift @@ -301,6 +301,9 @@ public class FileTypeSpec : Spec, SpecType, @unchecked Sendable { /// Returns `true` if the `isWrapperFolder` value is set in the XCSpec for the file spec. public let isWrapper: Bool + /// Returns any common prefix this file may have (currently used when specifying searched libraries to linker) + public let prefix: String? + required init(_ parser: SpecParser, _ basedOnSpec: Spec?) { let basedOnFileTypeSpec = basedOnSpec as? FileTypeSpec ?? nil @@ -318,8 +321,8 @@ public class FileTypeSpec : Spec, SpecType, @unchecked Sendable { self.isEmbeddableInProduct = parser.parseBool("IsEmbeddable") ?? false self.validateOnCopy = parser.parseBool("ValidateOnCopy") ?? false self.codeSignOnCopy = parser.parseBool("CodeSignOnCopy") ?? false - self.isWrapper = parser.parseBool("IsWrapperFolder") ?? false + self.prefix = parser.parseString("Prefix") // Parse and ignore keys we have no use for. // @@ -358,7 +361,6 @@ public class FileTypeSpec : Spec, SpecType, @unchecked Sendable { parser.parseStringList("MIMETypes") parser.parseString("Permissions") parser.parseString("PlistStructureDefinition") - parser.parseStringList("Prefix") parser.parseBool("RemoveHeadersOnCopy") parser.parseBool("RequiresHardTabs") parser.parseString("UTI") diff --git a/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift b/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift index 414ec486..9cfc2d8c 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift @@ -1289,41 +1289,12 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec private static func computeLibraryArgs(_ libraries: [LibrarySpecifier], scope: MacroEvaluationScope) -> (args: [String], inputs: [Path]) { // Construct the library arguments. return libraries.compactMap { specifier -> (args: [String], inputs: [Path]) in - let basename = specifier.path.basename - - // FIXME: This isn't a good system, we need to redesign how we talk to the linker w.r.t. search paths and our notion of paths. switch specifier.kind { - case .static: - if specifier.useSearchPaths, basename.hasPrefix("lib"), basename.hasSuffix(".a") { - return (specifier.searchPathFlagsForLd(basename.withoutPrefix("lib").withoutSuffix(".a")), []) - } - return (specifier.absolutePathFlagsForLd(), [specifier.path]) - case .dynamic: - let suffix = ".\(scope.evaluate(BuiltinMacros.DYNAMIC_LIBRARY_EXTENSION))" - if specifier.useSearchPaths, basename.hasPrefix("lib"), basename.hasSuffix(suffix) { - return (specifier.searchPathFlagsForLd(basename.withoutPrefix("lib").withoutSuffix(suffix)), []) - } - return (specifier.absolutePathFlagsForLd(), [specifier.path]) - case .textBased: - if specifier.useSearchPaths, basename.hasPrefix("lib"), basename.hasSuffix(".tbd") { - // .merge and .reexport are not supported for text-based libraries. - return (specifier.searchPathFlagsForLd(basename.withoutPrefix("lib").withoutSuffix(".tbd")), []) - } - return (specifier.absolutePathFlagsForLd(), [specifier.path]) - case .framework: - let frameworkName = Path(basename).withoutSuffix + case .static, .dynamic, .textBased, .framework: if specifier.useSearchPaths { - return (specifier.searchPathFlagsForLd(frameworkName), []) + return (specifier.searchPathFlagsForLd(), []) } - let absPathArgs = specifier.absolutePathFlagsForLd() - let returnPath: Path - if let pathArg = absPathArgs.last, Path(pathArg).basename == frameworkName { - returnPath = Path(pathArg) - } - else { - returnPath = specifier.path - } - return (absPathArgs, [returnPath]) + return (specifier.absolutePathFlagsForLd(), [specifier.path]) case .object: // Object files are added to linker inputs in the sources task producer. return ([], []) @@ -1559,35 +1530,47 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec /// Extensions to `LinkerSpec.LibrarySpecifier` specific to the dynamic linker. fileprivate extension LinkerSpec.LibrarySpecifier { - func searchPathFlagsForLd(_ name: String) -> [String] { + + func searchPathFlagsForLd() -> [String] { + precondition(useSearchPaths) + // Extract basename once to avoid redundant operations + let basename = path.basename + let basenameWithoutSuffix = Path(basename).withoutSuffix + // Strip the prefix if one exists and is present in the basename + let strippedName: String + if let prefix = libPrefix, basename.hasPrefix(prefix) { + strippedName = basenameWithoutSuffix.withoutPrefix(prefix) + } else { + strippedName = basenameWithoutSuffix + } switch (kind, mode) { case (.dynamic, .normal): - return ["-l" + name] + return ["-l" + strippedName] case (.dynamic, .reexport): - return ["-Xlinker", "-reexport-l" + name] + return ["-Xlinker", "-reexport-l" + strippedName] case (.dynamic, .merge): - return ["-Xlinker", "-merge-l" + name] + return ["-Xlinker", "-merge-l" + strippedName] case (.dynamic, .reexport_merge): - return ["-Xlinker", "-no_merge-l" + name] + return ["-Xlinker", "-no_merge-l" + strippedName] case (.dynamic, .weak): - return ["-weak-l" + name] + return ["-weak-l" + strippedName] case (.static, .weak), (.textBased, .weak): - return ["-weak-l" + name] + return ["-weak-l" + strippedName] case (.static, _), (.textBased, _): // Other modes are not supported for these kinds. - return ["-l" + name] + return ["-l" + strippedName] case (.framework, .normal): - return ["-framework", name] + return ["-framework", strippedName] case (.framework, .reexport): - return ["-Xlinker", "-reexport_framework", "-Xlinker", name] + return ["-Xlinker", "-reexport_framework", "-Xlinker", strippedName] case (.framework, .merge): - return ["-Xlinker", "-merge_framework", "-Xlinker", name] + return ["-Xlinker", "-merge_framework", "-Xlinker", strippedName] case (.framework, .reexport_merge): - return ["-Xlinker", "-no_merge_framework", "-Xlinker", name] + return ["-Xlinker", "-no_merge_framework", "-Xlinker", strippedName] case (.framework, .weak): - return ["-weak_framework", name] + return ["-weak_framework", strippedName] case (.object, _): // Object files are added to linker inputs in the sources task producer. return [] @@ -1729,15 +1712,17 @@ public final class LibtoolLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @u delegate.warning("Product \(cbc.output.basename) cannot weak-link \(specifier.kind) \(basename)") } - if specifier.useSearchPaths, basename.hasPrefix("lib"), basename.hasSuffix(".a") { + if specifier.useSearchPaths { // Locate using search paths: Add a -l option and *don't* add the path to the library as an input to the task. - return ["-l" + basename.withoutPrefix("lib").withoutSuffix(".a")] - } - else { - // Locate using an absolute path: Add the path as an option and as an input to the task. - inputPaths.append(specifier.path) - return [specifier.path.str] + let basename = specifier.path.basename + let expectedPrefix = specifier.libPrefix ?? "lib" + if basename.hasPrefix(expectedPrefix) { + return ["-l" + Path(basename).withoutSuffix.withoutPrefix(expectedPrefix)] + } } + // Locate using an absolute path: Add the path as an option and as an input to the task. + inputPaths.append(specifier.path) + return [specifier.path.str] case .object: // Object files are added to linker inputs in the sources task producer and so end up in the link-file-list. diff --git a/Sources/SWBGenericUnixPlatform/Specs/Unix.xcspec b/Sources/SWBGenericUnixPlatform/Specs/Unix.xcspec index 446d925b..0922b489 100644 --- a/Sources/SWBGenericUnixPlatform/Specs/Unix.xcspec +++ b/Sources/SWBGenericUnixPlatform/Specs/Unix.xcspec @@ -91,4 +91,14 @@ IconNamePrefix = "TargetPlugin"; DefaultTargetName = "Object File"; }, + { + Domain = generic-unix; + Type = FileType; + Identifier = compiled.mach-o.dylib; + BasedOn = compiled.mach-o; + Prefix = lib; + Extensions = (so); + IsLibrary = YES; + IsDynamicLibrary = YES; + } ) diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift index c8e6296f..a16e4b75 100644 --- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift +++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift @@ -364,6 +364,7 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F } // Link using search paths unless the reference is in a different project, in which case we use a full path (to support legacy build locations, primarily). + // On Windows, always use search paths to ensure import libraries (.lib) are found correctly instead of DLLs. let useSearchPaths: Bool switch buildFile.buildableItem { case .reference: @@ -371,7 +372,12 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F case .targetProduct(guid: let guid): if let referenceTarget = self.context.workspaceContext.workspace.target(for: guid) { let referenceProject = self.context.workspaceContext.workspace.project(for: referenceTarget) - useSearchPaths = referenceProject === self.context.project + // Always use search paths on Windows to ensure .lib files are found instead of .dll files + if context.sdkVariant?.llvmTargetTripleSys == "windows" { + useSearchPaths = true + } else { + useSearchPaths = referenceProject === self.context.project + } } else { useSearchPaths = true } @@ -507,6 +513,7 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F useSearchPaths: useSearchPaths, swiftModulePaths: swiftModulePaths, swiftModuleAdditionalLinkerArgResponseFilePaths: swiftModuleAdditionalLinkerArgResponseFilePaths, + prefix: fileType.prefix, privacyFile: privacyFile ) } else if fileType.conformsTo(context.lookupFileType(identifier: "compiled.mach-o.dylib")!) { @@ -517,6 +524,7 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F useSearchPaths: useSearchPaths, swiftModulePaths: [:], swiftModuleAdditionalLinkerArgResponseFilePaths: [:], + prefix: fileType.prefix, privacyFile: privacyFile ) } else if fileType.conformsTo(context.lookupFileType(identifier: "sourcecode.text-based-dylib-definition")!) { @@ -527,17 +535,18 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F useSearchPaths: useSearchPaths, swiftModulePaths: [:], swiftModuleAdditionalLinkerArgResponseFilePaths: [:], + prefix: fileType.prefix, privacyFile: privacyFile ) } else if fileType.conformsTo(context.lookupFileType(identifier: "wrapper.framework")!) { - func kindFromSettings(_ settings: Settings) -> LinkerSpec.LibrarySpecifier.Kind? { + func kindFromSettings(_ settings: Settings) -> (kind: LinkerSpec.LibrarySpecifier.Kind, prefix: String?)? { switch settings.globalScope.evaluate(BuiltinMacros.MACH_O_TYPE) { case "staticlib": - return .static + return (.static, context.lookupFileType(identifier: "archive.ar")?.prefix) case "mh_dylib": - return .dynamic + return (.dynamic, context.lookupFileType(identifier: "compiled.mach-o.dylib")?.prefix) case "mh_object": - return .object + return (.object, nil) default: return nil } @@ -547,9 +556,11 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F let path: Path let dsymPath: Path? let topLevelItemPath: Path? + let prefix: String? if let settingsForRef, let presumedKind = kindFromSettings(settingsForRef), !useSearchPaths { // If we have a Settings from a cross-project reference, use the _actual_ library path. This prevents downstream code from reconstituting the framework path by joining the framework path with the basename of the framework, which won't be correct for deep frameworks which also need the Versions/A path component. - kind = presumedKind + kind = presumedKind.kind + prefix = presumedKind.prefix path = settingsForRef.globalScope.evaluate(BuiltinMacros.TARGET_BUILD_DIR).join(settingsForRef.globalScope.evaluate(BuiltinMacros.EXECUTABLE_PATH)).normalize() topLevelItemPath = absolutePath if shouldGenerateDSYM(settingsForRef.globalScope) { @@ -563,6 +574,7 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F path = absolutePath topLevelItemPath = nil dsymPath = nil + prefix = nil } return LinkerSpec.LibrarySpecifier( @@ -572,6 +584,7 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F useSearchPaths: useSearchPaths, swiftModulePaths: [:], swiftModuleAdditionalLinkerArgResponseFilePaths: [:], + prefix: prefix, topLevelItemPath: topLevelItemPath, dsymPath: dsymPath, privacyFile: privacyFile @@ -581,7 +594,7 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F kind: .object, path: absolutePath, mode: buildFile.shouldLinkWeakly ? .weak : .normal, - useSearchPaths: useSearchPaths, + useSearchPaths: false, swiftModulePaths: swiftModulePaths, swiftModuleAdditionalLinkerArgResponseFilePaths: swiftModuleAdditionalLinkerArgResponseFilePaths, privacyFile: privacyFile @@ -621,10 +634,16 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F } let libraryKind: LinkerSpec.LibrarySpecifier.Kind + let prefix: String? switch library.libraryType { - case .framework: libraryKind = .framework; break - case .dynamicLibrary: libraryKind = .dynamic; break - case .staticLibrary: libraryKind = .static; break + case .framework: libraryKind = .framework; prefix = nil + case .dynamicLibrary: + libraryKind = .dynamic; + prefix = context.lookupFileType(identifier: "compiled.mach-o.dylib")?.prefix + case .staticLibrary: + libraryKind = .static + prefix = context.lookupFileType(identifier: "archive.ar")?.prefix + break case let .unknown(fileExtension): // An error of type this type should have already been manifested. assertionFailure("unknown xcframework type: \(fileExtension)") @@ -651,6 +670,7 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F useSearchPaths: useSearchPaths, swiftModulePaths: [:], swiftModuleAdditionalLinkerArgResponseFilePaths: [:], + prefix: prefix, explicitDependencies: outputFilePaths, xcframeworkSourcePath: xcframeworkPath, privacyFile: nil diff --git a/Sources/SWBTestSupport/RunDestinationTestSupport.swift b/Sources/SWBTestSupport/RunDestinationTestSupport.swift index fa29f90c..c54777b6 100644 --- a/Sources/SWBTestSupport/RunDestinationTestSupport.swift +++ b/Sources/SWBTestSupport/RunDestinationTestSupport.swift @@ -319,7 +319,7 @@ extension RunDestinationInfo { /// An `Environment` object with `PATH` or `LD_LIBRARY_PATH` set appropriately pointing into the toolchain to be able to run a built Swift binary in tests. /// /// - note: On macOS, the OS provided Swift runtime is used, so `DYLD_LIBRARY_PATH` is never set for Mach-O destinations. - package func hostRuntimeEnvironment(_ core: Core, initialEnvironment: Environment = Environment()) -> Environment { + package func hostRuntimeEnvironment(_ core: Core, initialEnvironment: Environment = Environment()) throws -> Environment { var environment = initialEnvironment guard let toolchain = core.toolchainRegistry.defaultToolchain else { return environment @@ -328,7 +328,10 @@ extension RunDestinationInfo { case .elf: environment.prependPath(key: "LD_LIBRARY_PATH", value: toolchain.path.join("usr/lib/swift/\(platform)").str) case .pe: - environment.prependPath(key: .path, value: core.developerPath.path.join("Runtimes").join(toolchain.version.description).join("usr/bin").str) + let currentPath = Environment.current[.path] ?? "" + let runtimePath = core.developerPath.path.join("Runtimes").join(toolchain.version.description).join("usr/bin").str + let newPath = currentPath.isEmpty ? "\(runtimePath)" : "\(runtimePath);\(currentPath)" + environment[.path] = newPath case .macho: // Fall back to the OS provided Swift runtime break diff --git a/Sources/SWBUniversalPlatform/Specs/StandardFileTypes.xcspec b/Sources/SWBUniversalPlatform/Specs/StandardFileTypes.xcspec index 6afa5833..13695671 100644 --- a/Sources/SWBUniversalPlatform/Specs/StandardFileTypes.xcspec +++ b/Sources/SWBUniversalPlatform/Specs/StandardFileTypes.xcspec @@ -903,6 +903,7 @@ Class = PBXMachOFileType; BasedOn = compiled.mach-o; Extensions = (dylib); + Prefix = lib; IsLibrary = YES; IsDynamicLibrary = YES; CodeSignOnCopy = YES; @@ -939,6 +940,7 @@ Identifier = sourcecode.text-based-dylib-definition; BasedOn = sourcecode; Extensions = (tbd); + Prefix = lib; IsLibrary = YES; IsDynamicLibrary = YES; CodeSignOnCopy = YES; @@ -1471,7 +1473,7 @@ Identifier = archive.ar; BasedOn = archive; Extensions = (a); - Prefix = (lib); + Prefix = lib; IsLibrary = YES; IsStaticLibrary = YES; ContainsNativeCode = YES; diff --git a/Sources/SWBWindowsPlatform/Plugin.swift b/Sources/SWBWindowsPlatform/Plugin.swift index b3c095ef..c4e07e67 100644 --- a/Sources/SWBWindowsPlatform/Plugin.swift +++ b/Sources/SWBWindowsPlatform/Plugin.swift @@ -170,7 +170,7 @@ struct WindowsSDKRegistryExtension: SDKRegistryExtension { "GENERATE_INTERMEDIATE_TEXT_BASED_STUBS": "NO", "LIBRARY_SEARCH_PATHS": "$(inherited) $(SDKROOT)/usr/lib/swift/windows/$(CURRENT_ARCH)", - "TEST_LIBRARY_SEARCH_PATHS": .plString("\(testingLibraryPath.strWithPosixSlashes)/Testing-$(SWIFT_TESTING_VERSION)/usr/lib/swift/windows/$(CURRENT_ARCH) \(testingLibraryPath.strWithPosixSlashes)/XCTest-$(XCTEST_VERSION)/usr/lib/swift/windows/$(CURRENT_ARCH)"), + "TEST_LIBRARY_SEARCH_PATHS": .plString("\(testingLibraryPath.strWithPosixSlashes)/Testing-$(SWIFT_TESTING_VERSION)/usr/lib/swift/windows/ \(testingLibraryPath.strWithPosixSlashes)/Testing-$(SWIFT_TESTING_VERSION)/usr/lib/swift/windows/$(CURRENT_ARCH) \(testingLibraryPath.strWithPosixSlashes)/XCTest-$(XCTEST_VERSION)/usr/lib/swift/windows/$(CURRENT_ARCH) \(testingLibraryPath.strWithPosixSlashes)/XCTest-$(XCTEST_VERSION)/usr/lib/swift/windows"), "OTHER_SWIFT_FLAGS": "$(inherited) -libc $(DEFAULT_USE_RUNTIME)", "DEFAULT_USE_RUNTIME": "MD", diff --git a/Sources/SWBWindowsPlatform/Specs/Windows.xcspec b/Sources/SWBWindowsPlatform/Specs/Windows.xcspec index 3ed0fccb..c182b273 100644 --- a/Sources/SWBWindowsPlatform/Specs/Windows.xcspec +++ b/Sources/SWBWindowsPlatform/Specs/Windows.xcspec @@ -48,6 +48,7 @@ Identifier = com.apple.product-type.bundle.unit-test; BasedOn = com.apple.product-type.library.dynamic; DefaultBuildProperties = { + ENABLE_TESTING_SEARCH_PATHS = YES; // Index store data is required to discover XCTest tests COMPILER_INDEX_STORE_ENABLE = YES; SWIFT_INDEX_STORE_ENABLE = YES; @@ -87,6 +88,7 @@ BasedOn = default:com.apple.product-type.library.dynamic; HasInfoPlist = NO; DefaultBuildProperties = { + EXECUTABLE_PREFIX = ""; PUBLIC_HEADERS_FOLDER_PATH = ""; PRIVATE_HEADERS_FOLDER_PATH = ""; }; @@ -111,4 +113,25 @@ Identifier = org.swift.product-type.common.object; BasedOn = com.apple.product-type.library.static; }, + + { + Domain = windows; + Type = FileType; + Identifier = archive.ar; + BasedOn = archive; + Extensions = (lib); + IsLibrary = YES; + IsStaticLibrary = YES; + ContainsNativeCode = YES; + }, + + { + Domain = windows; + Type = FileType; + Identifier = compiled.mach-o.dylib; + BasedOn = compiled.mach-o; + Extensions = (dll); + IsLibrary = YES; + IsDynamicLibrary = YES; + } ) diff --git a/Tests/SWBBuildSystemTests/BuildOperationTests.swift b/Tests/SWBBuildSystemTests/BuildOperationTests.swift index 25f8c854..9b83dc32 100644 --- a/Tests/SWBBuildSystemTests/BuildOperationTests.swift +++ b/Tests/SWBBuildSystemTests/BuildOperationTests.swift @@ -94,7 +94,8 @@ fileprivate struct BuildOperationTests: CoreBasedTests { ], buildPhases: [ TestSourcesBuildPhase(["dynamic library.swift"]), - ] + ], + productReferenceName: "$(EXECUTABLE_NAME)", ), TestStandardTarget( "staticlib", @@ -108,7 +109,8 @@ fileprivate struct BuildOperationTests: CoreBasedTests { ], buildPhases: [ TestSourcesBuildPhase(["static library.swift"]), - ] + ], + productReferenceName: "$(EXECUTABLE_NAME)", ), ]) let core = try await getCore() @@ -279,7 +281,8 @@ fileprivate struct BuildOperationTests: CoreBasedTests { ], buildPhases: [ TestSourcesBuildPhase(["dynamic.swift"]), - ] + ], + productReferenceName: "$(EXECUTABLE_NAME)", ), TestStandardTarget( "staticlib", @@ -293,7 +296,8 @@ fileprivate struct BuildOperationTests: CoreBasedTests { ], buildPhases: [ TestSourcesBuildPhase(["static.swift"]), - ] + ], + productReferenceName: "$(EXECUTABLE_NAME)", ), ]) let core = try await getCore() @@ -422,7 +426,8 @@ fileprivate struct BuildOperationTests: CoreBasedTests { ], buildPhases: [ TestSourcesBuildPhase(["dynamic.swift"]), - ] + ], + productReferenceName: "$(EXECUTABLE_NAME)", ), TestStandardTarget( "staticlib", @@ -436,7 +441,8 @@ fileprivate struct BuildOperationTests: CoreBasedTests { ], buildPhases: [ TestSourcesBuildPhase(["static.swift"]), - ] + ], + productReferenceName: "$(EXECUTABLE_NAME)", ), ]) let core = try await getCore() @@ -546,7 +552,7 @@ fileprivate struct BuildOperationTests: CoreBasedTests { } } - @Test(.requireSDKs(.host), .skipHostOS(.macOS), .skipHostOS(.windows, "cannot find testing library")) + @Test(.requireSDKs(.host), .skipHostOS(.macOS)) func unitTestWithGeneratedEntryPoint() async throws { try await withTemporaryDirectory(removeTreeOnDeinit: false) { (tmpDir: Path) in let testProject = try await TestProject( @@ -582,7 +588,7 @@ fileprivate struct BuildOperationTests: CoreBasedTests { buildPhases: [ TestSourcesBuildPhase(), TestFrameworksBuildPhase([ - "MyTests.so" + TestBuildFile(.target("MyTests")) ]) ], dependencies: ["MyTests"] @@ -593,7 +599,7 @@ fileprivate struct BuildOperationTests: CoreBasedTests { buildConfigurations: [ TestBuildConfiguration("Debug", buildSettings: [ "LD_RUNPATH_SEARCH_PATHS": "$(RPATH_ORIGIN)", - "LD_DYLIB_INSTALL_NAME": "MyTests.so" + "LD_DYLIB_INSTALL_NAME": "$(EXECUTABLE_NAME)" ]) ], buildPhases: [ @@ -604,7 +610,7 @@ fileprivate struct BuildOperationTests: CoreBasedTests { ], dependencies: [ "library" ], - productReferenceName: "MyTests.so" + productReferenceName: "$(EXECUTABLE_NAME)" ), TestStandardTarget( "library", @@ -612,16 +618,16 @@ fileprivate struct BuildOperationTests: CoreBasedTests { buildConfigurations: [ TestBuildConfiguration("Debug", buildSettings: [ "LD_RUNPATH_SEARCH_PATHS": "$(RPATH_ORIGIN)", - "LD_DYLIB_INSTALL_NAME": "liblibrary.so", - // FIXME: Find a way to make these default "EXECUTABLE_PREFIX": "lib", "EXECUTABLE_PREFIX[sdk=windows*]": "", + "LD_DYLIB_INSTALL_NAME": "$(EXECUTABLE_NAME)", ]) ], buildPhases: [ TestSourcesBuildPhase(["library.swift"]), ], + productReferenceName: "$(EXECUTABLE_NAME)", ) ]) let core = try await getCore() @@ -656,21 +662,23 @@ fileprivate struct BuildOperationTests: CoreBasedTests { try await tester.checkBuild(runDestination: destination, persistent: true) { results in results.checkNoErrors() - let environment = destination.hostRuntimeEnvironment(core) + let environment = try destination.hostRuntimeEnvironment(core) do { let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: projectDir.join("build").join("Debug\(destination.builtProductsDirSuffix)").join(core.hostOperatingSystem.imageFormat.executableName(basename: "UnitTestRunner")).str), arguments: [], environment: environment) + #expect(executionResult.exitStatus == .exit(0)) #expect(String(decoding: executionResult.stdout, as: UTF8.self).contains("Executed 1 test")) } do { let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: projectDir.join("build").join("Debug\(destination.builtProductsDirSuffix)").join(core.hostOperatingSystem.imageFormat.executableName(basename: "UnitTestRunner")).str), arguments: ["--testing-library", "swift-testing"], environment: environment) + #expect(executionResult.exitStatus == .exit(0)) #expect(String(decoding: executionResult.stderr, as: UTF8.self).contains("Test run with 1 test ")) } } } } - @Test(.requireSDKs(.host), .skipHostOS(.macOS), .skipHostOS(.windows, "cannot find testing library")) + @Test(.requireSDKs(.host), .skipHostOS(.macOS)) func unitTestWithGeneratedEntryPoint_testabilityDisabled() async throws { try await withTemporaryDirectory(removeTreeOnDeinit: false) { (tmpDir: Path) in let testProject = try await TestProject( @@ -708,7 +716,7 @@ fileprivate struct BuildOperationTests: CoreBasedTests { buildPhases: [ TestSourcesBuildPhase(), TestFrameworksBuildPhase([ - "MyTests.so" + TestBuildFile(.target("MyTests")) ]) ], dependencies: ["MyTests"] @@ -719,7 +727,7 @@ fileprivate struct BuildOperationTests: CoreBasedTests { buildConfigurations: [ TestBuildConfiguration("Debug", buildSettings: [ "LD_RUNPATH_SEARCH_PATHS": "$(RPATH_ORIGIN)", - "LD_DYLIB_INSTALL_NAME": "MyTests.so" + "LD_DYLIB_INSTALL_NAME": "$(EXECUTABLE_NAME)" ]) ], buildPhases: [ @@ -730,7 +738,7 @@ fileprivate struct BuildOperationTests: CoreBasedTests { ], dependencies: [ "library" ], - productReferenceName: "MyTests.so" + productReferenceName: "$(EXECUTABLE_NAME)" ), TestStandardTarget( "library", @@ -738,16 +746,16 @@ fileprivate struct BuildOperationTests: CoreBasedTests { buildConfigurations: [ TestBuildConfiguration("Debug", buildSettings: [ "LD_RUNPATH_SEARCH_PATHS": "$(RPATH_ORIGIN)", - "LD_DYLIB_INSTALL_NAME": "liblibrary.so", - // FIXME: Find a way to make these default "EXECUTABLE_PREFIX": "lib", "EXECUTABLE_PREFIX[sdk=windows*]": "", + "LD_DYLIB_INSTALL_NAME": "$(EXECUTABLE_NAME)", ]) ], buildPhases: [ TestSourcesBuildPhase(["library.swift"]), ], + productReferenceName: "$(EXECUTABLE_NAME)", ) ]) let core = try await getCore() @@ -783,15 +791,21 @@ fileprivate struct BuildOperationTests: CoreBasedTests { results.checkWarning(.prefix("Skipping XCTest discovery for 'MyTests' because it was not built for testing")) results.checkNoErrors() - let environment = destination.hostRuntimeEnvironment(core) + let environment = try destination.hostRuntimeEnvironment(core) do { let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: projectDir.join("build").join("Debug\(destination.builtProductsDirSuffix)").join(core.hostOperatingSystem.imageFormat.executableName(basename: "UnitTestRunner")).str), arguments: [], environment: environment) + #expect(executionResult.exitStatus == .exit(0)) #expect(String(decoding: executionResult.stdout, as: UTF8.self).contains("Executed 0 tests")) } do { let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: projectDir.join("build").join("Debug\(destination.builtProductsDirSuffix)").join(core.hostOperatingSystem.imageFormat.executableName(basename: "UnitTestRunner")).str), arguments: ["--testing-library", "swift-testing"], environment: environment) - #expect(String(decoding: executionResult.stderr, as: UTF8.self).contains("Test run with 1 test ")) + withKnownIssue("On windows the test output indicates no tests ran, needs investigation") { + #expect(executionResult.exitStatus == .exit(0)) + #expect(String(decoding: executionResult.stderr, as: UTF8.self).contains("Test run with 1 test ")) + } when: { + core.hostOperatingSystem == .windows + } } } } diff --git a/Tests/SWBBuildSystemTests/CustomTaskBuildOperationTests.swift b/Tests/SWBBuildSystemTests/CustomTaskBuildOperationTests.swift index 736b48d2..e92f796c 100644 --- a/Tests/SWBBuildSystemTests/CustomTaskBuildOperationTests.swift +++ b/Tests/SWBBuildSystemTests/CustomTaskBuildOperationTests.swift @@ -29,7 +29,7 @@ fileprivate struct CustomTaskBuildOperationTests: CoreBasedTests { try await withTemporaryDirectory { tmpDir in let destination: RunDestinationInfo = .host let core = try await getCore() - let environment = destination.hostRuntimeEnvironment(core) + let environment = try destination.hostRuntimeEnvironment(core) let testProject = TestProject( "aProject", diff --git a/Tests/SWBBuildSystemTests/LinkerTests.swift b/Tests/SWBBuildSystemTests/LinkerTests.swift index 8991b51e..ed6fa8cd 100644 --- a/Tests/SWBBuildSystemTests/LinkerTests.swift +++ b/Tests/SWBBuildSystemTests/LinkerTests.swift @@ -223,7 +223,8 @@ fileprivate struct LinkerTests: CoreBasedTests { TestSourcesBuildPhase([ "library.swift" ]) - ] + ], + productReferenceName: "$(EXECUTABLE_NAME)" ), ]) let tester = try await BuildOperationTester(getCore(), testProject, simulated: false) diff --git a/Tests/SWBCoreTests/CommandLineSpecTests.swift b/Tests/SWBCoreTests/CommandLineSpecTests.swift index fa1c49e1..1a137783 100644 --- a/Tests/SWBCoreTests/CommandLineSpecTests.swift +++ b/Tests/SWBCoreTests/CommandLineSpecTests.swift @@ -1346,23 +1346,30 @@ import SWBMacro for useSearchPaths in [true, false] { let searchPathString = useSearchPaths ? "search" : "abs" for mode in LinkerSpec.LibrarySpecifier.Mode.allCases { + let prefix: String? let suffix = "_\(mode)_\(searchPathString)" let filePath: Path switch kind { case .static: filePath = Path.root.join("usr/lib/libfoo\(suffix).a") + prefix = producer.lookupFileType(identifier: "archive.ar")?.prefix case .dynamic: filePath = Path.root.join("usr/lib/libbar\(suffix).dylib") + prefix = producer.lookupFileType(identifier: "compiled.mach-o.dylib")?.prefix case .textBased: filePath = Path.root.join("usr/lib/libbaz\(suffix).tbd") + prefix = producer.lookupFileType(identifier: "sourcecode.text-based-dylib-definition")?.prefix case .framework: filePath = Path.root.join("tmp/Foo\(suffix).framework") + prefix = nil case .object: + prefix = nil continue case .objectLibrary: + prefix = nil continue } - result.append(LinkerSpec.LibrarySpecifier(kind: kind, path: filePath, mode: mode, useSearchPaths: useSearchPaths, swiftModulePaths: [:], swiftModuleAdditionalLinkerArgResponseFilePaths: [:])) + result.append(LinkerSpec.LibrarySpecifier(kind: kind, path: filePath, mode: mode, useSearchPaths: useSearchPaths, swiftModulePaths: [:], swiftModuleAdditionalLinkerArgResponseFilePaths: [:], prefix: prefix)) } } return result diff --git a/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift index f818fbef..71ec7d72 100644 --- a/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift +++ b/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift @@ -328,9 +328,7 @@ fileprivate struct UnitTestTaskConstructionTests: CoreBasedTests { "UnitTestRunner", type: .swiftpmTestRunner, buildConfigurations: [ - TestBuildConfiguration( - "Debug", - buildSettings: [:]) + TestBuildConfiguration("Debug") ], buildPhases: [ TestSourcesBuildPhase(), @@ -346,7 +344,11 @@ fileprivate struct UnitTestTaskConstructionTests: CoreBasedTests { buildConfigurations: [ TestBuildConfiguration( "Debug", - buildSettings: [:]) + buildSettings: [ + // FIXME: Find a way to make these default + "EXECUTABLE_PREFIX": "lib", + "EXECUTABLE_PREFIX[sdk=windows*]": "", + ]) ], buildPhases: [ TestSourcesBuildPhase([ @@ -355,7 +357,6 @@ fileprivate struct UnitTestTaskConstructionTests: CoreBasedTests { ]) ], dependencies: [], - productReferenceName: "$(EXCTABLE_NAME)" ), ]) let core = try await getCore() @@ -384,7 +385,7 @@ fileprivate struct UnitTestTaskConstructionTests: CoreBasedTests { ]) task.checkInputs([ .pathPattern(.suffix("UnitTestTarget.LinkFileList")), - .pathPattern(.or(.suffix("UnitTestTarget.so"), .suffix("UnitTestTarget.dll"))), + .pathPattern(.or(.suffix("/libUnitTestTarget.so"), .suffix("\\UnitTestTarget.dll"))), .namePattern(.any), .namePattern(.any), ]) From 8d4b214248100d08fcf6da128af9f8a2e2d666f5 Mon Sep 17 00:00:00 2001 From: Bassam Khouri Date: Tue, 25 Nov 2025 19:48:08 +0000 Subject: [PATCH 2/2] Set linker settings for non-Apple platform when use static swift stdlib Update the linker settings on non-Apple platforms when use static Swift stdlib is set. --- .../Tools/SwiftCompiler.swift | 13 +- Tests/SWBCoreTests/SwiftCompilerTests.swift | 119 ++++++++++++++++-- 2 files changed, 120 insertions(+), 12 deletions(-) diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift index d9ed7ad7..051c29fb 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift @@ -3061,9 +3061,16 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi if !forTAPI { if shouldStaticLinkStdlib { - args += [["-Xlinker", "-force_load_swift_libs"]] - // The Swift runtime requires libc++ & Foundation. - args += [["-lc++", "-framework", "Foundation"]] + // Platform-specific static linking flags + if producer.isApplePlatform { + // Darwin/Apple platforms use force_load_swift_libs and Framework linking + args += [["-Xlinker", "-force_load_swift_libs"]] + args += [["-lc++", "-framework", "Foundation"]] + } else { + // Non-Apple platforms (Linux, etc.) use static library linking + // Note: Foundation is not available as a framework on non-Apple platforms + args += [["-static-stdlib"]] + } } // Add the AST, if debugging. diff --git a/Tests/SWBCoreTests/SwiftCompilerTests.swift b/Tests/SWBCoreTests/SwiftCompilerTests.swift index f37311db..c5de4e4b 100644 --- a/Tests/SWBCoreTests/SwiftCompilerTests.swift +++ b/Tests/SWBCoreTests/SwiftCompilerTests.swift @@ -117,8 +117,9 @@ fileprivate final class TestSwiftParserDelegate: TaskOutputParserDelegate, Senda } /// Check the standard library linking options. - @Test(.requireHostOS(.macOS)) - func standardLibraryLinking() async throws { + @Test( + .requireHostOS(.macOS) + ) func standardLibraryLinkingMacOS() async throws { let core = try await getCore() // Computes the expected standard swift linker arguments. @@ -130,7 +131,6 @@ fileprivate final class TestSwiftParserDelegate: TaskOutputParserDelegate, Senda let defaultToolchain = try #require(core.toolchainRegistry.defaultToolchain) let swiftcPath = defaultToolchain.path.join("usr/bin/swiftc") - // Check basics. do { let producer = try MockCommandProducer(core: core, productTypeIdentifier: "com.apple.product-type.framework", platform: "macosx", toolchain: core.toolchainRegistry.defaultToolchain) @@ -142,10 +142,22 @@ fileprivate final class TestSwiftParserDelegate: TaskOutputParserDelegate, Senda let scope = MacroEvaluationScope(table: table) let delegate = TestTaskPlanningDelegate(clientDelegate: MockTestTaskPlanningClientDelegate(), fs: localFS) let optionContext = await spec.discoveredCommandLineToolSpecInfo(producer, scope, delegate) - try await #expect(spec.computeAdditionalLinkerArgs(producer, scope: scope, lookup: { _ in nil }, inputFileTypes: [], optionContext: optionContext, delegate: CapturingTaskGenerationDelegate(producer: producer, userPreferences: .defaultForTesting)).args == additionalSwiftLinkerArgs(spec, producer, scope, stdlibPath)) + let actual = try await spec.computeAdditionalLinkerArgs( + producer, + scope: scope, + lookup: { _ in nil }, + inputFileTypes: [], + optionContext: optionContext, + delegate: CapturingTaskGenerationDelegate( + producer: producer, + userPreferences: .defaultForTesting, + ), + ).args + let expected = additionalSwiftLinkerArgs(spec, producer, scope, stdlibPath) + #expect(actual == expected) } - // Check force static stdlib. + // Check force static stdlib on Apple platforms (macOS). do { let producer = try MockCommandProducer(core: core, productTypeIdentifier: "com.apple.product-type.framework", platform: "macosx") let stdlibPath = swiftcPath.dirname.dirname.join("lib/swift_static/fakeos") @@ -157,7 +169,20 @@ fileprivate final class TestSwiftParserDelegate: TaskOutputParserDelegate, Senda let scope = MacroEvaluationScope(table: table) let delegate = TestTaskPlanningDelegate(clientDelegate: MockTestTaskPlanningClientDelegate(), fs: localFS) let optionContext = await spec.discoveredCommandLineToolSpecInfo(producer, scope, delegate) - try await #expect(spec.computeAdditionalLinkerArgs(producer, scope: scope, lookup: { _ in nil }, inputFileTypes: [], optionContext: optionContext, delegate: CapturingTaskGenerationDelegate(producer: producer, userPreferences: .defaultForTesting)).args == (additionalSwiftLinkerArgs(spec, producer, scope, stdlibPath)) + [["-Xlinker", "-force_load_swift_libs"], ["-lc++", "-framework", "Foundation"]]) + // On Apple platforms, should use Apple-specific static linking flags + let actual = try await spec.computeAdditionalLinkerArgs( + producer, + scope: scope, + lookup: { _ in nil }, + inputFileTypes: [], + optionContext: optionContext, + delegate: CapturingTaskGenerationDelegate( + producer: producer, + userPreferences: .defaultForTesting, + ), + ).args + let expected = (additionalSwiftLinkerArgs(spec, producer, scope, stdlibPath)) + [["-Xlinker", "-force_load_swift_libs"], ["-lc++", "-framework", "Foundation"]] + #expect(actual == expected) } // Check tool product type. @@ -171,7 +196,18 @@ fileprivate final class TestSwiftParserDelegate: TaskOutputParserDelegate, Senda let scope = MacroEvaluationScope(table: table) let delegate = TestTaskPlanningDelegate(clientDelegate: MockTestTaskPlanningClientDelegate(), fs: localFS) let optionContext = await spec.discoveredCommandLineToolSpecInfo(producer, scope, delegate) - try await #expect(spec.computeAdditionalLinkerArgs(producer, scope: scope, lookup: { _ in nil }, inputFileTypes: [], optionContext: optionContext, delegate: CapturingTaskGenerationDelegate(producer: producer, userPreferences: .defaultForTesting)).args == additionalSwiftLinkerArgs(spec, producer, scope, stdlibPath)) + let actual = try await spec.computeAdditionalLinkerArgs( + producer, + scope: scope, lookup: { _ in nil }, + inputFileTypes: [], + optionContext: optionContext, + delegate: CapturingTaskGenerationDelegate( + producer: producer, + userPreferences: .defaultForTesting, + ), + ).args + let expected = additionalSwiftLinkerArgs(spec, producer, scope, stdlibPath) + #expect(actual == expected) } // Check tool product type forced to dynamic link. @@ -186,7 +222,19 @@ fileprivate final class TestSwiftParserDelegate: TaskOutputParserDelegate, Senda let scope = MacroEvaluationScope(table: table) let delegate = TestTaskPlanningDelegate(clientDelegate: MockTestTaskPlanningClientDelegate(), fs: localFS) let optionContext = await spec.discoveredCommandLineToolSpecInfo(producer, scope, delegate) - try await #expect(spec.computeAdditionalLinkerArgs(producer, scope: scope, lookup: { _ in nil }, inputFileTypes: [], optionContext: optionContext, delegate: CapturingTaskGenerationDelegate(producer: producer, userPreferences: .defaultForTesting)).args == additionalSwiftLinkerArgs(spec, producer, scope, stdlibPath)) + let actual = try await spec.computeAdditionalLinkerArgs( + producer, + scope: scope, + lookup: { _ in nil }, + inputFileTypes: [], + optionContext: optionContext, + delegate: CapturingTaskGenerationDelegate( + producer: producer, + userPreferences: .defaultForTesting, + ), + ).args + let expected = additionalSwiftLinkerArgs(spec, producer, scope, stdlibPath) + #expect(actual == expected) } // Check system stdlib option. @@ -201,7 +249,60 @@ fileprivate final class TestSwiftParserDelegate: TaskOutputParserDelegate, Senda let scope = MacroEvaluationScope(table: table) let delegate = TestTaskPlanningDelegate(clientDelegate: MockTestTaskPlanningClientDelegate(), fs: localFS) let optionContext = await spec.discoveredCommandLineToolSpecInfo(producer, scope, delegate) - try await #expect(spec.computeAdditionalLinkerArgs(producer, scope: scope, lookup: { _ in nil }, inputFileTypes: [], optionContext: optionContext, delegate: CapturingTaskGenerationDelegate(producer: producer, userPreferences: .defaultForTesting)).args == ([["-L/usr/lib/swift"]] + additionalSwiftLinkerArgs(spec, producer, scope, stdlibPath))) + let actual = try await spec.computeAdditionalLinkerArgs( + producer, + scope: scope, + lookup: { _ in nil }, + inputFileTypes: [], + optionContext: optionContext, + delegate: CapturingTaskGenerationDelegate( + producer: producer, + userPreferences: .defaultForTesting, + ), + ).args + let expected = ([["-L/usr/lib/swift"]] + additionalSwiftLinkerArgs(spec, producer, scope, stdlibPath)) + #expect(actual == expected) + } + } + @Test + func standardLibraryLinkingAllPlatforms() async throws { + let core = try await getCore() + + // Computes the expected standard swift linker arguments. + func additionalSwiftLinkerArgs(_ spec: CompilerSpec, _ producer: any CommandProducer, _ scope: MacroEvaluationScope, _ stdlibPath: Path) -> [[String]] { + return localFS.exists(stdlibPath) ? [["-L\(stdlibPath.str)"], ["-L/usr/lib/swift"]] : [["-L/usr/lib/swift"]] + } + + let spec = try core.specRegistry.getSpec() as SwiftCompilerSpec + + let defaultToolchain = try #require(core.toolchainRegistry.defaultToolchain) + let swiftcPath = defaultToolchain.path.join("usr/bin/swiftc") + // Check force static stdlib on non-Apple platforms (Linux). + do { + let producer = try MockCommandProducer(core: core, productTypeIdentifier: "com.apple.product-type.framework", platform: "linux") + let stdlibPath = swiftcPath.dirname.dirname.join("lib/swift_static/linux") + var table = MacroValueAssignmentTable(namespace: core.specRegistry.internalMacroNamespace) + table.push(BuiltinMacros.SWIFT_EXEC, literal: swiftcPath.str) + table.push(BuiltinMacros.SWIFT_STDLIB, literal: "swiftCore") + table.push(BuiltinMacros.PLATFORM_NAME, literal: "linux") + table.push(BuiltinMacros.SWIFT_FORCE_STATIC_LINK_STDLIB, literal: true) + let scope = MacroEvaluationScope(table: table) + let delegate = TestTaskPlanningDelegate(clientDelegate: MockTestTaskPlanningClientDelegate(), fs: localFS) + let optionContext = await spec.discoveredCommandLineToolSpecInfo(producer, scope, delegate) + // On non-Apple platforms, should use standard static linking flags (no Darwin-specific flags) + let actual = try await spec.computeAdditionalLinkerArgs( + producer, + scope: scope, + lookup: { _ in nil }, + inputFileTypes: [], + optionContext: optionContext, + delegate: CapturingTaskGenerationDelegate( + producer: producer, + userPreferences: .defaultForTesting, + ), + ).args + let expected = additionalSwiftLinkerArgs(spec, producer, scope, stdlibPath) + [["-static-stdlib"]] + #expect(actual == expected) } } }