From c22aa2d81d9ef11125b7d7ea4fdb9d86554e6f7e Mon Sep 17 00:00:00 2001 From: Jake Petroules Date: Tue, 15 Jul 2025 08:39:57 -0700 Subject: [PATCH] Make Android NDK discovery more robust, and add tests Add support for Android NDK versions 23 through 26, and emit an explicit error for older versions. Add test coverage to verify parsing of the metadata. I chose 23 as the cutoff for now simply because that's the next last time that the abis.json schema changed. That version was released in August 2021. We can add older versions if anyone really wants them. --- Sources/SWBAndroidPlatform/AndroidSDK.swift | 231 +++++++---- Sources/SWBAndroidPlatform/Plugin.swift | 11 +- .../AndroidSDKTests.swift | 361 ++++++++++++++++++ 3 files changed, 523 insertions(+), 80 deletions(-) create mode 100644 Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift diff --git a/Sources/SWBAndroidPlatform/AndroidSDK.swift b/Sources/SWBAndroidPlatform/AndroidSDK.swift index 44b93114..cb66619d 100644 --- a/Sources/SWBAndroidPlatform/AndroidSDK.swift +++ b/Sources/SWBAndroidPlatform/AndroidSDK.swift @@ -10,30 +10,51 @@ // //===----------------------------------------------------------------------===// -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 @@ -41,87 +62,145 @@ struct AndroidSDK: Sendable { } 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 #/(?.+)-(?.+)-(?.+)-(?.+)/#.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 #/(?.+)-(?.+)-(?.+)-(?.+)/#.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) + } } } diff --git a/Sources/SWBAndroidPlatform/Plugin.swift b/Sources/SWBAndroidPlatform/Plugin.swift index 8b6c8d5a..88fa5f76 100644 --- a/Sources/SWBAndroidPlatform/Plugin.swift +++ b/Sources/SWBAndroidPlatform/Plugin.swift @@ -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: @@ -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 @@ -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"), @@ -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 [] } diff --git a/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift b/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift new file mode 100644 index 00000000..37329d94 --- /dev/null +++ b/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift @@ -0,0 +1,361 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation +@_spi(Testing) import SWBAndroidPlatform +import SWBTestSupport +import SWBUtil +import Testing + +@Suite +fileprivate struct AndroidSDKTests { + @Test func findInstallations() async throws { + let host = try ProcessInfo.processInfo.hostOperatingSystem() + let installations = try await AndroidSDK.findInstallations(host: host, fs: localFS) + // It's OK if `installations` is an empty set, the host system might have no Android SDK/NDK installed + for installation in installations { + #expect(installation.host == host) + #expect(installation.latestNDK == installation.ndks.last) + } + } + + @Test func abis_r22() async throws { + try await withNDKVersion(version: Version("22.1.7171670")) { host, fs, ndkVersionPath in + let error = #expect(throws: StubError.self) { + try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs) + } + #expect(error?.description == "Android NDK version at path '\(ndkVersionPath.str)' is not supported (r23 or later required)") + } + } + + @Test func abis_r26_3() async throws { + try await withNDKVersion(version: Version("26.3.11579264")) { host, fs, ndkVersionPath in + try await fs.writeFileContents(ndkVersionPath.join("meta").join("abis.json")) { contents in + contents <<< + """ + { + "armeabi-v7a": { + "bitness": 32, + "default": true, + "deprecated": false, + "proc": "armv7-a", + "arch": "arm", + "triple": "arm-linux-androideabi", + "llvm_triple": "armv7-none-linux-androideabi" + }, + "arm64-v8a": { + "bitness": 64, + "default": true, + "deprecated": false, + "proc": "aarch64", + "arch": "arm64", + "triple": "aarch64-linux-android", + "llvm_triple": "aarch64-none-linux-android" + }, + "x86": { + "bitness": 32, + "default": true, + "deprecated": false, + "proc": "i686", + "arch": "x86", + "triple": "i686-linux-android", + "llvm_triple": "i686-none-linux-android" + }, + "x86_64": { + "bitness": 64, + "default": true, + "deprecated": false, + "proc": "x86_64", + "arch": "x86_64", + "triple": "x86_64-linux-android", + "llvm_triple": "x86_64-none-linux-android" + } + } + """ + } + + try await fs.writeFileContents(ndkVersionPath.join("meta").join("platforms.json")) { contents in + contents <<< + """ + { + "min": 21, + "max": 34, + "aliases": { + "20": 19, + "25": 24, + "J": 16, + "J-MR1": 17, + "J-MR2": 18, + "K": 19, + "L": 21, + "L-MR1": 22, + "M": 23, + "N": 24, + "N-MR1": 24, + "O": 26, + "O-MR1": 27, + "P": 28, + "Q": 29, + "R": 30, + "S": 31, + "Sv2": 32, + "Tiramisu": 33, + "UpsideDownCake": 34 + } + } + """ + } + + let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs) + let installation = try #require(installations.only) + #expect(installation.host == host) + #expect(installation.path == ndkVersionPath) + #expect(try installation.version == Version("26.3.11579264")) + #expect(installation.deploymentTargetRange.min == 21) + #expect(installation.deploymentTargetRange.max == 34) + + #expect(installation.abis.count == 4) + + let armv7 = try #require(installation.abis["armeabi-v7a"]) + #expect(armv7.bitness == .bits32) + #expect(armv7.default == true) + #expect(armv7.deprecated == false) + #expect(armv7.proc == "armv7-a") + #expect(armv7.arch == "arm") + #expect(armv7.triple == "arm-linux-androideabi") + #expect(armv7.llvm_triple.arch == "armv7") + #expect(armv7.llvm_triple.vendor == "none") + #expect(armv7.llvm_triple.system == "linux") + #expect(armv7.llvm_triple.environment == "androideabi") + #expect(armv7.min_os_version == 21) + + let arm64 = try #require(installation.abis["arm64-v8a"]) + #expect(arm64.bitness == .bits64) + #expect(arm64.default == true) + #expect(arm64.deprecated == false) + #expect(arm64.proc == "aarch64") + #expect(arm64.arch == "arm64") + #expect(arm64.triple == "aarch64-linux-android") + #expect(arm64.llvm_triple.arch == "aarch64") + #expect(arm64.llvm_triple.vendor == "none") + #expect(arm64.llvm_triple.system == "linux") + #expect(arm64.llvm_triple.environment == "android") + #expect(arm64.min_os_version == 21) + + let x86 = try #require(installation.abis["x86"]) + #expect(x86.bitness == .bits32) + #expect(x86.default == true) + #expect(x86.deprecated == false) + #expect(x86.proc == "i686") + #expect(x86.arch == "x86") + #expect(x86.triple == "i686-linux-android") + #expect(x86.llvm_triple.arch == "i686") + #expect(x86.llvm_triple.vendor == "none") + #expect(x86.llvm_triple.system == "linux") + #expect(x86.llvm_triple.environment == "android") + #expect(x86.min_os_version == 21) + + let x86_64 = try #require(installation.abis["x86_64"]) + #expect(x86_64.bitness == .bits64) + #expect(x86_64.default == true) + #expect(x86_64.deprecated == false) + #expect(x86_64.proc == "x86_64") + #expect(x86_64.arch == "x86_64") + #expect(x86_64.triple == "x86_64-linux-android") + #expect(x86_64.llvm_triple.arch == "x86_64") + #expect(x86_64.llvm_triple.vendor == "none") + #expect(x86_64.llvm_triple.system == "linux") + #expect(x86_64.llvm_triple.environment == "android") + #expect(x86_64.min_os_version == 21) + } + } + + @Test func abis_r27() async throws { + try await withNDKVersion(version: Version("27.0.11718014")) { host, fs, ndkVersionPath in + try await fs.writeFileContents(ndkVersionPath.join("meta").join("abis.json")) { contents in + contents <<< + """ + { + "armeabi-v7a": { + "bitness": 32, + "default": true, + "deprecated": false, + "proc": "armv7-a", + "arch": "arm", + "triple": "arm-linux-androideabi", + "llvm_triple": "armv7-none-linux-androideabi", + "min_os_version": 21 + }, + "arm64-v8a": { + "bitness": 64, + "default": true, + "deprecated": false, + "proc": "aarch64", + "arch": "arm64", + "triple": "aarch64-linux-android", + "llvm_triple": "aarch64-none-linux-android", + "min_os_version": 21 + }, + "riscv64": { + "bitness": 64, + "default": false, + "deprecated": false, + "proc": "riscv64", + "arch": "riscv64", + "triple": "riscv64-linux-android", + "llvm_triple": "riscv64-none-linux-android", + "min_os_version": 35 + }, + "x86": { + "bitness": 32, + "default": true, + "deprecated": false, + "proc": "i686", + "arch": "x86", + "triple": "i686-linux-android", + "llvm_triple": "i686-none-linux-android", + "min_os_version": 21 + }, + "x86_64": { + "bitness": 64, + "default": true, + "deprecated": false, + "proc": "x86_64", + "arch": "x86_64", + "triple": "x86_64-linux-android", + "llvm_triple": "x86_64-none-linux-android", + "min_os_version": 21 + } + } + """ + } + + try await fs.writeFileContents(ndkVersionPath.join("meta").join("platforms.json")) { contents in + contents <<< + """ + { + "min": 21, + "max": 35, + "aliases": { + "20": 19, + "25": 24, + "J": 16, + "J-MR1": 17, + "J-MR2": 18, + "K": 19, + "L": 21, + "L-MR1": 22, + "M": 23, + "N": 24, + "N-MR1": 24, + "O": 26, + "O-MR1": 27, + "P": 28, + "Q": 29, + "R": 30, + "S": 31, + "Sv2": 32, + "Tiramisu": 33, + "UpsideDownCake": 34, + "VanillaIceCream": 35 + } + } + """ + } + + let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs) + let installation = try #require(installations.only) + #expect(installation.host == host) + #expect(installation.path == ndkVersionPath) + #expect(try installation.version == Version("27.0.11718014")) + #expect(installation.deploymentTargetRange.min == 21) + #expect(installation.deploymentTargetRange.max == 35) + + #expect(installation.abis.count == 5) + + let armv7 = try #require(installation.abis["armeabi-v7a"]) + #expect(armv7.bitness == .bits32) + #expect(armv7.default == true) + #expect(armv7.deprecated == false) + #expect(armv7.proc == "armv7-a") + #expect(armv7.arch == "arm") + #expect(armv7.triple == "arm-linux-androideabi") + #expect(armv7.llvm_triple.arch == "armv7") + #expect(armv7.llvm_triple.vendor == "none") + #expect(armv7.llvm_triple.system == "linux") + #expect(armv7.llvm_triple.environment == "androideabi") + #expect(armv7.min_os_version == 21) + + let arm64 = try #require(installation.abis["arm64-v8a"]) + #expect(arm64.bitness == .bits64) + #expect(arm64.default == true) + #expect(arm64.deprecated == false) + #expect(arm64.proc == "aarch64") + #expect(arm64.arch == "arm64") + #expect(arm64.triple == "aarch64-linux-android") + #expect(arm64.llvm_triple.arch == "aarch64") + #expect(arm64.llvm_triple.vendor == "none") + #expect(arm64.llvm_triple.system == "linux") + #expect(arm64.llvm_triple.environment == "android") + #expect(arm64.min_os_version == 21) + + let riscv64 = try #require(installation.abis["riscv64"]) + #expect(riscv64.bitness == .bits64) + #expect(riscv64.default == false) + #expect(riscv64.deprecated == false) + #expect(riscv64.proc == "riscv64") + #expect(riscv64.arch == "riscv64") + #expect(riscv64.triple == "riscv64-linux-android") + #expect(riscv64.llvm_triple.arch == "riscv64") + #expect(riscv64.llvm_triple.vendor == "none") + #expect(riscv64.llvm_triple.system == "linux") + #expect(riscv64.llvm_triple.environment == "android") + #expect(riscv64.min_os_version == 35) + + let x86 = try #require(installation.abis["x86"]) + #expect(x86.bitness == .bits32) + #expect(x86.default == true) + #expect(x86.deprecated == false) + #expect(x86.proc == "i686") + #expect(x86.arch == "x86") + #expect(x86.triple == "i686-linux-android") + #expect(x86.llvm_triple.arch == "i686") + #expect(x86.llvm_triple.vendor == "none") + #expect(x86.llvm_triple.system == "linux") + #expect(x86.llvm_triple.environment == "android") + #expect(x86.min_os_version == 21) + + let x86_64 = try #require(installation.abis["x86_64"]) + #expect(x86_64.bitness == .bits64) + #expect(x86_64.default == true) + #expect(x86_64.deprecated == false) + #expect(x86_64.proc == "x86_64") + #expect(x86_64.arch == "x86_64") + #expect(x86_64.triple == "x86_64-linux-android") + #expect(x86_64.llvm_triple.arch == "x86_64") + #expect(x86_64.llvm_triple.vendor == "none") + #expect(x86_64.llvm_triple.system == "linux") + #expect(x86_64.llvm_triple.environment == "android") + #expect(x86_64.min_os_version == 21) + } + } + + private func withNDKVersion(version: Version, _ block: (OperatingSystem, any FSProxy, Path) async throws -> ()) async throws { + let fs = PseudoFS() + let ndkPath = Path.root.join("ndk") + let ndkVersionPath = ndkPath.join(version.description) + try fs.createDirectory(ndkPath, recursive: true) + try fs.createDirectory(ndkVersionPath.join("meta"), recursive: true) + let host = try ProcessInfo.processInfo.hostOperatingSystem() + try await block(host, fs, ndkVersionPath) + } +}