Skip to content

Make Android NDK discovery more robust, and add tests #659

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
231 changes: 155 additions & 76 deletions Sources/SWBAndroidPlatform/AndroidSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,118 +10,197 @@
//
//===----------------------------------------------------------------------===//

import SWBUtil
import Foundation
public import SWBUtil
public import Foundation

struct AndroidSDK: Sendable {
@_spi(Testing) public struct AndroidSDK: Sendable {
public let host: OperatingSystem
public let path: Path
public let ndkVersion: Version?

/// List of NDKs available in this SDK installation, sorted by version number from oldest to newest.
@_spi(Testing) public let ndks: [NDK]

public var latestNDK: NDK? {
ndks.last
}

init(host: OperatingSystem, path: Path, fs: any FSProxy) throws {
self.host = host
self.path = path
self.ndks = try NDK.findInstallations(host: host, sdkPath: path, fs: fs)
}

let ndkBasePath = path.join("ndk")
if fs.exists(ndkBasePath) {
self.ndkVersion = try fs.listdir(ndkBasePath).map { try Version($0) }.max()
} else {
self.ndkVersion = nil
}
@_spi(Testing) public struct NDK: Equatable, Sendable {
public static let minimumNDKVersion = Version(23)

public let host: OperatingSystem
public let path: Path
public let version: Version
public let abis: [String: ABI]
public let deploymentTargetRange: DeploymentTargetRange

init(host: OperatingSystem, path ndkPath: Path, version: Version, fs: any FSProxy) throws {
self.host = host
self.path = ndkPath
self.version = version

if let ndkVersion {
let ndkPath = ndkBasePath.join(ndkVersion.description)
let metaPath = ndkPath.join("meta")

self.abis = try JSONDecoder().decode([String: ABI].self, from: Data(fs.read(metaPath.join("abis.json"))))
guard #available(macOS 14, *) else {
throw StubError.error("Unsupported macOS version")
}

if version < Self.minimumNDKVersion {
throw StubError.error("Android NDK version at path '\(ndkPath.str)' is not supported (r\(Self.minimumNDKVersion.description) or later required)")
}

self.abis = try JSONDecoder().decode(ABIs.self, from: Data(fs.read(metaPath.join("abis.json"))), configuration: version).abis

struct PlatformsInfo: Codable {
let min: Int
let max: Int
}

let platformsInfo = try JSONDecoder().decode(PlatformsInfo.self, from: Data(fs.read(metaPath.join("platforms.json"))))
self.ndkPath = ndkPath
deploymentTargetRange = (platformsInfo.min, platformsInfo.max)
} else {
ndkPath = nil
deploymentTargetRange = nil
abis = nil
deploymentTargetRange = DeploymentTargetRange(min: platformsInfo.min, max: platformsInfo.max)
}
}

struct ABI: Codable {
enum Bitness: Int, Codable {
case bits32 = 32
case bits64 = 64
struct ABIs: DecodableWithConfiguration {
let abis: [String: ABI]

init(from decoder: any Decoder, configuration: Version) throws {
struct DynamicCodingKey: CodingKey {
var stringValue: String

init?(stringValue: String) {
self.stringValue = stringValue
}

let intValue: Int? = nil

init?(intValue: Int) {
nil
}
}
let container = try decoder.container(keyedBy: DynamicCodingKey.self)
abis = try Dictionary(uniqueKeysWithValues: container.allKeys.map { try ($0.stringValue, container.decode(ABI.self, forKey: $0, configuration: configuration)) })
}
}

struct LLVMTriple: Codable {
var arch: String
var vendor: String
var system: String
var environment: String

var description: String {
"\(arch)-\(vendor)-\(system)-\(environment)"
@_spi(Testing) public struct ABI: DecodableWithConfiguration, Equatable, Sendable {
@_spi(Testing) public enum Bitness: Int, Codable, Equatable, Sendable {
case bits32 = 32
case bits64 = 64
}

init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let triple = try container.decode(String.self)
if let match = try #/(?<arch>.+)-(?<vendor>.+)-(?<system>.+)-(?<environment>.+)/#.wholeMatch(in: triple) {
self.arch = String(match.output.arch)
self.vendor = String(match.output.vendor)
self.system = String(match.output.system)
self.environment = String(match.output.environment)
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid triple string: \(triple)")
@_spi(Testing) public struct LLVMTriple: Codable, Equatable, Sendable {
public var arch: String
public var vendor: String
public var system: String
public var environment: String

var description: String {
"\(arch)-\(vendor)-\(system)-\(environment)"
}

public init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let triple = try container.decode(String.self)
if let match = try #/(?<arch>.+)-(?<vendor>.+)-(?<system>.+)-(?<environment>.+)/#.wholeMatch(in: triple) {
self.arch = String(match.output.arch)
self.vendor = String(match.output.vendor)
self.system = String(match.output.system)
self.environment = String(match.output.environment)
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid triple string: \(triple)")
}
}
}

func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(description)
public let bitness: Bitness
public let `default`: Bool
public let deprecated: Bool
public let proc: String
public let arch: String
public let triple: String
public let llvm_triple: LLVMTriple
public let min_os_version: Int

enum CodingKeys: String, CodingKey {
case bitness
case `default` = "default"
case deprecated
case proc
case arch
case triple
case llvm_triple = "llvm_triple"
case min_os_version = "min_os_version"
}

public init(from decoder: any Decoder, configuration ndkVersion: Version) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.bitness = try container.decode(Bitness.self, forKey: .bitness)
self.default = try container.decode(Bool.self, forKey: .default)
self.deprecated = try container.decode(Bool.self, forKey: .deprecated)
self.proc = try container.decode(String.self, forKey: .proc)
self.arch = try container.decode(String.self, forKey: .arch)
self.triple = try container.decode(String.self, forKey: .triple)
self.llvm_triple = try container.decode(LLVMTriple.self, forKey: .llvm_triple)
self.min_os_version = try container.decodeIfPresent(Int.self, forKey: .min_os_version) ?? {
if ndkVersion < Version(27) {
return 21 // min_os_version wasn't present prior to NDKr27, fill it in with 21, which is the appropriate value
} else {
throw DecodingError.valueNotFound(Int.self, .init(codingPath: container.codingPath, debugDescription: "No value associated with key \(CodingKeys.min_os_version) (\"\(CodingKeys.min_os_version.rawValue)\")."))
}
}()
}
}

let bitness: Bitness
let `default`: Bool
let deprecated: Bool
let proc: String
let arch: String
let triple: String
let llvm_triple: LLVMTriple
let min_os_version: Int
}
@_spi(Testing) public struct DeploymentTargetRange: Equatable, Sendable {
public let min: Int
public let max: Int
}

public let abis: [String: ABI]?
public var toolchainPath: Path {
path.join("toolchains").join("llvm").join("prebuilt").join(hostTag)
}

public let deploymentTargetRange: (min: Int, max: Int)?
public var sysroot: Path {
toolchainPath.join("sysroot")
}

public let ndkPath: Path?
private var hostTag: String? {
switch host {
case .windows:
// Also works on Windows on ARM via Prism binary translation.
"windows-x86_64"
case .macOS:
// Despite the x86_64 tag in the Darwin name, these are universal binaries including arm64.
"darwin-x86_64"
case .linux:
// Also works on non-x86 archs via binfmt support and qemu (or Rosetta on Apple-hosted VMs).
"linux-x86_64"
default:
nil // unsupported host
}
}

public var toolchainPath: Path? {
ndkPath?.join("toolchains").join("llvm").join("prebuilt").join(hostTag)
}
public static func findInstallations(host: OperatingSystem, sdkPath: Path, fs: any FSProxy) throws -> [NDK] {
let ndkBasePath = sdkPath.join("ndk")
guard fs.exists(ndkBasePath) else {
return []
}

public var sysroot: Path? {
toolchainPath?.join("sysroot")
}
let ndks = try fs.listdir(ndkBasePath).map({ try Version($0) }).sorted()
let supportedNdks = ndks.filter { $0 >= minimumNDKVersion }

private var hostTag: String? {
switch host {
case .windows:
// Also works on Windows on ARM via Prism binary translation.
"windows-x86_64"
case .macOS:
// Despite the x86_64 tag in the Darwin name, these are universal binaries including arm64.
"darwin-x86_64"
case .linux:
// Also works on non-x86 archs via binfmt support and qemu (or Rosetta on Apple-hosted VMs).
"linux-x86_64"
default:
nil // unsupported host
// If we have some NDKs but all of them are unsupported, try parsing them so that parsing fails and provides a more useful error. Otherwise, simply filter out and ignore the unsupported versions.
let discoveredNdks = supportedNdks.isEmpty && !ndks.isEmpty ? ndks : supportedNdks

return try discoveredNdks.map { ndkVersion in
let ndkPath = ndkBasePath.join(ndkVersion.description)
return try NDK(host: host, path: ndkPath, version: ndkVersion, fs: fs)
}
}
}

Expand Down
11 changes: 7 additions & 4 deletions Sources/SWBAndroidPlatform/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ struct AndroidEnvironmentExtension: EnvironmentExtension {
if let latest = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first {
return [
"ANDROID_SDK_ROOT": latest.path.str,
"ANDROID_NDK_ROOT": latest.ndkPath?.str,
"ANDROID_NDK_ROOT": latest.ndks.last?.path.str,
].compactMapValues { $0 }
}
default:
Expand Down Expand Up @@ -112,10 +112,13 @@ struct AndroidSDKRegistryExtension: SDKRegistryExtension {
return []
}

guard let abis = androidSdk.abis, let deploymentTargetRange = androidSdk.deploymentTargetRange else {
guard let androidNdk = androidSdk.latestNDK else {
return []
}

let abis = androidNdk.abis
let deploymentTargetRange = androidNdk.deploymentTargetRange

let allPossibleTriples = abis.values.flatMap { abi in
(max(deploymentTargetRange.min, abi.min_os_version)...deploymentTargetRange.max).map { deploymentTarget in
var triple = abi.llvm_triple
Expand Down Expand Up @@ -147,7 +150,7 @@ struct AndroidSDKRegistryExtension: SDKRegistryExtension {
swiftSettings = [:]
}

return [(androidSdk.sysroot ?? .root, androidPlatform, [
return [(androidNdk.sysroot, androidPlatform, [
"Type": .plString("SDK"),
"Version": .plString("0.0.0"),
"CanonicalName": .plString("android"),
Expand Down Expand Up @@ -184,7 +187,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?.toolchainPath else {
guard let toolchainPath = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first?.latestNDK?.toolchainPath else {
return []
}

Expand Down
Loading
Loading