diff --git a/Sources/SWBCore/Core.swift b/Sources/SWBCore/Core.swift index 59dc5c87..fd8bfecd 100644 --- a/Sources/SWBCore/Core.swift +++ b/Sources/SWBCore/Core.swift @@ -100,9 +100,10 @@ public final class Core: Sendable { await core.initializeToolchainRegistry() + await core.initializePlatformRegistry() + // Force loading SDKs. let sdkRegistry = core.sdkRegistry - for `extension` in await pluginManager.extensions(of: SDKRegistryExtensionPoint.self) { await sdkRegistry.registerSDKs(extension: `extension`, platformRegistry: core.platformRegistry) } @@ -178,6 +179,7 @@ public final class Core: Sendable { public let connectionMode: ServiceHostConnectionMode @_spi(Testing) public init(delegate: any CoreDelegate, hostOperatingSystem: OperatingSystem, pluginManager: PluginManager, developerPath: String, inferiorProductsPath: Path?, additionalContentPaths: [Path], environment: [String:String], buildServiceModTime: Date, connectionMode: ServiceHostConnectionMode) async throws { + self.delegate = delegate self.hostOperatingSystem = hostOperatingSystem self.pluginManager = pluginManager @@ -301,31 +303,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] - if let onlySearchAdditionalPlatformPaths = getEnvironmentVariable("XCODE_ONLY_EXTRA_PLATFORM_FOLDERS"), onlySearchAdditionalPlatformPaths.boolValue { - searchPaths = [] - } - else { - let platformsDir = self.developerPath.join("Platforms") - searchPaths = [platformsDir] - if hostOperatingSystem == .windows { - for dir in (try? localFS.listdir(platformsDir)) ?? [] { - searchPaths.append(platformsDir.join(dir)) - } - } - } - 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) - }() + let _platformRegistry: UnsafeDelayedInitializationSendableWrapper = .init() + public var platformRegistry: PlatformRegistry { + _platformRegistry.value + } @PluginExtensionSystemActor public var loadedPluginPaths: [Path] { pluginManager.pluginsByIdentifier.values.map(\.path) @@ -423,6 +404,29 @@ public final class Core: Sendable { _specRegistry = await SpecRegistry(self.pluginManager, self.registryDelegate, searchPaths, domainInclusions, [:]) } + 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 hostOperatingSystem == .windows { + for dir in (try? fs.listdir(platformsDir)) ?? [] { + searchPaths.append(platformsDir.join(dir)) + } + } + } + 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)) + } + /// Force all specs to be loaded. @_spi(Testing) public func loadAllSpecs() { // Load all platform domain specs first, as they provide the canonical definitions of build settings. diff --git a/Sources/SWBCore/Extensions/PlatformInfoExtension.swift b/Sources/SWBCore/Extensions/PlatformInfoExtension.swift index 784550a7..21b9f0c5 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] @@ -54,7 +54,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 329badb6..35bbec94 100644 --- a/Sources/SWBCore/PlatformRegistry.swift +++ b/Sources/SWBCore/PlatformRegistry.swift @@ -180,9 +180,9 @@ public final class Platform: Sendable { @_spi(Testing) public var sdks: [SDK] = [] /// The list of executable search paths in the platform. - @_spi(Testing) public var executableSearchPaths: [Path] + @_spi(Testing) public var executableSearchPaths: StackedSearchPath - init(_ name: String, _ displayName: String, _ familyName: String, _ familyDisplayName: String?, _ identifier: String, _ devicePlatformName: String?, _ simulatorPlatformName: String?, _ path: Path, _ version: String?, _ productBuildVersion: String?, _ defaultSettings: [String: PropertyListItem], _ additionalInfoPlistEntries: [String: PropertyListItem], _ isDeploymentPlatform: Bool, _ specRegistryProvider: any SpecRegistryProvider, preferredArchValue: String?, executableSearchPaths: [Path]) { + init(_ name: String, _ displayName: String, _ familyName: String, _ familyDisplayName: String?, _ identifier: String, _ devicePlatformName: String?, _ simulatorPlatformName: String?, _ path: Path, _ version: String?, _ productBuildVersion: String?, _ defaultSettings: [String: PropertyListItem], _ additionalInfoPlistEntries: [String: PropertyListItem], _ isDeploymentPlatform: Bool, _ specRegistryProvider: any SpecRegistryProvider, preferredArchValue: String?, executableSearchPaths: [Path], fs: any FSProxy) { self.name = name self.displayName = displayName self.familyName = familyName @@ -198,7 +198,7 @@ public final class Platform: Sendable { self.isDeploymentPlatform = isDeploymentPlatform self.specRegistryProvider = specRegistryProvider self.preferredArch = preferredArchValue - self.executableSearchPaths = executableSearchPaths + self.executableSearchPaths = StackedSearchPath(paths: executableSearchPaths, fs: fs) self.sdkCanonicalName = name } @@ -319,34 +319,34 @@ public final class PlatformRegistry { }) } - @_spi(Testing) public init(delegate: any PlatformRegistryDelegate, searchPaths: [Path], hostOperatingSystem: OperatingSystem) { + @_spi(Testing) public init(delegate: any PlatformRegistryDelegate, searchPaths: [Path], hostOperatingSystem: OperatingSystem, fs: any FSProxy) async { self.delegate = delegate for path in searchPaths { - registerPlatformsInDirectory(path) + await registerPlatformsInDirectory(path, fs) } do { if hostOperatingSystem.createFallbackSystemToolchain { - try registerFallbackSystemPlatform(operatingSystem: hostOperatingSystem) + 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) } - for platformExtension in platformInfoExtensions() { + for platformExtension in await platformInfoExtensions() { for (path, data) in platformExtension.additionalPlatforms() { - registerPlatform(path, .plDict(data)) + await registerPlatform(path, .plDict(data), fs) } } } - private func registerFallbackSystemPlatform(operatingSystem: OperatingSystem) throws { - try registerPlatform(Path("/"), .plDict(fallbackSystemPlatformSettings(operatingSystem: operatingSystem))) + 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] { @@ -413,10 +413,10 @@ public final class PlatformRegistry { } /// Register all platforms in the given directory. - private func registerPlatformsInDirectory(_ path: Path) { - for item in (try? localFS.listdir(path))?.sorted(by: <) ?? [] { - let itemPath = path.join(item) + private func registerPlatformsInDirectory(_ path: Path, _ fs: any FSProxy) async { + for item in (try? fs.listdir(path))?.sorted(by: <) ?? [] { + let itemPath = path.join(item) // Check if this is a platform we should load guard itemPath.fileSuffix == ".platform" else { continue } @@ -432,14 +432,15 @@ public final class PlatformRegistry { continue } - registerPlatform(itemPath, infoPlist) + 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) { + + private func registerPlatform(_ path: Path, _ data: PropertyListItem, _ fs: any FSProxy) async { // The data should always be a dictionary. guard case .plDict(var items) = data else { delegate.error(path, "unexpected platform data") @@ -614,7 +615,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 } @@ -624,8 +625,8 @@ public final class PlatformRegistry { path.join("usr").join("bin"), ] - 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)) } executableSearchPaths.append(contentsOf: [ @@ -635,7 +636,7 @@ public final class PlatformRegistry { ]) // FIXME: Need to parse other fields. It would also be nice to diagnose unused keys like we do for Spec data (and we might want to just use the spec parser here). - let platform = Platform(name, displayName, familyName, familyDisplayName, identifier, devicePlatformName, simulatorPlatformName, path, version, productBuildVersion, defaultSettings, additionalInfoPlistEntries, isDeploymentPlatform, delegate, preferredArchValue: preferredArchValue, executableSearchPaths: executableSearchPaths) + let platform = Platform(name, displayName, familyName, familyDisplayName, identifier, devicePlatformName, simulatorPlatformName, path, version, productBuildVersion, defaultSettings, additionalInfoPlistEntries, isDeploymentPlatform, delegate, preferredArchValue: preferredArchValue, executableSearchPaths: executableSearchPaths, fs: fs) if let duplicatePlatform = platformsByIdentifier[identifier] { delegate.error(path, "platform '\(identifier)' already registered from \(duplicatePlatform.path.str)") return diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift index 6a6c8d5a..e0a76e10 100644 --- a/Sources/SWBCore/Settings/BuiltinMacros.swift +++ b/Sources/SWBCore/Settings/BuiltinMacros.swift @@ -811,6 +811,7 @@ public final class BuiltinMacros { public static let LIBTOOL_DEPENDENCY_INFO_FILE = BuiltinMacros.declarePathMacro("LIBTOOL_DEPENDENCY_INFO_FILE") public static let LINKER = BuiltinMacros.declareStringMacro("LINKER") public static let ALTERNATE_LINKER = BuiltinMacros.declareStringMacro("ALTERNATE_LINKER") + public static let _LINKER_EXEC = BuiltinMacros.declarePathMacro("_LINKER_EXEC") public static let LINK_OBJC_RUNTIME = BuiltinMacros.declareBooleanMacro("LINK_OBJC_RUNTIME") public static let LINK_WITH_STANDARD_LIBRARIES = BuiltinMacros.declareBooleanMacro("LINK_WITH_STANDARD_LIBRARIES") public static let LIPO = BuiltinMacros.declareStringMacro("LIPO") @@ -1357,6 +1358,7 @@ public final class BuiltinMacros { ALL_SETTINGS, ALTERNATE_GROUP, ALTERNATE_LINKER, + _LINKER_EXEC, ALTERNATE_MODE, ALTERNATE_OWNER, ALTERNATE_PERMISSIONS_FILES, diff --git a/Sources/SWBCore/Settings/Settings.swift b/Sources/SWBCore/Settings/Settings.swift index 36efd74b..1c29d960 100644 --- a/Sources/SWBCore/Settings/Settings.swift +++ b/Sources/SWBCore/Settings/Settings.swift @@ -1015,7 +1015,7 @@ extension WorkspaceContext { } // Add the platform search paths. - for path in platform?.executableSearchPaths ?? [] { + for path in platform?.executableSearchPaths.paths ?? [] { paths.append(path) } diff --git a/Sources/SWBCore/Specs/Tools/LinkerTools.swift b/Sources/SWBCore/Specs/Tools/LinkerTools.swift index 28daf721..ceeb0698 100644 --- a/Sources/SWBCore/Specs/Tools/LinkerTools.swift +++ b/Sources/SWBCore/Specs/Tools/LinkerTools.swift @@ -401,7 +401,7 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec // FIXME: Honor LD_QUITE_LINKER_ARGUMENTS_FOR_COMPILER_DRIVER == NO ? - let optionContext = await discoveredCommandLineToolSpecInfo(cbc.producer, cbc.scope, delegate) + let optionContext: (any DiscoveredCommandLineToolSpecInfo)? = await discoveredCommandLineToolSpecInfo(cbc.producer, cbc.scope, delegate) // Gather additional linker arguments from the used tools. // @@ -571,7 +571,6 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec if !usesLDClassic, supportsSDKImportsFeature, !sdkImportsInfoFile.isEmpty, cbc.scope.evaluate(BuiltinMacros.ENABLE_SDK_IMPORTS), cbc.producer.isApplePlatform { commandLine.insert(contentsOf: ["-Xlinker", "-sdk_imports", "-Xlinker", sdkImportsInfoFile.str, "-Xlinker", "-sdk_imports_each_object"], at: commandLine.count - 2) // This preserves the assumption that the last argument is the linker output which a few tests make. outputs.append(delegate.createNode(sdkImportsInfoFile)) - await cbc.producer.processSDKImportsSpec.createTasks(CommandBuildContext(producer: cbc.producer, scope: cbc.scope, inputs: []), delegate, ldSDKImportsPath: sdkImportsInfoFile) } @@ -1239,11 +1238,18 @@ 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) + var linker = if alternateLinker != "" { Path(alternateLinker) } else { producer.hostOperatingSystem == .windows ? Path("link.exe") : Path("ld") } - let linkerPath = if alternateLinker != "" { Path(alternateLinker) } else { Path("ld") } + // ALTERNATE_LINKER is used in specs i.e. -fuseld=$ALTERNATE_LINKER, so it cannot be used to force the linker location. + // _LINKER_EXEC can be used to force the linker location, for usage in tests with empty pseudo filesystems. + let linkerExec = scope.evaluate(BuiltinMacros._LINKER_EXEC) + if !linkerExec.isEmpty{ + linker = linkerExec + } // Create the cache key. This is just the path to the ld linker we would invoke if we were invoking the linker directly. - guard let toolPath = producer.executableSearchPaths.lookup(linkerPath) else { + // Note: If the linker is an absolute path 'findExectable' will simply return the path to execute. + guard let toolPath = producer.executableSearchPaths.findExecutable(operatingSystem: producer.hostOperatingSystem, basename: linker.withoutSuffix) else { return nil } @@ -1547,7 +1553,7 @@ public final class LibtoolLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @u public func discoveredLinkerToolsInfo(_ producer: any CommandProducer, _ delegate: any CoreClientTargetDiagnosticProducingDelegate, at toolPath: Path) async -> (any DiscoveredCommandLineToolSpecInfo)? { do { do { - let commandLine = [toolPath.str, "-version_details"] + let commandLine = [toolPath.str] + (producer.hostOperatingSystem == .windows ? [] : ["-version_details"]) return try await producer.discoveredCommandLineToolSpecInfo(delegate, nil, commandLine, { executionResult in let gnuLD = [ #/GNU ld version (?[\d.]+)-.*/#, @@ -1556,7 +1562,6 @@ public func discoveredLinkerToolsInfo(_ producer: any CommandProducer, _ delegat if let match = try gnuLD.compactMap({ try $0.firstMatch(in: String(decoding: executionResult.stdout, as: UTF8.self)) }).first { return DiscoveredLdLinkerToolSpecInfo(linker: .gnuld, toolPath: toolPath, toolVersion: try Version(String(match.output.version)), architectures: Set()) } - let goLD = [ #/GNU gold version (?[\d.]+)-.*/#, #/GNU gold \(GNU Binutils.*\) (?[\d.]+)/#, @@ -1564,6 +1569,13 @@ public func discoveredLinkerToolsInfo(_ producer: any CommandProducer, _ delegat 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 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 @@ -1597,7 +1609,6 @@ public func discoveredLinkerToolsInfo(_ producer: any CommandProducer, _ delegat if let match = try lld.compactMap({ try $0.firstMatch(in: String(decoding: executionResult.stdout, as: UTF8.self)) }).first { return DiscoveredLdLinkerToolSpecInfo(linker: .lld, toolPath: toolPath, toolVersion: try Version(String(match.output.version)), architectures: Set()) } - throw e }) }) diff --git a/Sources/SWBTestSupport/CoreBasedTests.swift b/Sources/SWBTestSupport/CoreBasedTests.swift index 20b5796a..4865d7cf 100644 --- a/Sources/SWBTestSupport/CoreBasedTests.swift +++ b/Sources/SWBTestSupport/CoreBasedTests.swift @@ -234,6 +234,28 @@ extension CoreBasedTests { return try #require(defaultToolchain.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: "llvm-lib") ?? (localFS.exists(fallbacklibtool) ? fallbacklibtool : nil), "couldn't find llvm-lib in default toolchain") } } + + // The path to ld in default toolchain or plaftform. + package var ldPath: Path { + get async throws { + let (core, defaultToolchain) = try await coreAndToolchain() + let fallbacklibtool = Path("/usr/bin/ld") + let toolName = core.hostOperatingSystem == .windows ? "link" : "ld" + + // Look in the default toolchain. + if let executable = defaultToolchain.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: toolName) { + return executable + } + // Try in the registered platforms. + for platform in core.platformRegistry.platforms { + if let executable = platform.executableSearchPaths.findExecutable(operatingSystem: core.hostOperatingSystem, basename: toolName) { + return executable + } + } + // Finally try the fallback + return try #require(localFS.exists(fallbacklibtool) ? fallbacklibtool : nil, "couldn't find \(toolName) in default toolchain or in platform") + } + } /// If compilation caching is supported. package var supportsCompilationCaching: Bool { diff --git a/Sources/SWBTestSupport/DummyCommandProducer.swift b/Sources/SWBTestSupport/DummyCommandProducer.swift index f7e186a5..2d8cfc3f 100644 --- a/Sources/SWBTestSupport/DummyCommandProducer.swift +++ b/Sources/SWBTestSupport/DummyCommandProducer.swift @@ -56,7 +56,7 @@ package struct MockCommandProducer: CommandProducer, Sendable { for path in core.toolchainRegistry.defaultToolchain?.executableSearchPaths.paths ?? [] { paths.append(path) } - for path in platform?.executableSearchPaths ?? [] { + for path in platform?.executableSearchPaths.paths ?? [] { paths.append(path) } paths.append(core.developerPath.join("usr").join("bin")) diff --git a/Sources/SWBTestSupport/TaskPlanningTestSupport.swift b/Sources/SWBTestSupport/TaskPlanningTestSupport.swift index 806593ef..2e2598bd 100644 --- a/Sources/SWBTestSupport/TaskPlanningTestSupport.swift +++ b/Sources/SWBTestSupport/TaskPlanningTestSupport.swift @@ -192,6 +192,7 @@ open class MockTestTaskPlanningClientDelegate: TaskPlanningClientDelegate, @unch open func executeExternalTool(commandLine: [String], workingDirectory: String?, environment: [String: String]) async throws -> ExternalToolResult { let args = commandLine.dropFirst() + let commandBinary = commandLine.first.map(Path.init)?.basenameWithoutSuffix switch commandLine.first.map(Path.init)?.basenameWithoutSuffix { case "actool" where args == ["--version", "--output-format", "xml1"]: return .deferred @@ -208,6 +209,8 @@ open class MockTestTaskPlanningClientDelegate: TaskPlanningClientDelegate, @unch case "iig" where args == ["--version"]: return .deferred case "ld" where args == ["-version_details"]: + return .deferred + case "link": return .deferred case "libtool" where args == ["-V"] || args == ["--version"]: return .deferred @@ -219,6 +222,7 @@ open class MockTestTaskPlanningClientDelegate: TaskPlanningClientDelegate, @unch return .deferred case "what": return .deferred + default: break } diff --git a/Sources/SWBWindowsPlatform/Plugin.swift b/Sources/SWBWindowsPlatform/Plugin.swift index 59a31d8f..673a7f67 100644 --- a/Sources/SWBWindowsPlatform/Plugin.swift +++ b/Sources/SWBWindowsPlatform/Plugin.swift @@ -17,6 +17,7 @@ import Foundation @PluginExtensionSystemActor public func initializePlugin(_ manager: PluginManager) { manager.register(WindowsPlatformSpecsExtension(), type: SpecificationsExtensionPoint.self) manager.register(WindowsEnvironmentExtension(), type: EnvironmentExtensionPoint.self) + manager.register(WindowsPlatformExtension(), type: PlatformInfoExtensionPoint.self) } struct WindowsPlatformSpecsExtension: SpecificationsExtension { @@ -25,22 +26,44 @@ struct WindowsPlatformSpecsExtension: SpecificationsExtension { } } + +private func findLatestInstallDirectory(fs: any FSProxy) async throws -> Path? { + if try ProcessInfo.processInfo.hostOperatingSystem() == .windows { + let installations = try await VSInstallation.findInstallations(fs: fs) + .sorted(by: { $0.installationVersion > $1.installationVersion }) + if let latest = installations.first { + let msvcDir = latest.installationPath.join("VC").join("Tools").join("MSVC") + let versions = try fs.listdir(msvcDir).map { try Version($0) }.sorted { $0 > $1 } + if let latestVersion = versions.first { + let dir = msvcDir.join(latestVersion.description).str + return Path(dir) + } + } + } + return nil +} + struct WindowsEnvironmentExtension: EnvironmentExtension { func additionalEnvironmentVariables(fs: any FSProxy) async throws -> [String: String] { - if try ProcessInfo.processInfo.hostOperatingSystem() == .windows { - // Add the environment variable for the MSVC toolset for Swift and Clang to find it - let vcToolsInstallDir = "VCToolsInstallDir" - let installations = try await VSInstallation.findInstallations(fs: fs) - .sorted(by: { $0.installationVersion > $1.installationVersion }) - if let latest = installations.first { - let msvcDir = latest.installationPath.join("VC").join("Tools").join("MSVC") - let versions = try 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] - } - } + // Add the environment variable for the MSVC toolset for Swift and Clang to find it + let vcToolsInstallDir = "VCToolsInstallDir" + guard let dir = try? await findLatestInstallDirectory(fs: fs) else { + return [:] } - return [:] + return [vcToolsInstallDir: dir.str] } } + +struct WindowsPlatformExtension: PlatformInfoExtension { + public func additionalPlatformExecutableSearchPaths(platformName: String, platformPath: Path, fs: any FSProxy) async -> [Path] { + guard let dir = try? await findLatestInstallDirectory(fs: fs) else { + return [] + } + if Architecture.hostStringValue == "aarch64" { + return [dir.join("bin/Hostarm64/arm64")] + } else { + return [dir.join("bin/Hostx64/x64")] + } + } +} + diff --git a/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift b/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift index c2c1025e..a58f58b8 100644 --- a/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift +++ b/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift @@ -141,7 +141,7 @@ import Testing } } - @Test(.skipHostOS(.windows, "Failed to obtain command line tool spec info but no errors were emitted")) + @Test(.skipHostOS(.windows), .requireSystemPackages(apt: "libtool", yum: "libtool")) func discoveredLdLinkerSpecInfo() async throws { try await withSpec(LdLinkerSpec.self, .deferred) { (info: DiscoveredLdLinkerToolSpecInfo) in #expect(!info.toolPath.isEmpty) @@ -168,7 +168,24 @@ import Testing } } - @Test(.skipHostOS(.windows), .requireSystemPackages(apt: "libtool", yum: "libtool")) + + @Test(.requireHostOS(.windows)) + func discoveredLdLinkerSpecInfoWindows() async throws { + try await withSpec(LdLinkerSpec.self, .deferred, platform: "windows") { (info: DiscoveredLdLinkerToolSpecInfo) in + #expect(!info.toolPath.isEmpty) + #expect(info.toolVersion != nil) + if let toolVersion = info.toolVersion { + #expect(toolVersion > Version(0, 0, 0)) + } + } + 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") { (info: DiscoveredLdLinkerToolSpecInfo) in + #expect(!info.toolPath.isEmpty) + #expect(info.toolVersion == Version(14, 41, 34120)) + #expect(info.architectures == Set()) + } + } + + @Test(.skipHostOS(.windows)) func discoveredLibtoolSpecInfo() async throws { try await withSpec(LibtoolLinkerSpec.self, .deferred) { (info: DiscoveredLibtoolLinkerToolSpecInfo) in #expect(info.toolPath.basename == "libtool") diff --git a/Tests/SWBCoreTests/PlatformRegistryTests.swift b/Tests/SWBCoreTests/PlatformRegistryTests.swift index 3dedc454..b27a1f45 100644 --- a/Tests/SWBCoreTests/PlatformRegistryTests.swift +++ b/Tests/SWBCoreTests/PlatformRegistryTests.swift @@ -66,7 +66,7 @@ import SWBMacro } let delegate = await TestDataDelegate(pluginManager: PluginManager(skipLoadingPluginIdentifiers: [])) - let registry = PlatformRegistry(delegate: delegate, searchPaths: [tmpDirPath], hostOperatingSystem: try ProcessInfo.processInfo.hostOperatingSystem()) + let registry = await PlatformRegistry(delegate: delegate, searchPaths: [tmpDirPath], hostOperatingSystem: try ProcessInfo.processInfo.hostOperatingSystem(), fs: localFS) try await perform(registry, delegate) } }