|
10 | 10 | //
|
11 | 11 | //===----------------------------------------------------------------------===//
|
12 | 12 |
|
13 |
| -import SWBUtil |
14 |
| -import Foundation |
| 13 | +public import SWBUtil |
| 14 | +public import Foundation |
15 | 15 |
|
16 |
| -struct AndroidSDK: Sendable { |
| 16 | +@_spi(Testing) public struct AndroidSDK: Sendable { |
17 | 17 | public let host: OperatingSystem
|
18 | 18 | public let path: Path
|
19 |
| - public let ndkVersion: Version? |
| 19 | + |
| 20 | + /// List of NDKs available in this SDK installation, sorted by version number from oldest to newest. |
| 21 | + @_spi(Testing) public let ndks: [NDK] |
| 22 | + |
| 23 | + public var latestNDK: NDK? { |
| 24 | + ndks.last |
| 25 | + } |
20 | 26 |
|
21 | 27 | init(host: OperatingSystem, path: Path, fs: any FSProxy) throws {
|
22 | 28 | self.host = host
|
23 | 29 | self.path = path
|
| 30 | + self.ndks = try NDK.findInstallations(host: host, sdkPath: path, fs: fs) |
| 31 | + } |
24 | 32 |
|
25 |
| - let ndkBasePath = path.join("ndk") |
26 |
| - if fs.exists(ndkBasePath) { |
27 |
| - self.ndkVersion = try fs.listdir(ndkBasePath).map { try Version($0) }.max() |
28 |
| - } else { |
29 |
| - self.ndkVersion = nil |
30 |
| - } |
| 33 | + @_spi(Testing) public struct NDK: Equatable, Sendable { |
| 34 | + public static let minimumNDKVersion = Version(23) |
| 35 | + |
| 36 | + public let host: OperatingSystem |
| 37 | + public let path: Path |
| 38 | + public let version: Version |
| 39 | + public let abis: [String: ABI] |
| 40 | + public let deploymentTargetRange: DeploymentTargetRange |
| 41 | + |
| 42 | + init(host: OperatingSystem, path ndkPath: Path, version: Version, fs: any FSProxy) throws { |
| 43 | + self.host = host |
| 44 | + self.path = ndkPath |
| 45 | + self.version = version |
31 | 46 |
|
32 |
| - if let ndkVersion { |
33 |
| - let ndkPath = ndkBasePath.join(ndkVersion.description) |
34 | 47 | let metaPath = ndkPath.join("meta")
|
35 | 48 |
|
36 |
| - self.abis = try JSONDecoder().decode([String: ABI].self, from: Data(fs.read(metaPath.join("abis.json")))) |
| 49 | + guard #available(macOS 14, *) else { |
| 50 | + throw StubError.error("Unsupported macOS version") |
| 51 | + } |
| 52 | + |
| 53 | + if version < Self.minimumNDKVersion { |
| 54 | + throw StubError.error("Android NDK version at path '\(ndkPath.str)' is not supported (r\(Self.minimumNDKVersion.description) or later required)") |
| 55 | + } |
| 56 | + |
| 57 | + self.abis = try JSONDecoder().decode(ABIs.self, from: Data(fs.read(metaPath.join("abis.json"))), configuration: version).abis |
37 | 58 |
|
38 | 59 | struct PlatformsInfo: Codable {
|
39 | 60 | let min: Int
|
40 | 61 | let max: Int
|
41 | 62 | }
|
42 | 63 |
|
43 | 64 | let platformsInfo = try JSONDecoder().decode(PlatformsInfo.self, from: Data(fs.read(metaPath.join("platforms.json"))))
|
44 |
| - self.ndkPath = ndkPath |
45 |
| - deploymentTargetRange = (platformsInfo.min, platformsInfo.max) |
46 |
| - } else { |
47 |
| - ndkPath = nil |
48 |
| - deploymentTargetRange = nil |
49 |
| - abis = nil |
| 65 | + deploymentTargetRange = DeploymentTargetRange(min: platformsInfo.min, max: platformsInfo.max) |
50 | 66 | }
|
51 |
| - } |
52 | 67 |
|
53 |
| - struct ABI: Codable { |
54 |
| - enum Bitness: Int, Codable { |
55 |
| - case bits32 = 32 |
56 |
| - case bits64 = 64 |
| 68 | + struct ABIs: DecodableWithConfiguration { |
| 69 | + let abis: [String: ABI] |
| 70 | + |
| 71 | + init(from decoder: any Decoder, configuration: Version) throws { |
| 72 | + struct DynamicCodingKey: CodingKey { |
| 73 | + var stringValue: String |
| 74 | + |
| 75 | + init?(stringValue: String) { |
| 76 | + self.stringValue = stringValue |
| 77 | + } |
| 78 | + |
| 79 | + let intValue: Int? = nil |
| 80 | + |
| 81 | + init?(intValue: Int) { |
| 82 | + nil |
| 83 | + } |
| 84 | + } |
| 85 | + let container = try decoder.container(keyedBy: DynamicCodingKey.self) |
| 86 | + abis = try Dictionary(uniqueKeysWithValues: container.allKeys.map { try ($0.stringValue, container.decode(ABI.self, forKey: $0, configuration: configuration)) }) |
| 87 | + } |
57 | 88 | }
|
58 | 89 |
|
59 |
| - struct LLVMTriple: Codable { |
60 |
| - var arch: String |
61 |
| - var vendor: String |
62 |
| - var system: String |
63 |
| - var environment: String |
64 |
| - |
65 |
| - var description: String { |
66 |
| - "\(arch)-\(vendor)-\(system)-\(environment)" |
| 90 | + @_spi(Testing) public struct ABI: DecodableWithConfiguration, Equatable, Sendable { |
| 91 | + @_spi(Testing) public enum Bitness: Int, Codable, Equatable, Sendable { |
| 92 | + case bits32 = 32 |
| 93 | + case bits64 = 64 |
67 | 94 | }
|
68 | 95 |
|
69 |
| - init(from decoder: any Decoder) throws { |
70 |
| - let container = try decoder.singleValueContainer() |
71 |
| - let triple = try container.decode(String.self) |
72 |
| - if let match = try #/(?<arch>.+)-(?<vendor>.+)-(?<system>.+)-(?<environment>.+)/#.wholeMatch(in: triple) { |
73 |
| - self.arch = String(match.output.arch) |
74 |
| - self.vendor = String(match.output.vendor) |
75 |
| - self.system = String(match.output.system) |
76 |
| - self.environment = String(match.output.environment) |
77 |
| - } else { |
78 |
| - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid triple string: \(triple)") |
| 96 | + @_spi(Testing) public struct LLVMTriple: Codable, Equatable, Sendable { |
| 97 | + public var arch: String |
| 98 | + public var vendor: String |
| 99 | + public var system: String |
| 100 | + public var environment: String |
| 101 | + |
| 102 | + var description: String { |
| 103 | + "\(arch)-\(vendor)-\(system)-\(environment)" |
| 104 | + } |
| 105 | + |
| 106 | + public init(from decoder: any Decoder) throws { |
| 107 | + let container = try decoder.singleValueContainer() |
| 108 | + let triple = try container.decode(String.self) |
| 109 | + if let match = try #/(?<arch>.+)-(?<vendor>.+)-(?<system>.+)-(?<environment>.+)/#.wholeMatch(in: triple) { |
| 110 | + self.arch = String(match.output.arch) |
| 111 | + self.vendor = String(match.output.vendor) |
| 112 | + self.system = String(match.output.system) |
| 113 | + self.environment = String(match.output.environment) |
| 114 | + } else { |
| 115 | + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid triple string: \(triple)") |
| 116 | + } |
79 | 117 | }
|
80 | 118 | }
|
81 | 119 |
|
82 |
| - func encode(to encoder: any Encoder) throws { |
83 |
| - var container = encoder.singleValueContainer() |
84 |
| - try container.encode(description) |
| 120 | + public let bitness: Bitness |
| 121 | + public let `default`: Bool |
| 122 | + public let deprecated: Bool |
| 123 | + public let proc: String |
| 124 | + public let arch: String |
| 125 | + public let triple: String |
| 126 | + public let llvm_triple: LLVMTriple |
| 127 | + public let min_os_version: Int |
| 128 | + |
| 129 | + enum CodingKeys: String, CodingKey { |
| 130 | + case bitness |
| 131 | + case `default` = "default" |
| 132 | + case deprecated |
| 133 | + case proc |
| 134 | + case arch |
| 135 | + case triple |
| 136 | + case llvm_triple = "llvm_triple" |
| 137 | + case min_os_version = "min_os_version" |
| 138 | + } |
| 139 | + |
| 140 | + public init(from decoder: any Decoder, configuration ndkVersion: Version) throws { |
| 141 | + let container = try decoder.container(keyedBy: CodingKeys.self) |
| 142 | + self.bitness = try container.decode(Bitness.self, forKey: .bitness) |
| 143 | + self.default = try container.decode(Bool.self, forKey: .default) |
| 144 | + self.deprecated = try container.decode(Bool.self, forKey: .deprecated) |
| 145 | + self.proc = try container.decode(String.self, forKey: .proc) |
| 146 | + self.arch = try container.decode(String.self, forKey: .arch) |
| 147 | + self.triple = try container.decode(String.self, forKey: .triple) |
| 148 | + self.llvm_triple = try container.decode(LLVMTriple.self, forKey: .llvm_triple) |
| 149 | + self.min_os_version = try container.decodeIfPresent(Int.self, forKey: .min_os_version) ?? { |
| 150 | + if ndkVersion < Version(27) { |
| 151 | + return 21 // min_os_version wasn't present prior to NDKr27, fill it in with 21, which is the appropriate value |
| 152 | + } else { |
| 153 | + throw DecodingError.valueNotFound(Int.self, .init(codingPath: container.codingPath, debugDescription: "No value associated with key \(CodingKeys.min_os_version) (\"\(CodingKeys.min_os_version.rawValue)\").")) |
| 154 | + } |
| 155 | + }() |
85 | 156 | }
|
86 | 157 | }
|
87 | 158 |
|
88 |
| - let bitness: Bitness |
89 |
| - let `default`: Bool |
90 |
| - let deprecated: Bool |
91 |
| - let proc: String |
92 |
| - let arch: String |
93 |
| - let triple: String |
94 |
| - let llvm_triple: LLVMTriple |
95 |
| - let min_os_version: Int |
96 |
| - } |
| 159 | + @_spi(Testing) public struct DeploymentTargetRange: Equatable, Sendable { |
| 160 | + public let min: Int |
| 161 | + public let max: Int |
| 162 | + } |
97 | 163 |
|
98 |
| - public let abis: [String: ABI]? |
| 164 | + public var toolchainPath: Path { |
| 165 | + path.join("toolchains").join("llvm").join("prebuilt").join(hostTag) |
| 166 | + } |
99 | 167 |
|
100 |
| - public let deploymentTargetRange: (min: Int, max: Int)? |
| 168 | + public var sysroot: Path { |
| 169 | + toolchainPath.join("sysroot") |
| 170 | + } |
101 | 171 |
|
102 |
| - public let ndkPath: Path? |
| 172 | + private var hostTag: String? { |
| 173 | + switch host { |
| 174 | + case .windows: |
| 175 | + // Also works on Windows on ARM via Prism binary translation. |
| 176 | + "windows-x86_64" |
| 177 | + case .macOS: |
| 178 | + // Despite the x86_64 tag in the Darwin name, these are universal binaries including arm64. |
| 179 | + "darwin-x86_64" |
| 180 | + case .linux: |
| 181 | + // Also works on non-x86 archs via binfmt support and qemu (or Rosetta on Apple-hosted VMs). |
| 182 | + "linux-x86_64" |
| 183 | + default: |
| 184 | + nil // unsupported host |
| 185 | + } |
| 186 | + } |
103 | 187 |
|
104 |
| - public var toolchainPath: Path? { |
105 |
| - ndkPath?.join("toolchains").join("llvm").join("prebuilt").join(hostTag) |
106 |
| - } |
| 188 | + public static func findInstallations(host: OperatingSystem, sdkPath: Path, fs: any FSProxy) throws -> [NDK] { |
| 189 | + let ndkBasePath = sdkPath.join("ndk") |
| 190 | + guard fs.exists(ndkBasePath) else { |
| 191 | + return [] |
| 192 | + } |
107 | 193 |
|
108 |
| - public var sysroot: Path? { |
109 |
| - toolchainPath?.join("sysroot") |
110 |
| - } |
| 194 | + let ndks = try fs.listdir(ndkBasePath).map({ try Version($0) }).sorted() |
| 195 | + let supportedNdks = ndks.filter { $0 >= minimumNDKVersion } |
111 | 196 |
|
112 |
| - private var hostTag: String? { |
113 |
| - switch host { |
114 |
| - case .windows: |
115 |
| - // Also works on Windows on ARM via Prism binary translation. |
116 |
| - "windows-x86_64" |
117 |
| - case .macOS: |
118 |
| - // Despite the x86_64 tag in the Darwin name, these are universal binaries including arm64. |
119 |
| - "darwin-x86_64" |
120 |
| - case .linux: |
121 |
| - // Also works on non-x86 archs via binfmt support and qemu (or Rosetta on Apple-hosted VMs). |
122 |
| - "linux-x86_64" |
123 |
| - default: |
124 |
| - nil // unsupported host |
| 197 | + // 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. |
| 198 | + let discoveredNdks = supportedNdks.isEmpty && !ndks.isEmpty ? ndks : supportedNdks |
| 199 | + |
| 200 | + return try discoveredNdks.map { ndkVersion in |
| 201 | + let ndkPath = ndkBasePath.join(ndkVersion.description) |
| 202 | + return try NDK(host: host, path: ndkPath, version: ndkVersion, fs: fs) |
| 203 | + } |
125 | 204 | }
|
126 | 205 | }
|
127 | 206 |
|
|
0 commit comments