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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/windows_pre_build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ if ($InstallCMake) {
}

if (-not $SkipAndroid) {
choco install android-ndk
choco install -y android-ndk

Import-Module $env:ChocolateyInstall\helpers\chocolateyProfile.psm1
refreshenv
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
linux_pre_build_command: ./.github/scripts/linux_pre_build.sh
linux_build_command: 'swift test --no-parallel'
linux_swift_versions: '["nightly-main", "nightly-6.2"]'
windows_pre_build_command: 'Invoke-Program .\.github\scripts\windows_pre_build.ps1'
windows_swift_versions: '["nightly-main"]'
windows_build_command: 'Invoke-Program swift test --no-parallel'
enable_linux_static_sdk_build: true
Expand Down
6 changes: 3 additions & 3 deletions Sources/SWBAndroidPlatform/AndroidSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -266,16 +266,16 @@ public import Foundation
}
}

fileprivate extension AndroidSDK.NDK {
extension AndroidSDK.NDK {
/// The location of the Android NDK based on the `ANDROID_NDK_ROOT` environment variable (falling back to the deprecated but well known `ANDROID_NDK_HOME`).
/// - seealso: [Configuring NDK Path](https://github.com/android/ndk-samples/wiki/Configure-NDK-Path#terminologies)
static var environmentOverrideLocation: AbsolutePath? {
internal static var environmentOverrideLocation: AbsolutePath? {
(getEnvironmentVariable("ANDROID_NDK_ROOT") ?? getEnvironmentVariable("ANDROID_NDK_HOME"))?.nilIfEmpty.map { AbsolutePath($0) } ?? nil
}

/// Location of the Android NDK installed by the `google-android-ndk-*-installer` family of packages available in Debian 13 "Trixie" and Ubuntu 24.04 "Noble".
/// These packages are available in non-free / multiverse and multiple versions can be installed simultaneously.
static var defaultDebianLocation: AbsolutePath? {
fileprivate static var defaultDebianLocation: AbsolutePath? {
AbsolutePath("/usr/lib/android-ndk")
}
}
Expand Down
25 changes: 20 additions & 5 deletions Sources/SWBAndroidPlatform/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public let initializePlugin: PluginInitializationFunction = { manager in

@_spi(Testing) public final class AndroidPlugin: Sendable {
private let androidSDKInstallations = AsyncCache<OperatingSystem, [AndroidSDK]>()
private let androidOverrideNDKInstallation = AsyncCache<OperatingSystem, AndroidSDK.NDK?>()

func cachedAndroidSDKInstallations(host: OperatingSystem) async throws -> [AndroidSDK] {
try await androidSDKInstallations.value(forKey: host) {
Expand All @@ -34,8 +35,22 @@ public let initializePlugin: PluginInitializationFunction = { manager in
}
}

@_spi(Testing) public func effectiveInstallation(host: OperatingSystem) async throws -> (sdk: AndroidSDK, ndk: AndroidSDK.NDK)? {
func cachedAndroidOverrideNDKInstallation(host: OperatingSystem) async throws -> AndroidSDK.NDK? {
try await androidOverrideNDKInstallation.value(forKey: host) {
if let overridePath = AndroidSDK.NDK.environmentOverrideLocation {
return try AndroidSDK.NDK(host: host, path: overridePath, fs: localFS)
}
return nil
}
}

@_spi(Testing) public func effectiveInstallation(host: OperatingSystem) async throws -> (sdk: AndroidSDK?, ndk: AndroidSDK.NDK)? {
guard let androidSdk = try? await cachedAndroidSDKInstallations(host: host).first else {
// No SDK, but we might still have a standalone NDK from the env var override
if let overrideNDK = try? await cachedAndroidOverrideNDKInstallation(host: host) {
return (nil, overrideNDK)
}

return nil
}

Expand Down Expand Up @@ -63,9 +78,9 @@ struct AndroidEnvironmentExtension: EnvironmentExtension {
func additionalEnvironmentVariables(context: any EnvironmentExtensionAdditionalEnvironmentVariablesContext) async throws -> [String: String] {
switch context.hostOperatingSystem {
case .windows, .macOS, .linux:
if let latest = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first {
let sdkPath = latest.path.path.str
let ndkPath = latest.preferredNDK?.path.path.str
if let (sdk, ndk) = try? await plugin.effectiveInstallation(host: context.hostOperatingSystem) {
let sdkPath = sdk?.path.path.str
let ndkPath = ndk.path.path.str
return [
"ANDROID_HOME": sdkPath,
"ANDROID_SDK_ROOT": sdkPath,
Expand Down Expand Up @@ -220,7 +235,7 @@ struct AndroidToolchainRegistryExtension: ToolchainRegistryExtension {
let plugin: AndroidPlugin

func additionalToolchains(context: any ToolchainRegistryExtensionAdditionalToolchainsContext) async throws -> [Toolchain] {
guard let toolchainPath = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first?.preferredNDK?.toolchainPath else {
guard let toolchainPath = try? await plugin.effectiveInstallation(host: context.hostOperatingSystem)?.ndk.toolchainPath else {
return []
}

Expand Down
7 changes: 4 additions & 3 deletions Sources/SWBBuildService/Tools.swift
Original file line number Diff line number Diff line change
Expand Up @@ -569,11 +569,12 @@ private class SerializedDiagnosticsTool {
return false
}

let toolchain = core.toolchainRegistry.defaultToolchain
guard let libclangPath = toolchain?.librarySearchPaths.findLibrary(operatingSystem: core.hostOperatingSystem, basename: "clang") ?? toolchain?.fallbackLibrarySearchPaths.findLibrary(operatingSystem: core.hostOperatingSystem, basename: "clang") else {
throw StubError.error("unable to find libclang")
guard let toolchain = core.toolchainRegistry.defaultToolchain else {
throw StubError.error("unable to find libclang (no default toolchain)")
}

let libclangPath = try toolchain.lookup(subject: .library(basename: "clang"), operatingSystem: core.hostOperatingSystem)

guard let libclang = Libclang(path: libclangPath.str) else {
emitError("unable to open libclang: \(libclangPath)")
return false
Expand Down
18 changes: 9 additions & 9 deletions Sources/SWBCore/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -269,35 +269,35 @@ public final class Core: Sendable {
self.stopAfterOpeningLibClang = UserDefaults.stopAfterOpeningLibClang

self.toolchainPaths = {
var toolchainPaths = [(Path, strict: Bool)]()
var toolchainPaths = [ToolchainRegistry.SearchPath]()

switch developerPath {
case .xcode(let path):
toolchainPaths.append((path.join("Toolchains"), strict: path.str.hasSuffix(".app/Contents/Developer")))
toolchainPaths.append(.init(path: path.join("Toolchains"), strict: path.str.hasSuffix(".app/Contents/Developer")))
case .swiftToolchain(let path, xcodeDeveloperPath: let xcodeDeveloperPath):
if hostOperatingSystem == .windows {
toolchainPaths.append((path.join("Toolchains"), strict: true))
toolchainPaths.append(.init(path: path.join("Toolchains"), strict: true, aliases: ["default"]))
} else {
toolchainPaths.append((path, strict: true))
toolchainPaths.append(.init(path: path, strict: true))
}
if let xcodeDeveloperPath {
toolchainPaths.append((xcodeDeveloperPath.join("Toolchains"), strict: xcodeDeveloperPath.str.hasSuffix(".app/Contents/Developer")))
toolchainPaths.append(.init(path: xcodeDeveloperPath.join("Toolchains"), strict: xcodeDeveloperPath.str.hasSuffix(".app/Contents/Developer")))
}
}

// FIXME: We should support building the toolchain locally (for `inferiorProductsPath`).

toolchainPaths.append((Path("/Library/Developer/Toolchains"), strict: false))
toolchainPaths.append(.init(path: Path("/Library/Developer/Toolchains"), strict: false))

if let homeString = getEnvironmentVariable("HOME")?.nilIfEmpty {
let userToolchainsPath = Path(homeString).join("Library/Developer/Toolchains")
toolchainPaths.append((userToolchainsPath, strict: false))
toolchainPaths.append(.init(path: userToolchainsPath, strict: false))
}

if let externalToolchainDirs = getEnvironmentVariable("EXTERNAL_TOOLCHAINS_DIR") ?? environment["EXTERNAL_TOOLCHAINS_DIR"] {
let envPaths = externalToolchainDirs.split(separator: Path.pathEnvironmentSeparator)
for envPath in envPaths {
toolchainPaths.append((Path(envPath), strict: false))
toolchainPaths.append(.init(path: Path(envPath), strict: false))
}
}

Expand Down Expand Up @@ -367,7 +367,7 @@ public final class Core: Sendable {
}()

/// The list of toolchain search paths.
@_spi(Testing) public var toolchainPaths: [(Path, strict: Bool)]
@_spi(Testing) public var toolchainPaths: [ToolchainRegistry.SearchPath]

/// The platform registry.
let _platformRegistry: UnsafeDelayedInitializationSendableWrapper<PlatformRegistry> = .init()
Expand Down
2 changes: 1 addition & 1 deletion Sources/SWBCore/Settings/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ fileprivate struct PreOverridesSettings {
if let toolchain = core.toolchainRegistry.lookup("default") {
self.defaultToolchain = toolchain
} else {
core.delegate.error("missing required default toolchain")
core.delegate.error("missing required default toolchain (\(core.toolchainRegistry.toolchains.count) loaded toolchain(s): \(core.toolchainRegistry.toolchains.map { $0.identifier }.joined(separator: " "))")
self.defaultToolchain = nil
}

Expand Down
36 changes: 34 additions & 2 deletions Sources/SWBCore/Settings/StackedSearchPaths.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,43 @@ public final class StackedSearchPath: Sendable {
}

extension StackedSearchPath {
public func lookup(subject: StackedSearchPathLookupSubject, operatingSystem: OperatingSystem) -> Path? {
lookup(subject.fileName(operatingSystem: operatingSystem))
}

public func findExecutable(operatingSystem: OperatingSystem, basename: String) -> Path? {
lookup(Path(operatingSystem.imageFormat.executableName(basename: basename)))
lookup(subject: .executable(basename: basename), operatingSystem: operatingSystem)
}

public func findLibrary(operatingSystem: OperatingSystem, basename: String) -> Path? {
lookup(Path("lib\(basename).\(operatingSystem.imageFormat.dynamicLibraryExtension)"))
lookup(subject: .library(basename: basename), operatingSystem: operatingSystem)
}
}

public enum StackedSearchPathLookupSubject {
case executable(basename: String)
case library(basename: String)

func fileName(operatingSystem: OperatingSystem) -> Path {
switch self {
case let .executable(basename):
Path(operatingSystem.imageFormat.executableName(basename: basename))
case let .library(basename):
Path("lib\(basename).\(operatingSystem.imageFormat.dynamicLibraryExtension)")
}
}
}

public enum StackedSearchPathLookupError: Error {
case unableToFind(subject: StackedSearchPathLookupSubject, operatingSystem: OperatingSystem, searchPaths: [StackedSearchPath])
}

extension StackedSearchPathLookupError: CustomStringConvertible {
public var description: String {
switch self {
case let .unableToFind(subject, operatingSystem, searchPaths):
let candidates = searchPaths.flatMap { $0.paths.map { $0.join(subject.fileName(operatingSystem: operatingSystem)).str }}
return "unable to find \(subject.fileName(operatingSystem: operatingSystem)) among search paths: \(candidates.joined(separator: ", "))"
}
}
}
6 changes: 3 additions & 3 deletions Sources/SWBCore/TaskGeneration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1308,10 +1308,10 @@ extension TaskOutputParserDelegate {
func readSerializedDiagnostics(at path: Path, workingDirectory: Path, workspaceContext: WorkspaceContext) -> [Diagnostic] {
do {
// Using the default toolchain's libclang regardless of context should be sufficient, since we assume serialized diagnostics to be a stable format.
let toolchain = workspaceContext.core.toolchainRegistry.defaultToolchain
guard let libclangPath = toolchain?.librarySearchPaths.findLibrary(operatingSystem: workspaceContext.core.hostOperatingSystem, basename: "clang") ?? toolchain?.fallbackLibrarySearchPaths.findLibrary(operatingSystem: workspaceContext.core.hostOperatingSystem, basename: "clang") else {
throw StubError.error("unable to find libclang")
guard let toolchain = workspaceContext.core.toolchainRegistry.defaultToolchain else {
throw StubError.error("unable to find libclang (no default toolchain)")
}
let libclangPath = try toolchain.lookup(subject: .library(basename: "clang"), operatingSystem: workspaceContext.core.hostOperatingSystem)
guard let libclang = workspaceContext.core.lookupLibclang(path: libclangPath).libclang else {
throw StubError.error("unable to open libclang: '\(libclangPath.str)'")
}
Expand Down
57 changes: 38 additions & 19 deletions Sources/SWBCore/ToolchainRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public final class Toolchain: Hashable, Sendable {
self.testingLibraryPlatformNames = testingLibraryPlatformNames
}

convenience init(path: Path, operatingSystem: OperatingSystem, fs: any FSProxy, pluginManager: any PluginManager, platformRegistry: PlatformRegistry?) async throws {
convenience init(path: Path, operatingSystem: OperatingSystem, aliases additionalAliases: Set<String>, fs: any FSProxy, pluginManager: any PluginManager, platformRegistry: PlatformRegistry?) async throws {
let data: PropertyListItem

do {
Expand Down Expand Up @@ -216,6 +216,8 @@ public final class Toolchain: Hashable, Sendable {
aliases = Toolchain.deriveAliases(path: path, identifier: identifier)
}

aliases.formUnion(additionalAliases)

// Framework Search Paths
var frameworkSearchPaths = Array<String>()
if let infoFrameworkSearchPaths = items["FallbackFrameworkSearchPaths"] {
Expand Down Expand Up @@ -412,8 +414,32 @@ extension Array where Element == Toolchain {
}
}

extension Toolchain {
public func lookup(subject: StackedSearchPathLookupSubject, operatingSystem: OperatingSystem) throws(StackedSearchPathLookupError) -> Path {
let searchPathsList = [librarySearchPaths, fallbackLibrarySearchPaths]
for searchPaths in searchPathsList {
if let library = searchPaths.lookup(subject: subject, operatingSystem: operatingSystem) {
return library
}
}
throw .unableToFind(subject: subject, operatingSystem: operatingSystem, searchPaths: searchPathsList)
}
}

/// The ToolchainRegistry manages the set of registered toolchains.
public final class ToolchainRegistry: @unchecked Sendable {
@_spi(Testing) public struct SearchPath: Sendable {
public var path: Path
public var strict: Bool
public var aliases: Set<String> = []

public init(path: Path, strict: Bool, aliases: Set<String> = []) {
self.path = path
self.strict = strict
self.aliases = aliases
}
}

let fs: any FSProxy
let hostOperatingSystem: OperatingSystem

Expand All @@ -427,17 +453,19 @@ public final class ToolchainRegistry: @unchecked Sendable {

public static let appleToolchainIdentifierPrefix: String = "com.apple.dt.toolchain."

@_spi(Testing) public init(delegate: any ToolchainRegistryDelegate, searchPaths: [(Path, strict: Bool)], fs: any FSProxy, hostOperatingSystem: OperatingSystem) async {
@_spi(Testing) public init(delegate: any ToolchainRegistryDelegate, searchPaths: [SearchPath], fs: any FSProxy, hostOperatingSystem: OperatingSystem) async {
self.fs = fs
self.hostOperatingSystem = hostOperatingSystem

for (path, strict) in searchPaths {
for searchPath in searchPaths {
let path = searchPath.path
let strict = searchPath.strict
if !strict && !fs.exists(path) {
continue
}

do {
try await registerToolchainsInDirectory(path, strict: strict, operatingSystem: hostOperatingSystem, delegate: delegate)
try await registerToolchainsInDirectory(path, strict: strict, aliases: searchPath.aliases, operatingSystem: hostOperatingSystem, delegate: delegate)
}
catch let err {
delegate.issue(strict: strict, path, "failed to load toolchains in \(path.str): \(err)")
Expand All @@ -462,7 +490,7 @@ public final class ToolchainRegistry: @unchecked Sendable {
}

/// Register all the toolchains in the given directory.
private func registerToolchainsInDirectory(_ path: Path, strict: Bool, operatingSystem: OperatingSystem, delegate: any ToolchainRegistryDelegate) async throws {
private func registerToolchainsInDirectory(_ path: Path, strict: Bool, aliases: Set<String>, operatingSystem: OperatingSystem, delegate: any ToolchainRegistryDelegate) async throws {
let toolchainPaths: [Path] = try fs.listdir(path)
.sorted()
.map { path.join($0) }
Expand All @@ -475,7 +503,7 @@ public final class ToolchainRegistry: @unchecked Sendable {
guard toolchainPath.basenameWithoutSuffix != "swift-latest" else { continue }

do {
let toolchain = try await Toolchain(path: toolchainPath, operatingSystem: operatingSystem, fs: fs, pluginManager: delegate.pluginManager, platformRegistry: delegate.platformRegistry)
let toolchain = try await Toolchain(path: toolchainPath, operatingSystem: operatingSystem, aliases: aliases, fs: fs, pluginManager: delegate.pluginManager, platformRegistry: delegate.platformRegistry)
try register(toolchain)
} catch let err {
delegate.issue(strict: strict, toolchainPath, "failed to load toolchain: \(err)")
Expand Down Expand Up @@ -505,24 +533,15 @@ public final class ToolchainRegistry: @unchecked Sendable {
/// Look up the toolchain with the given identifier.
public func lookup(_ identifier: String) -> Toolchain? {
let lowercasedIdentifier = identifier.lowercased()
if hostOperatingSystem == .macOS {
if ["default", "xcode"].contains(lowercasedIdentifier) {
return toolchainsByIdentifier[ToolchainRegistry.defaultToolchainIdentifier] ?? toolchainsByAlias[lowercasedIdentifier]
} else {
return toolchainsByIdentifier[identifier] ?? toolchainsByAlias[lowercasedIdentifier]
}
if ["default", "xcode"].contains(lowercasedIdentifier) {
return toolchainsByIdentifier[ToolchainRegistry.defaultToolchainIdentifier] ?? toolchainsByAlias[lowercasedIdentifier]
} else {
// On non-Darwin, assume if there is only one registered toolchain, it is the default.
if ["default", "xcode"].contains(lowercasedIdentifier) || identifier == ToolchainRegistry.defaultToolchainIdentifier {
return toolchainsByIdentifier[ToolchainRegistry.defaultToolchainIdentifier] ?? toolchainsByAlias[lowercasedIdentifier] ?? toolchainsByIdentifier.values.only
} else {
return toolchainsByIdentifier[identifier] ?? toolchainsByAlias[lowercasedIdentifier]
}
return toolchainsByIdentifier[identifier] ?? toolchainsByAlias[lowercasedIdentifier]
}
}

public var defaultToolchain: Toolchain? {
return self.lookup(ToolchainRegistry.defaultToolchainIdentifier)
return self.lookup("default")
}

public var toolchains: Set<Toolchain> {
Expand Down
Loading