diff --git a/Sources/SWBCore/Core.swift b/Sources/SWBCore/Core.swift index 6bef93f1..24ea9a0c 100644 --- a/Sources/SWBCore/Core.swift +++ b/Sources/SWBCore/Core.swift @@ -98,6 +98,8 @@ public final class Core: Sendable { await core.initializeSpecRegistry() + await core.initializePlatformRegistry() + await core.initializeToolchainRegistry() // Force loading SDKs. @@ -315,27 +317,10 @@ public final class Core: Sendable { @_spi(Testing) public var toolchainPaths: [(Path, strict: Bool)] /// The platform registry. - public lazy var platformRegistry: PlatformRegistry = { - // FIXME: We should support building the platforms (with symlinks) locally (for `inferiorProductsPath`). - - // Search the default location first (unless directed not to), then search any extra locations we've been passed. - var searchPaths: [Path] - let fs = localFS - if let onlySearchAdditionalPlatformPaths = getEnvironmentVariable("XCODE_ONLY_EXTRA_PLATFORM_FOLDERS"), onlySearchAdditionalPlatformPaths.boolValue { - searchPaths = [] - } - else { - let platformsDir = self.developerPath.join("Platforms") - searchPaths = [platformsDir] - } - if let additionalPlatformSearchPaths = getEnvironmentVariable("XCODE_EXTRA_PLATFORM_FOLDERS") { - for searchPath in additionalPlatformSearchPaths.split(separator: ":") { - searchPaths.append(Path(searchPath)) - } - } - searchPaths += UserDefaults.additionalPlatformSearchPaths - return PlatformRegistry(delegate: self.registryDelegate, searchPaths: searchPaths, hostOperatingSystem: hostOperatingSystem, fs: fs) - }() + let _platformRegistry: UnsafeDelayedInitializationSendableWrapper = .init() + public var platformRegistry: PlatformRegistry { + _platformRegistry.value + } @PluginExtensionSystemActor public var loadedPluginPaths: [Path] { pluginManager.pluginsByIdentifier.values.map(\.path) @@ -397,6 +382,25 @@ public final class Core: Sendable { private var _specRegistry: SpecRegistry? + private func initializePlatformRegistry() async { + var searchPaths: [Path] + let fs = localFS + if let onlySearchAdditionalPlatformPaths = getEnvironmentVariable("XCODE_ONLY_EXTRA_PLATFORM_FOLDERS"), onlySearchAdditionalPlatformPaths.boolValue { + searchPaths = [] + } else { + let platformsDir = self.developerPath.join("Platforms") + searchPaths = [platformsDir] + } + if let additionalPlatformSearchPaths = getEnvironmentVariable("XCODE_EXTRA_PLATFORM_FOLDERS") { + for searchPath in additionalPlatformSearchPaths.split(separator: Path.pathEnvironmentSeparator) { + searchPaths.append(Path(searchPath)) + } + } + searchPaths += UserDefaults.additionalPlatformSearchPaths + _platformRegistry.initialize(to: await PlatformRegistry(delegate: self.registryDelegate, searchPaths: searchPaths, hostOperatingSystem: hostOperatingSystem, fs: fs)) + } + + private func initializeToolchainRegistry() async { self.toolchainRegistry = await ToolchainRegistry(delegate: self.registryDelegate, searchPaths: self.toolchainPaths, fs: localFS, hostOperatingSystem: hostOperatingSystem) } diff --git a/Sources/SWBCore/Extensions/PlatformInfoExtension.swift b/Sources/SWBCore/Extensions/PlatformInfoExtension.swift index a5b21d34..584d4cbc 100644 --- a/Sources/SWBCore/Extensions/PlatformInfoExtension.swift +++ b/Sources/SWBCore/Extensions/PlatformInfoExtension.swift @@ -30,7 +30,7 @@ public protocol PlatformInfoExtension: Sendable { func additionalKnownTestLibraryPathSuffixes() -> [Path] - func additionalPlatformExecutableSearchPaths(platformName: String, platformPath: Path) -> [Path] + func additionalPlatformExecutableSearchPaths(platformName: String, platformPath: Path, fs: any FSProxy) async -> [Path] func additionalToolchainExecutableSearchPaths(toolchainIdentifier: String, toolchainPath: Path) -> [Path] @@ -56,7 +56,7 @@ extension PlatformInfoExtension { [] } - public func additionalPlatformExecutableSearchPaths(platformName: String, platformPath: Path) -> [Path] { + public func additionalPlatformExecutableSearchPaths(platformName: String, platformPath: Path, fs: any FSProxy) async -> [Path] { [] } diff --git a/Sources/SWBCore/PlatformRegistry.swift b/Sources/SWBCore/PlatformRegistry.swift index 8ad27ef2..4ada3d85 100644 --- a/Sources/SWBCore/PlatformRegistry.swift +++ b/Sources/SWBCore/PlatformRegistry.swift @@ -324,23 +324,23 @@ public final class PlatformRegistry { }) } - @_spi(Testing) public init(delegate: any PlatformRegistryDelegate, searchPaths: [Path], hostOperatingSystem: OperatingSystem, fs: any FSProxy) { + @_spi(Testing) public init(delegate: any PlatformRegistryDelegate, searchPaths: [Path], hostOperatingSystem: OperatingSystem, fs: any FSProxy) async { self.delegate = delegate for path in searchPaths { - registerPlatformsInDirectory(path, fs) + await registerPlatformsInDirectory(path, fs) } do { if hostOperatingSystem.createFallbackSystemToolchain { - try registerFallbackSystemPlatform(operatingSystem: hostOperatingSystem, fs: fs) + try await registerFallbackSystemPlatform(operatingSystem: hostOperatingSystem, fs: fs) } } catch { delegate.error(error) } - @preconcurrency @PluginExtensionSystemActor func platformInfoExtensions() -> [any PlatformInfoExtensionPoint.ExtensionProtocol] { - delegate.pluginManager.extensions(of: PlatformInfoExtensionPoint.self) + @preconcurrency @PluginExtensionSystemActor func platformInfoExtensions() async -> [any PlatformInfoExtensionPoint.ExtensionProtocol] { + return await delegate.pluginManager.extensions(of: PlatformInfoExtensionPoint.self) } struct Context: PlatformInfoExtensionAdditionalPlatformsContext { @@ -349,19 +349,20 @@ public final class PlatformRegistry { var fs: any FSProxy } - for platformExtension in platformInfoExtensions() { + for platformExtension in await platformInfoExtensions() { do { for (path, data) in try platformExtension.additionalPlatforms(context: Context(hostOperatingSystem: hostOperatingSystem, developerPath: delegate.developerPath, fs: fs)) { - registerPlatform(path, .plDict(data), fs) + await registerPlatform(path, .plDict(data), fs) } } catch { delegate.error(error) + } } } - private func registerFallbackSystemPlatform(operatingSystem: OperatingSystem, fs: any FSProxy) throws { - try registerPlatform(Path("/"), .plDict(fallbackSystemPlatformSettings(operatingSystem: operatingSystem)), fs) + private func registerFallbackSystemPlatform(operatingSystem: OperatingSystem, fs: any FSProxy) async throws { + try await registerPlatform(Path("/"), .plDict(fallbackSystemPlatformSettings(operatingSystem: operatingSystem)), fs) } private func fallbackSystemPlatformSettings(operatingSystem: OperatingSystem) throws -> [String: PropertyListItem] { @@ -428,7 +429,7 @@ public final class PlatformRegistry { } /// Register all platforms in the given directory. - private func registerPlatformsInDirectory(_ path: Path, _ fs: any FSProxy) { + private func registerPlatformsInDirectory(_ path: Path, _ fs: any FSProxy) async { for item in (try? localFS.listdir(path))?.sorted(by: <) ?? [] { let itemPath = path.join(item) @@ -446,14 +447,14 @@ public final class PlatformRegistry { // Silently skip loading the platform if it does not have an Info.plist at all. (We will still error below if it has an Info.plist which is malformed.) continue } - registerPlatform(itemPath, infoPlist, fs) + await registerPlatform(itemPath, infoPlist, fs) } catch let err { delegate.error(itemPath, "unable to load platform: 'Info.plist' was malformed: \(err)") } } } - private func registerPlatform(_ path: Path, _ data: PropertyListItem, _ fs: any FSProxy) { + private func registerPlatform(_ path: Path, _ data: PropertyListItem, _ fs: any FSProxy) async { // The data should always be a dictionary. guard case .plDict(let items) = data else { delegate.error(path, "unexpected platform data") @@ -617,7 +618,7 @@ public final class PlatformRegistry { delegate.pluginManager.extensions(of: PlatformInfoExtensionPoint.self) } - for platformExtension in platformInfoExtensions() { + for platformExtension in await platformInfoExtensions() { if let value = platformExtension.preferredArchValue(for: name) { preferredArchValue = value } @@ -631,10 +632,11 @@ public final class PlatformRegistry { path.join("Developer").join("SDKs") ] - for platformExtension in platformInfoExtensions() { - executableSearchPaths.append(contentsOf: platformExtension.additionalPlatformExecutableSearchPaths(platformName: name, platformPath: path)) + for platformExtension in await platformInfoExtensions() { + await executableSearchPaths.append(contentsOf: platformExtension.additionalPlatformExecutableSearchPaths(platformName: name, platformPath: path, fs: localFS)) platformExtension.adjustPlatformSDKSearchPaths(platformName: name, platformPath: path, sdkSearchPaths: &sdkSearchPaths) + } executableSearchPaths.append(contentsOf: [ diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index 58104578..f0cba65e 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -803,6 +803,8 @@ public final class BuiltinMacros { public static let LD_RUNPATH_SEARCH_PATHS = BuiltinMacros.declareStringListMacro("LD_RUNPATH_SEARCH_PATHS") public static let LD_SDK_IMPORTS_FILE = BuiltinMacros.declarePathMacro("LD_SDK_IMPORTS_FILE") public static let LD_WARN_UNUSED_DYLIBS = BuiltinMacros.declareBooleanMacro("LD_WARN_UNUSED_DYLIBS") + public static let _LD_MULTIARCH = BuiltinMacros.declareBooleanMacro("_LD_MULTIARCH") + public static let _LD_MULTIARCH_PREFIX_MAP = BuiltinMacros.declareStringListMacro("_LD_MULTIARCH_PREFIX_MAP") public static let LEX = BuiltinMacros.declarePathMacro("LEX") public static let LEXFLAGS = BuiltinMacros.declareStringListMacro("LEXFLAGS") public static let LIBRARIAN = BuiltinMacros.declareStringMacro("LIBRARIAN") @@ -1858,6 +1860,8 @@ public final class BuiltinMacros { LD_RUNPATH_SEARCH_PATHS, LD_SDK_IMPORTS_FILE, LD_WARN_UNUSED_DYLIBS, + _LD_MULTIARCH, + _LD_MULTIARCH_PREFIX_MAP, LEGACY_DEVELOPER_DIR, LEX, LEXFLAGS, diff --git a/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift b/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift index 1f38b0d2..1b062522 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift @@ -1273,7 +1273,6 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec } override public func discoveredCommandLineToolSpecInfo(_ producer: any CommandProducer, _ scope: MacroEvaluationScope, _ delegate: any CoreClientTargetDiagnosticProducingDelegate) async -> (any DiscoveredCommandLineToolSpecInfo)? { - let alternateLinker = scope.evaluate(BuiltinMacros.ALTERNATE_LINKER) // The ALTERNATE_LINKER is the 'name' of the linker not the executable name, clang will find the linker binary based on name passed via -fuse-ld, but we need to discover // its properties by executing the actual binary. There is a common filename when the linker is not "ld" across all platforms using "ld.(.exe)" // macOS (Xcode SDK) @@ -1309,15 +1308,47 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec // // Note: On Linux you cannot invoke the llvm linker by the direct name for determining the version, // you need to use ld. - var linkerPath = Path("ld") - if alternateLinker != "" && alternateLinker != "ld" { + let alternateLinker = scope.evaluate(BuiltinMacros.ALTERNATE_LINKER) + let isLinkerMultiarch = scope.evaluate(BuiltinMacros._LD_MULTIARCH) + + var linkerPath = producer.hostOperatingSystem == .windows ? Path("ld.lld") : Path("ld") + if alternateLinker != "" && alternateLinker != "ld" && alternateLinker != "link" { linkerPath = Path(producer.hostOperatingSystem.imageFormat.executableName(basename: "ld.\(alternateLinker)")) + } else if alternateLinker != "" { + linkerPath = Path(alternateLinker) + } + // If the linker does not support multiple architectures update the path to include a subfolder based on the prefix map + // to find the architecture specific executable. + if !isLinkerMultiarch { + let archMap = scope.evaluate(BuiltinMacros._LD_MULTIARCH_PREFIX_MAP) + let archMappings = archMap.reduce(into: [String: String]()) { mappings, map in + let (arch, prefixDir) = map.split(":") + if !arch.isEmpty && !prefixDir.isEmpty { + return mappings[arch] = prefixDir + } + } + if archMappings.isEmpty { + delegate.error("_LD_MULTIARCH is 'false', but no prefix mappings are present in _LD_MULTIARCH_PREFIX_MAP") + return nil + } + // Linkers that don't support multiple architectures cannot support universal binaries, so ARCHS will + // contain the target architecture and can only be a single value. + guard let arch = scope.evaluate(BuiltinMacros.ARCHS).only else { + delegate.error("_LD_MULTIARCH is 'false', but multiple ARCHS have been given, this is invalid") + return nil + } + if let prefix = archMappings[arch] { + // Add in the target architecture prefix directory to path for search. + linkerPath = Path(prefix).join(linkerPath) + } else { + delegate.error("Could not find prefix mapping for \(arch) in _LD_MULTIARCH_PREFIX_MAP") + return nil + } } - // Create the cache key. This is just the path to the linker we would invoke if we were invoking the linker directly. - guard let toolPath = producer.executableSearchPaths.lookup(linkerPath) else { + guard let toolPath = producer.executableSearchPaths.findExecutable(operatingSystem: producer.hostOperatingSystem, basename: linkerPath.str) else { return nil } - + // Create the cache key. This is just the path to the linker we would invoke if we were invoking the linker directly. return await discoveredLinkerToolsInfo(producer, delegate, at: toolPath) } } @@ -1638,10 +1669,19 @@ public func discoveredLinkerToolsInfo(_ producer: any CommandProducer, _ delegat #/GNU gold \(GNU Binutils.*\) (?[\d.]+)/#, // Ubuntu "GNU gold (GNU Binutils for Ubuntu 2.38) 1.16", Debian "GNU gold (GNU Binutils for Debian 2.40) 1.16" #/GNU gold \(version .*\) (?[\d.]+)/#, // Fedora "GNU gold (version 2.40-14.fc39) 1.16", RHEL "GNU gold (version 2.35.2-54.el9) 1.16", Amazon "GNU gold (version 2.29.1-31.amzn2.0.1) 1.14" ] + if let match = try goLD.compactMap({ try $0.firstMatch(in: String(decoding: executionResult.stdout, as: UTF8.self)) }).first { return DiscoveredLdLinkerToolSpecInfo(linker: .gold, toolPath: toolPath, toolVersion: try Version(String(match.output.version)), architectures: Set()) } + // link.exe has no option to simply dump the version, running, the program will no arguments or an invalid one will dump a header that contains the version. + let linkExe = [ + #/Microsoft \(R\) Incremental Linker Version (?[\d.]+)/# + ] + if let match = try linkExe.compactMap({ try $0.firstMatch(in: String(decoding: executionResult.stdout, as: UTF8.self)) }).first { + return DiscoveredLdLinkerToolSpecInfo(linker: .linkExe, toolPath: toolPath, toolVersion: try Version(String(match.output.version)), architectures: Set()) + } + struct LDVersionDetails: Decodable { let version: Version let architectures: Set diff --git a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec index 85a10454..d481110e 100644 --- a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec +++ b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec @@ -1148,6 +1148,11 @@ When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [CFBundleIdenti DefaultValue = NO; Category = "Linking - Warnings"; }, + { + Name = "_LD_MULTIARCH"; + Type = Boolean; + DefaultValue = YES; + }, { Name = "LIBRARY_SEARCH_PATHS"; Type = PathList; diff --git a/Sources/SWBTestSupport/CoreBasedTests.swift b/Sources/SWBTestSupport/CoreBasedTests.swift index 7aa6d82a..93d8b80f 100644 --- a/Sources/SWBTestSupport/CoreBasedTests.swift +++ b/Sources/SWBTestSupport/CoreBasedTests.swift @@ -272,53 +272,55 @@ extension CoreBasedTests { return nil } } - package var linkPath: Path? { + package var lldPath: Path? { get async throws { let (core, defaultToolchain) = try await coreAndToolchain() - if core.hostOperatingSystem != .windows { - // Most unixes have a link executable, but that is not a linker - return nil - } - if let executable = defaultToolchain.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: "link") { + if let executable = defaultToolchain.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: "ld.lld") { return executable } for platform in core.platformRegistry.platforms { - if let executable = platform.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: "link") { + if let executable = platform.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: "ld.lld") { return executable } } return nil } } - - package var lldPath: Path? { + package var goldPath: Path? { get async throws { let (core, defaultToolchain) = try await coreAndToolchain() - if let executable = defaultToolchain.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: "ld.ld") { + if let executable = defaultToolchain.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: "ld.gold") { return executable } for platform in core.platformRegistry.platforms { - if let executable = platform.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: "ld.ld") { + if let executable = platform.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: "ld.gold") { return executable } } return nil } } + package func linkPath(_ targetArchitecture: String) async throws -> Path? { + let (core, defaultToolchain) = try await self.coreAndToolchain() + let prefixMapping = ["aarch64" : "arm64", "arm64ec" : "arm64", "armv7" : "arm", "x86_64": "x64", "i686": "x86"] - package var goldPath: Path? { - get async throws { - let (core, defaultToolchain) = try await coreAndToolchain() - if let executable = defaultToolchain.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: "ld.gold") { + guard let prefix = prefixMapping[targetArchitecture] else { + return nil + } + let linkerPath = Path(prefix).join("link").str + if core.hostOperatingSystem != .windows { + // Most unixes have a link executable, but that is not a linker + return nil + } + if let executable = defaultToolchain.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: linkerPath) { + return executable + } + for platform in core.platformRegistry.platforms { + if let executable = platform.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: linkerPath) { return executable } - for platform in core.platformRegistry.platforms { - if let executable = platform.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: "ld.gold") { - return executable - } - } - return nil } + return nil } } diff --git a/Sources/SWBWindowsPlatform/Plugin.swift b/Sources/SWBWindowsPlatform/Plugin.swift index 933f7d2c..9ddfdf17 100644 --- a/Sources/SWBWindowsPlatform/Plugin.swift +++ b/Sources/SWBWindowsPlatform/Plugin.swift @@ -18,12 +18,13 @@ import Foundation let plugin = WindowsPlugin() manager.register(WindowsPlatformSpecsExtension(), type: SpecificationsExtensionPoint.self) manager.register(WindowsEnvironmentExtension(plugin: plugin), type: EnvironmentExtensionPoint.self) - manager.register(WindowsPlatformExtension(), type: PlatformInfoExtensionPoint.self) + manager.register(WindowsPlatformExtension(plugin: plugin), type: PlatformInfoExtensionPoint.self) manager.register(WindowsSDKRegistryExtension(), type: SDKRegistryExtensionPoint.self) } public final class WindowsPlugin: Sendable { private let vsInstallations = AsyncSingleValueCache<[VSInstallation], any Error>() + private let latestVsInstallationDirectory = AsyncSingleValueCache() public func cachedVSInstallations() async throws -> [VSInstallation] { try await vsInstallations.value { @@ -31,6 +32,23 @@ public final class WindowsPlugin: Sendable { try await VSInstallation.findInstallations(fs: localFS) } } + + func cachedLatestVSInstallDirectory(fs: any FSProxy) async throws -> Path? { + try await latestVsInstallationDirectory.value { + let installations = try await cachedVSInstallations() + .sorted(by: { $0.installationVersion > $1.installationVersion }) + if let latest = installations.first { + let msvcDir = latest.installationPath.join("VC").join("Tools").join("MSVC") + if fs.exists(msvcDir) { + let versions = try fs.listdir(msvcDir).map { try Version($0) }.sorted { $0 > $1 } + if let latestVersion = versions.first { + return msvcDir.join(latestVersion.description) + } + } + } + return nil + } + } } struct WindowsPlatformSpecsExtension: SpecificationsExtension { @@ -44,26 +62,19 @@ struct WindowsPlatformSpecsExtension: SpecificationsExtension { @_spi(Testing) public func additionalEnvironmentVariables(context: any EnvironmentExtensionAdditionalEnvironmentVariablesContext) async throws -> [String: String] { if context.hostOperatingSystem == .windows { - // Add the environment variable for the MSVC toolset for Swift and Clang to find it let vcToolsInstallDir = "VCToolsInstallDir" - let installations = try await plugin.cachedVSInstallations() - .sorted(by: { $0.installationVersion > $1.installationVersion }) - if let latest = installations.first { - let msvcDir = latest.installationPath.join("VC").join("Tools").join("MSVC") - if context.fs.exists(msvcDir) { - let versions = try context.fs.listdir(msvcDir).map { try Version($0) }.sorted { $0 > $1 } - if let latestVersion = versions.first { - let dir = msvcDir.join(latestVersion.description).str - return [vcToolsInstallDir: dir] - } - } + guard let dir = try? await plugin.cachedLatestVSInstallDirectory(fs: context.fs) else { + return [:] } + return [vcToolsInstallDir: dir.str] + } else { + return [:] } - return [:] } } struct WindowsPlatformExtension: PlatformInfoExtension { + let plugin: WindowsPlugin func additionalPlatforms(context: any PlatformInfoExtensionAdditionalPlatformsContext) throws -> [(path: Path, data: [String: PropertyListItem])] { let operatingSystem = context.hostOperatingSystem guard operatingSystem == .windows else { @@ -107,6 +118,23 @@ struct WindowsPlatformExtension: PlatformInfoExtension { sdkSearchPaths = [] } } + + public func additionalPlatformExecutableSearchPaths(platformName: String, platformPath: Path, fs: any FSProxy) async -> [Path] { + guard let dir = try? await plugin.cachedLatestVSInstallDirectory(fs: fs) else { + return [] + } + + // Note: Do not add in the target directories under the host as these will end up in the global search paths, i.e. PATH + // Let the commandlinetool discovery add in the target subdirectory based on the targeted architecture. + switch Architecture.hostStringValue { + case "aarch64": + return [dir.join("bin/Hostarm64")] + case "x86_64": + return [dir.join("bin/Hostx64")] + default: + return [] + } + } } struct WindowsSDKRegistryExtension: SDKRegistryExtension { diff --git a/Sources/SWBWindowsPlatform/Specs/WindowsLd.xcspec b/Sources/SWBWindowsPlatform/Specs/WindowsLd.xcspec index 4678c8a6..13c8f67b 100644 --- a/Sources/SWBWindowsPlatform/Specs/WindowsLd.xcspec +++ b/Sources/SWBWindowsPlatform/Specs/WindowsLd.xcspec @@ -52,6 +52,18 @@ CommandLineFlag = "--sysroot"; IsInputDependency = Yes; }, + { + Name = _LD_MULTIARCH; + Type = Boolean; + DefaultValue = NO; + Condition = "$(ALTERNATE_LINKER) == link"; + }, + { + Name = _LD_MULTIARCH_PREFIX_MAP; + Type = StringList; + DefaultValue = "aarch64:arm64 armv7:arm arm64ec:arm64 x86_64:x64 i686:x86"; + Condition = "$(ALTERNATE_LINKER) == link"; + }, { // No such concept Name = LD_DETERMINISTIC_MODE; diff --git a/Tests/SWBBuildSystemTests/LinkerTests.swift b/Tests/SWBBuildSystemTests/LinkerTests.swift index 489ce717..8a2f3d25 100644 --- a/Tests/SWBBuildSystemTests/LinkerTests.swift +++ b/Tests/SWBBuildSystemTests/LinkerTests.swift @@ -29,23 +29,26 @@ fileprivate struct LinkerTests: CoreBasedTests { groupTree: TestGroup( "SomeFiles", children: [ - TestFile("source.swift"), + TestFile("source.swift") ]), targets: [ TestStandardTarget( "testTarget", type: .framework, buildConfigurations: [ - TestBuildConfiguration("Debug", buildSettings: [ - "GENERATE_INFOPLIST_FILE": "YES", - "PRODUCT_NAME": "$(TARGET_NAME)", - "SWIFT_VERSION": swiftVersion, - "OTHER_LDFLAGS": "-not-a-real-flag" - ]), + TestBuildConfiguration( + "Debug", + buildSettings: [ + "GENERATE_INFOPLIST_FILE": "YES", + "PRODUCT_NAME": "$(TARGET_NAME)", + "SWIFT_VERSION": swiftVersion, + "OTHER_LDFLAGS": "-not-a-real-flag", + "ARCHS" : "x86_64 aarch64" + ]) ], buildPhases: [ - TestSourcesBuildPhase(["source.swift"]), + TestSourcesBuildPhase(["source.swift"]) ] - ), + ) ]) let tester = try await BuildOperationTester(getCore(), testProject, simulated: false) @@ -74,37 +77,41 @@ fileprivate struct LinkerTests: CoreBasedTests { "SomeFiles", children: [ TestFile("source.swift"), - TestFile("source.mm") + TestFile("source.mm"), ]), targets: [ TestStandardTarget( "testTarget", type: .application, buildConfigurations: [ - TestBuildConfiguration("Debug", buildSettings: [ - "GENERATE_INFOPLIST_FILE": "YES", - "PRODUCT_NAME": "$(TARGET_NAME)", - "SWIFT_VERSION": swiftVersion, - ]), + TestBuildConfiguration( + "Debug", + buildSettings: [ + "GENERATE_INFOPLIST_FILE": "YES", + "PRODUCT_NAME": "$(TARGET_NAME)", + "SWIFT_VERSION": swiftVersion, + ]) ], buildPhases: [ - TestSourcesBuildPhase(["source.mm"]), + TestSourcesBuildPhase(["source.mm"]) ], dependencies: [TestTargetDependency("testFramework")] ), TestStandardTarget( "testFramework", type: .framework, buildConfigurations: [ - TestBuildConfiguration("Debug", buildSettings: [ - "GENERATE_INFOPLIST_FILE": "YES", - "PRODUCT_NAME": "$(TARGET_NAME)", - "SWIFT_VERSION": swiftVersion, - "SWIFT_OBJC_INTEROP_MODE": enableInterop ? "objcxx" : "objc", - ]), + TestBuildConfiguration( + "Debug", + buildSettings: [ + "GENERATE_INFOPLIST_FILE": "YES", + "PRODUCT_NAME": "$(TARGET_NAME)", + "SWIFT_VERSION": swiftVersion, + "SWIFT_OBJC_INTEROP_MODE": enableInterop ? "objcxx" : "objc", + ]) ], buildPhases: [ TestSourcesBuildPhase(["source.swift"]) ] - ) + ), ]) } @@ -120,7 +127,7 @@ fileprivate struct LinkerTests: CoreBasedTests { } try await tester.checkBuild(runDestination: .macOS) { results in try results.checkTasks(.matchRuleType("Ld")) { tasks in - let task = try #require(tasks.first(where: { $0.outputPaths[0].ends(with: "testTarget") })) + let task = try #require(tasks.first(where: { $0.outputPaths[0].ends(with: "testTarget") })) task.checkCommandLineMatches([StringPattern.and(StringPattern.prefix("-L"), StringPattern.suffix("usr/lib/swift/macosx"))]) task.checkCommandLineContains(["-L/usr/lib/swift", "-lswiftCore"]) task.checkCommandLineMatches([StringPattern.suffix("testTarget.app/Contents/MacOS/testTarget")]) @@ -147,7 +154,7 @@ fileprivate struct LinkerTests: CoreBasedTests { } try await tester.checkBuild(runDestination: .macOS) { results in results.checkTasks(.matchRuleType("Ld")) { tasks in - let task = tasks.first(where: { $0.outputPaths[0].ends(with: "testTarget") })! + let task = tasks.first(where: { $0.outputPaths[0].ends(with: "testTarget") })! task.checkCommandLineNoMatch([StringPattern.and(StringPattern.prefix("-L"), StringPattern.suffix("usr/lib/swift/macosx"))]) task.checkCommandLineDoesNotContain("-L/usr/lib/swift") task.checkCommandLineDoesNotContain("-lswiftCore") @@ -165,15 +172,19 @@ fileprivate struct LinkerTests: CoreBasedTests { /// There is no reliable way to determine from a final linked binary which /// linker was used, so the test enables some verbosity to see which linker /// clang invokes. - /// Note: There is an output parser in the LinkerTool spec that does - /// error parsing and creates a new build error diagnostic with - /// a capaitalized error snippet, so this needs to be handled. + /// Notes: + /// * There is an output parser in the LinkerTool spec that does + /// error parsing and creates a new build error diagnostic with + /// a capaitalized error snippet, so this needs to be handled. + /// * The clang output on Windows has paths that have double slashes, not + /// quite valid command quoted. i.e. "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC" + /// This needs to be taken into account. @Test(.requireSDKs(.host)) func alternateLinkerSelection() async throws { let runDestination: RunDestinationInfo = .host let swiftVersion = try await self.swiftVersion try await withTemporaryDirectory { tmpDir in - let testProject = try await TestProject( + let testProject = TestProject( "TestProject", sourceRoot: tmpDir, groupTree: TestGroup( @@ -233,22 +244,22 @@ fileprivate struct LinkerTests: CoreBasedTests { let ldLinkerPath = try await self.ldPath let lldLinkerPath = try await self.lldPath let goldLinkerPath = try await self.goldPath - var linkLinkerPath = try await self.linkPath - if runDestination == .windows { - // Issue: Finding link.exe will fail until https://github.com/swiftlang/swift-build/pull/163 is merged. Clang will find it via PATH. - linkLinkerPath = Path("link.exe") - } - let installedLinkerPaths = [lldLinkerPath, ldLinkerPath, goldLinkerPath, linkLinkerPath].compactMap { $0 } + let linkLinkerPathX86 = try await self.linkPath("x86_64") + let linkLinkerPathAarch64 = try await self.linkPath("aarch64") + var installedLinkerPaths = [ldLinkerPath, lldLinkerPath, goldLinkerPath, linkLinkerPathX86, linkLinkerPathAarch64].compactMap { $0 } // Default Linker - var parameters = BuildParameters(configuration: "Debug", overrides: ["ALTERNATE_LINKER": ""]) + var parameters = BuildParameters(configuration: "Debug") try await tester.checkBuild(parameters: parameters, runDestination: .host) { results in results.checkTask(.matchRuleType("Ld")) { task in - results.checkTaskOutput(task) { taskOutput in - results.checkTaskOutput(task) { output in - // Expect that one of the installed linkers is used, we are not sure which one. - #expect(installedLinkerPaths.map { $0.str }.contains(where: output.asString.contains)) + results.checkTaskOutput(task) { output in + if runDestination == .windows { + // clang will choose to run lld-link rather than ld.lld.exe. + if let lldLinkerPath { + installedLinkerPaths.append(lldLinkerPath.dirname.join("lld-link")) + } } + #expect(installedLinkerPaths.map { $0.str }.contains(where: output.asString.replacingOccurrences(of: "\\\\", with: "\\").contains)) } } results.checkNoDiagnostics() @@ -273,22 +284,15 @@ fileprivate struct LinkerTests: CoreBasedTests { if let lldLinkerPath { parameters = BuildParameters(configuration: "Debug", overrides: ["ALTERNATE_LINKER": "lld"]) try await tester.checkBuild(parameters: parameters, runDestination: .host) { results in - if runDestination == .windows { - // Issue: Linker cannot find dependent library - results.checkError(.contains("Linker command failed with exit code 1")) - results.checkError(.contains("lld-link: error: could not open 'Library.lib'")) - } - results.checkTask(.matchRuleType("Ld")) { task in task.checkCommandLineContains(["-fuse-ld=lld"]) results.checkTaskOutput(task) { output in - // Expect that the default linker is called by clang + // Expect that the llvm linker is called by clang if runDestination == .windows { // clang will choose to run lld-link rather than ld.lld.exe. - // clang output will have escaped slashes in stdout. #expect(output.asString.replacingOccurrences(of: "\\\\", with: "\\").contains(lldLinkerPath.dirname.join("lld-link").str)) } else { - #expect(output.asString.contains(lldLinkerPath.str)) + #expect(output.asString.replacingOccurrences(of: "\\\\", with: "\\").contains(lldLinkerPath.str)) } } } @@ -303,8 +307,8 @@ fileprivate struct LinkerTests: CoreBasedTests { results.checkTask(.matchRuleType("Ld")) { task in task.checkCommandLineContains(["-fuse-ld=gold"]) results.checkTaskOutput(task) { output in - // Expect that the default linker is called by clang - #expect(output.asString.contains(goldLinkerPath.str)) + // Expect that the gold linker is called by clang + #expect(output.asString.replacingOccurrences(of: "\\\\", with: "\\").contains(goldLinkerPath.str)) } } results.checkNoDiagnostics() @@ -312,19 +316,46 @@ fileprivate struct LinkerTests: CoreBasedTests { } // link.exe - if let linkLinkerPath { - parameters = BuildParameters(configuration: "Debug", overrides: ["ALTERNATE_LINKER": "link"]) + if let linkLinkerPathX86 { + parameters = BuildParameters(configuration: "Debug", overrides: ["ARCHS": "x86_64", "ALTERNATE_LINKER": "link"]) try await tester.checkBuild(parameters: parameters, runDestination: .host) { results in results.checkTask(.matchRuleType("Ld")) { task in task.checkCommandLineContains(["-fuse-ld=link"]) results.checkTaskOutput(task) { output in - // Expect that the default linker is called by clang - #expect(output.asString.contains(linkLinkerPath.str)) + // Expect that the 'link' linker is called by clang + if runDestination == .windows && Architecture.hostStringValue == "aarch64" { + // rdar://145868953 - On windows aarch64 'clang' picks the wrong host architecture for link.exe, choosing "MSVC\14.41.34120\bin\Hostx86\x64\link.exe" + withKnownIssue("'clang' picks the wrong binary for link.exe using the Hostx86 version") { + #expect(output.asString.replacingOccurrences(of: "\\\\", with: "\\").contains(linkLinkerPathX86.str)) + } + } else { + #expect(output.asString.replacingOccurrences(of: "\\\\", with: "\\").contains(linkLinkerPathX86.str)) + } } } results.checkNoDiagnostics() } } + if let linkLinkerPathAarch64 { + parameters = BuildParameters(configuration: "Debug", overrides: ["ARCHS": "aarch64", "ALTERNATE_LINKER": "link"]) + try await tester.checkBuild(parameters: parameters, runDestination: .host) { results in + results.checkTask(.matchRuleType("Ld")) { task in + task.checkCommandLineContains(["-fuse-ld=link"]) + results.checkTaskOutput(task) { output in + // Expect that the 'link' linker is called by clang + if runDestination == .windows && Architecture.hostStringValue == "aarch64" { + // rdar://145868953 - On windows aarch64 'clang' picks the wrong host architecture for link.exe, choosing "MSVC\14.41.34120\bin\Hostx86\x64\link.exe" + withKnownIssue("'clang' picks the wrong binary for link.exe using the Hostx86 version") { + #expect(output.asString.replacingOccurrences(of: "\\\\", with: "\\").contains(linkLinkerPathAarch64.str)) + } + } else { + #expect(output.asString.replacingOccurrences(of: "\\\\", with: "\\").contains(linkLinkerPathAarch64.str)) + } + } + } + results.checkNoDiagnostics() + } + } } } } diff --git a/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift b/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift index 4500b083..a72051d7 100644 --- a/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift +++ b/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift @@ -16,6 +16,7 @@ import SWBProtocol import SWBTestSupport import SWBUtil import Testing +import SWBMacro @Suite fileprivate struct CommandLineToolSpecDiscoveredInfoTests: CoreBasedTests { @Test(.skipHostOS(.windows, "Failed to obtain command line tool spec info but no errors were emitted")) @@ -149,31 +150,88 @@ import Testing } } - @Test(.skipHostOS(.windows, "Failed to obtain command line tool spec info but no errors were emitted")) + // Linker tool discovery is a bit more complex as it afffected by the ALTERNATE_LINKER build setting. + func ldMacroTable() async throws -> MacroValueAssignmentTable { + let core = try await getCore() + return MacroValueAssignmentTable(namespace: core.specRegistry.internalMacroNamespace) + } + + @Test func discoveredLdLinkerSpecInfo() async throws { - try await withSpec(LdLinkerSpec.self, .deferred) { (info: DiscoveredLdLinkerToolSpecInfo) in + var table = try await ldMacroTable() + table.push(BuiltinMacros._LD_MULTIARCH, literal: true) + // Default Linker, just check we have one. + try await withSpec(LdLinkerSpec.self, .deferred, additionalTable: table) { (info: DiscoveredLdLinkerToolSpecInfo) in #expect(!info.toolPath.isEmpty) #expect(info.toolVersion != nil) if let toolVersion = info.toolVersion { #expect(toolVersion > Version(0, 0, 0)) } + } + } + + @Test(.requireSDKs(.macOS)) + func discoveredLdLinkerSpecInfo_macOS() async throws { + var table = try await ldMacroTable() + table.push(BuiltinMacros._LD_MULTIARCH, literal: true) + // Default Linker + try await withSpec(LdLinkerSpec.self, .deferred, additionalTable: table) { (info: DiscoveredLdLinkerToolSpecInfo) in + #expect(!info.toolPath.isEmpty) + #expect(info.toolVersion != nil) + if let toolVersion = info.toolVersion { + #expect(toolVersion > Version(0, 0, 0)) + } + #expect(info.linker == .ld64) // rdar://112109825 (ld_prime only reports arm64 and arm64e architectures in ld -version_details) // let expectedArchs = Set(["armv7", "armv7k", "armv7s", "arm64", "arm64e", "i386", "x86_64"]) // XCTAssertEqual(info.architectures.intersection(expectedArchs), expectedArchs) // XCTAssertFalse(info.architectures.contains("(tvOS)")) } - - try await withSpec(LdLinkerSpec.self, .result(status: .exit(0), stdout: Data("GNU ld (GNU Binutils for Debian) 2.40\n".utf8), stderr: Data())) { (info: DiscoveredLdLinkerToolSpecInfo) in + } + @Test(.requireSDKs(.linux)) + func discoveredLdLinkerSpecInfo_Linux() async throws { + var table = try await ldMacroTable() + table.push(BuiltinMacros._LD_MULTIARCH, literal: true) + // Default Linker + try await withSpec(LdLinkerSpec.self, .deferred, additionalTable: table) { (info: DiscoveredLdLinkerToolSpecInfo) in + #expect(!info.toolPath.isEmpty) + #expect(info.toolVersion != nil) + if let toolVersion = info.toolVersion { + #expect(toolVersion > Version(0, 0, 0)) + } + #expect(info.linker == .gnuld) + } + try await withSpec(LdLinkerSpec.self, .result(status: .exit(0), stdout: Data("GNU ld (GNU Binutils for Debian) 2.40\n".utf8), stderr: Data()), additionalTable: table) { (info: DiscoveredLdLinkerToolSpecInfo) in #expect(!info.toolPath.isEmpty) #expect(info.toolVersion == Version(2, 40)) #expect(info.architectures == Set()) } - try await withSpec(LdLinkerSpec.self, .result(status: .exit(0), stdout: Data("GNU ld version 2.29.1-31.amzn2.0.1\n".utf8), stderr: Data())) { (info: DiscoveredLdLinkerToolSpecInfo) in + try await withSpec(LdLinkerSpec.self, .result(status: .exit(0), stdout: Data("GNU ld version 2.29.1-31.amzn2.0.1\n".utf8), stderr: Data()), additionalTable: table) { (info: DiscoveredLdLinkerToolSpecInfo) in #expect(!info.toolPath.isEmpty) #expect(info.toolVersion == Version(2, 29, 1)) #expect(info.architectures == Set()) } + // llvm-ld + table.push(BuiltinMacros.ALTERNATE_LINKER, literal: "lld") + try await withSpec(LdLinkerSpec.self, .deferred, additionalTable: table) { (info: DiscoveredLdLinkerToolSpecInfo) in + #expect(!info.toolPath.isEmpty) + #expect(info.toolVersion != nil) + if let toolVersion = info.toolVersion { + #expect(toolVersion > Version(0, 0, 0)) + } + #expect(info.linker == .lld) + } + // gold + table.push(BuiltinMacros.ALTERNATE_LINKER, literal: "gold") + try await withSpec(LdLinkerSpec.self, .deferred, additionalTable: table) { (info: DiscoveredLdLinkerToolSpecInfo) in + #expect(!info.toolPath.isEmpty) + #expect(info.toolVersion != nil) + if let toolVersion = info.toolVersion { + #expect(toolVersion > Version(0, 0, 0)) + } + #expect(info.linker == .gold) + } } @Test(.skipHostOS(.windows), .requireSystemPackages(apt: "libtool", yum: "libtool")) diff --git a/Tests/SWBCoreTests/PlatformRegistryTests.swift b/Tests/SWBCoreTests/PlatformRegistryTests.swift index e3b4fb6a..fd74bfc6 100644 --- a/Tests/SWBCoreTests/PlatformRegistryTests.swift +++ b/Tests/SWBCoreTests/PlatformRegistryTests.swift @@ -70,7 +70,7 @@ import SWBMacro } let delegate = await TestDataDelegate(pluginManager: PluginManager(skipLoadingPluginIdentifiers: [])) - let registry = PlatformRegistry(delegate: delegate, searchPaths: [tmpDirPath], hostOperatingSystem: try ProcessInfo.processInfo.hostOperatingSystem(), fs: localFS) + let registry = await PlatformRegistry(delegate: delegate, searchPaths: [tmpDirPath], hostOperatingSystem: try ProcessInfo.processInfo.hostOperatingSystem(), fs: localFS) try await perform(registry, delegate) } } diff --git a/Tests/SWBWindowsPlatformTests/SWBWindowsPlatformTests.swift b/Tests/SWBWindowsPlatformTests/SWBWindowsPlatformTests.swift index 380508dc..25771aa5 100644 --- a/Tests/SWBWindowsPlatformTests/SWBWindowsPlatformTests.swift +++ b/Tests/SWBWindowsPlatformTests/SWBWindowsPlatformTests.swift @@ -9,4 +9,104 @@ // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// +import Foundation +@_spi(Testing) import SWBCore +import SWBProtocol +import SWBTestSupport +import SWBUtil +import Testing +import SWBMacro +@Suite fileprivate struct CommandLineToolSpecDiscoveredInfoTests: CoreBasedTests { + // Linker tool discovery is a bit more complex as it afffected by the ALTERNATE_LINKER build setting. + func ldMacroTable() async throws -> MacroValueAssignmentTable { + let core = try await getCore() + return MacroValueAssignmentTable(namespace: core.specRegistry.internalMacroNamespace) + } + + @Test(.requireSDKs(.windows)) + func discoveredLdLinkerSpecInfo_Windows() async throws { + var table = try await ldMacroTable() + let core = try await getCore() + table.push(BuiltinMacros._LD_MULTIARCH, literal: true) + try await withSpec(LdLinkerSpec.self, .deferred, platform: "windows", additionalTable: table) { (info: DiscoveredLdLinkerToolSpecInfo) in + #expect(!info.toolPath.isEmpty) + #expect(info.toolVersion != nil) + if let toolVersion = info.toolVersion { + #expect(toolVersion > Version(0, 0, 0)) + } + #expect(info.linker == .lld) + } + try await withSpec(LdLinkerSpec.self, .result(status: .exit(0), stdout: Data("Microsoft (R) Incremental Linker Version 14.41.34120.0\n".utf8), stderr: Data()), platform: "windows", additionalTable: table) { (info: DiscoveredLdLinkerToolSpecInfo) in + #expect(!info.toolPath.isEmpty) + #expect(info.toolVersion == Version(14, 41, 34120)) + #expect(info.architectures == Set()) + } + + // link.exe cannot be used for multipler architectures and requires a distinct link.exe for each target architecture + table.push(BuiltinMacros.ALTERNATE_LINKER, literal: "link") + table.push(BuiltinMacros._LD_MULTIARCH, literal: false) + table.push(BuiltinMacros._LD_MULTIARCH_PREFIX_MAP, literal: ["aarch64:arm64", "arm64ec:arm64", "armv7:arm", "x86_64:x64", "i686:x86"]) + + // link x86_64 + if try await core.hasVisualStudioComponent(.visualCppBuildTools_x86_x64) { + table.push(BuiltinMacros.ARCHS, literal: ["x86_64"]) + try await withSpec(LdLinkerSpec.self, .deferred, platform: "windows", additionalTable: table) { (info: DiscoveredLdLinkerToolSpecInfo) in + #expect(!info.toolPath.isEmpty) + #expect(info.toolVersion != nil) + if let toolVersion = info.toolVersion { + #expect(toolVersion > Version(0, 0, 0)) + } + #expect(info.linker == .linkExe) + } + } + // link i686 + if try await core.hasVisualStudioComponent(.visualCppBuildTools_x86_x64) { + table.push(BuiltinMacros.ARCHS, literal: ["i686"]) + try await withSpec(LdLinkerSpec.self, .deferred, platform: "windows", additionalTable: table) { (info: DiscoveredLdLinkerToolSpecInfo) in + #expect(!info.toolPath.isEmpty) + #expect(info.toolVersion != nil) + if let toolVersion = info.toolVersion { + #expect(toolVersion > Version(0, 0, 0)) + } + #expect(info.linker == .linkExe) + } + } + // link aarch64 + if try await core.hasVisualStudioComponent(.visualCppBuildTools_arm64) { + table.push(BuiltinMacros.ARCHS, literal: ["aarch64"]) + try await withSpec(LdLinkerSpec.self, .deferred, platform: "windows", additionalTable: table) { (info: DiscoveredLdLinkerToolSpecInfo) in + #expect(!info.toolPath.isEmpty) + #expect(info.toolVersion != nil) + if let toolVersion = info.toolVersion { + #expect(toolVersion > Version(0, 0, 0)) + } + #expect(info.linker == .linkExe) + } + } + // link armv7 + if try await core.hasVisualStudioComponent(.visualCppBuildTools_arm) { + table.push(BuiltinMacros.ARCHS, literal: ["armv7"]) + try await withSpec(LdLinkerSpec.self, .deferred, platform: "windows", additionalTable: table) { (info: DiscoveredLdLinkerToolSpecInfo) in + #expect(!info.toolPath.isEmpty) + #expect(info.toolVersion != nil) + if let toolVersion = info.toolVersion { + #expect(toolVersion > Version(0, 0, 0)) + } + #expect(info.linker == .linkExe) + } + } + // link arm64ec + if try await core.hasVisualStudioComponent(.visualCppBuildTools_arm64ec) { + table.push(BuiltinMacros.ARCHS, literal: ["arm64ec"]) + try await withSpec(LdLinkerSpec.self, .deferred, platform: "windows", additionalTable: table) { (info: DiscoveredLdLinkerToolSpecInfo) in + #expect(!info.toolPath.isEmpty) + #expect(info.toolVersion != nil) + if let toolVersion = info.toolVersion { + #expect(toolVersion > Version(0, 0, 0)) + } + #expect(info.linker == .linkExe) + } + } + } +} \ No newline at end of file