@@ -15,43 +15,54 @@ public import Foundation
15
15
16
16
@_spi ( Testing) public struct AndroidSDK : Sendable {
17
17
public let host : OperatingSystem
18
- public let path : Path
18
+ public let path : AbsolutePath
19
+ private let ndkInstallations : NDK . Installations
19
20
20
21
/// List of NDKs available in this SDK installation, sorted by version number from oldest to newest.
21
- @_spi ( Testing) public let ndks : [ NDK ]
22
+ @_spi ( Testing) public var ndks : [ NDK ] {
23
+ ndkInstallations. ndks
24
+ }
22
25
23
- public var latestNDK : NDK ? {
24
- ndks. last
26
+ public var preferredNDK : NDK ? {
27
+ ndkInstallations . preferredNDK ?? ndks. last
25
28
}
26
29
27
- init ( host: OperatingSystem , path: Path , fs: any FSProxy ) throws {
30
+ init ( host: OperatingSystem , path: AbsolutePath , fs: any FSProxy ) throws {
28
31
self . host = host
29
32
self . path = path
30
- self . ndks = try NDK . findInstallations ( host: host, sdkPath: path, fs: fs)
33
+ self . ndkInstallations = try NDK . findInstallations ( host: host, sdkPath: path, fs: fs)
31
34
}
32
35
33
36
@_spi ( Testing) public struct NDK : Equatable , Sendable {
34
37
public static let minimumNDKVersion = Version ( 23 )
35
38
36
39
public let host : OperatingSystem
37
- public let path : Path
40
+ public let path : AbsolutePath
38
41
public let version : Version
39
42
public let abis : [ String : ABI ]
40
43
public let deploymentTargetRange : DeploymentTargetRange
41
44
42
- init ( host: OperatingSystem , path ndkPath: Path , version : Version , fs: any FSProxy ) throws {
45
+ @ _spi ( Testing ) public init ( host: OperatingSystem , path ndkPath: AbsolutePath , fs: any FSProxy ) throws {
43
46
self . host = host
44
47
self . path = ndkPath
45
- self . version = version
48
+ self . toolchainPath = try AbsolutePath ( validating: path. path. join ( " toolchains " ) . join ( " llvm " ) . join ( " prebuilt " ) . join ( Self . hostTag ( host) ) )
49
+ self . sysroot = try AbsolutePath ( validating: toolchainPath. path. join ( " sysroot " ) )
50
+
51
+ let propertiesFile = ndkPath. path. join ( " source.properties " )
52
+ guard fs. exists ( propertiesFile) else {
53
+ throw Error . notAnNDK ( ndkPath)
54
+ }
46
55
47
- let metaPath = ndkPath. join ( " meta " )
56
+ self . version = try NDK . Properties ( data: Data ( fs. read ( propertiesFile) ) ) . revision
57
+
58
+ let metaPath = ndkPath. path. join ( " meta " )
48
59
49
60
guard #available( macOS 14 , * ) else {
50
61
throw StubError . error ( " Unsupported macOS version " )
51
62
}
52
63
53
64
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) " )
65
+ throw Error . unsupportedVersion ( path: ndkPath, minimumVersion : Self . minimumNDKVersion)
55
66
}
56
67
57
68
self . abis = try JSONDecoder ( ) . decode ( ABIs . self, from: Data ( fs. read ( metaPath. join ( " abis.json " ) ) ) , configuration: version) . abis
@@ -65,6 +76,36 @@ public import Foundation
65
76
deploymentTargetRange = DeploymentTargetRange ( min: platformsInfo. min, max: platformsInfo. max)
66
77
}
67
78
79
+ public enum Error : Swift . Error , CustomStringConvertible , Sendable {
80
+ case notAnNDK( AbsolutePath )
81
+ case unsupportedVersion( path: AbsolutePath , minimumVersion: Version )
82
+ case noSupportedVersions( minimumVersion: Version )
83
+
84
+ public var description : String {
85
+ switch self {
86
+ case let . notAnNDK( path) :
87
+ " Package at path ' \( path. path. str) ' is not an Android NDK (no source.properties file) "
88
+ case let . unsupportedVersion( path, minimumVersion) :
89
+ " Android NDK version at path ' \( path. path. str) ' is not supported (r \( minimumVersion. description) or later required) "
90
+ case let . noSupportedVersions( minimumVersion) :
91
+ " All installed NDK versions are not supported (r \( minimumVersion. description) or later required) "
92
+ }
93
+ }
94
+ }
95
+
96
+ struct Properties {
97
+ let properties : JavaProperties
98
+ let revision : Version
99
+
100
+ init ( data: Data ) throws {
101
+ properties = try . init( data: data)
102
+ guard properties [ " Pkg.Desc " ] == " Android NDK " else {
103
+ throw StubError . error ( " Package is not an Android NDK " )
104
+ }
105
+ revision = try Version ( properties [ " Pkg.BaseRevision " ] ?? properties [ " Pkg.Revision " ] ?? " " )
106
+ }
107
+ }
108
+
68
109
struct ABIs : DecodableWithConfiguration {
69
110
let abis : [ String : ABI ]
70
111
@@ -161,15 +202,10 @@ public import Foundation
161
202
public let max : Int
162
203
}
163
204
164
- public var toolchainPath : Path {
165
- path. join ( " toolchains " ) . join ( " llvm " ) . join ( " prebuilt " ) . join ( hostTag)
166
- }
167
-
168
- public var sysroot : Path {
169
- toolchainPath. join ( " sysroot " )
170
- }
205
+ public let toolchainPath : AbsolutePath
206
+ public let sysroot : AbsolutePath
171
207
172
- private var hostTag : String ? {
208
+ private static func hostTag( _ host : OperatingSystem ) -> String ? {
173
209
switch host {
174
210
case . windows:
175
211
// Also works on Windows on ARM via Prism binary translation.
@@ -185,44 +221,119 @@ public import Foundation
185
221
}
186
222
}
187
223
188
- public static func findInstallations( host: OperatingSystem , sdkPath: Path , fs: any FSProxy ) throws -> [ NDK ] {
189
- let ndkBasePath = sdkPath. join ( " ndk " )
224
+ public struct Installations : Sendable {
225
+ private let preferredIndex : Int ?
226
+ public let ndks : [ NDK ]
227
+
228
+ init ( preferredIndex: Int ? = nil , ndks: [ NDK ] ) {
229
+ self . preferredIndex = preferredIndex
230
+ self . ndks = ndks
231
+ }
232
+
233
+ public var preferredNDK : NDK ? {
234
+ preferredIndex. map { ndks [ $0] } ?? ndks. only
235
+ }
236
+ }
237
+
238
+ public static func findInstallations( host: OperatingSystem , sdkPath: AbsolutePath , fs: any FSProxy ) throws -> Installations {
239
+ if let overridePath = NDK . environmentOverrideLocation {
240
+ return try Installations ( ndks: [ NDK ( host: host, path: overridePath, fs: fs) ] )
241
+ }
242
+
243
+ let ndkBasePath = sdkPath. path. join ( " ndk " )
190
244
guard fs. exists ( ndkBasePath) else {
191
- return [ ]
245
+ return Installations ( ndks : [ ] )
192
246
}
193
247
194
- let ndks = try fs. listdir ( ndkBasePath) . map ( { try Version ( $0) } ) . sorted ( )
195
- let supportedNdks = ndks. filter { $0 >= minimumNDKVersion }
248
+ var hadUnsupportedVersions : Bool = false
249
+ let ndks = try fs. listdir ( ndkBasePath) . compactMap ( { subdir in
250
+ do {
251
+ return try NDK ( host: host, path: AbsolutePath ( validating: ndkBasePath. join ( subdir) ) , fs: fs)
252
+ } catch Error . notAnNDK( _) {
253
+ return nil
254
+ } catch Error . unsupportedVersion( _, _) {
255
+ hadUnsupportedVersions = true
256
+ return nil
257
+ }
258
+ } ) . sorted ( by: \. version)
196
259
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
260
+ // If we have some NDKs but all of them are unsupported, provide a more useful error. Otherwise, simply filter out and ignore the unsupported versions.
261
+ if ndks. isEmpty && hadUnsupportedVersions {
262
+ throw Error . noSupportedVersions ( minimumVersion: Self . minimumNDKVersion)
263
+ }
199
264
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)
265
+ // Respect Debian alternatives
266
+ let preferredIndex : Int ?
267
+ if sdkPath == AndroidSDK . defaultDebianLocation, let ndkLinkPath = AndroidSDK . NDK. defaultDebianLocation {
268
+ preferredIndex = try ndks. firstIndex ( where: { try $0. path. path == fs. realpath ( ndkLinkPath. path) } )
269
+ } else {
270
+ preferredIndex = nil
203
271
}
272
+
273
+ return Installations ( preferredIndex: preferredIndex, ndks: ndks)
204
274
}
205
275
}
206
276
207
277
public static func findInstallations( host: OperatingSystem , fs: any FSProxy ) async throws -> [ AndroidSDK ] {
208
- let defaultLocation : Path ? = switch host {
278
+ var paths : [ AbsolutePath ] = [ ]
279
+ if let path = AndroidSDK . environmentOverrideLocation {
280
+ paths. append ( path)
281
+ }
282
+ if let path = try AndroidSDK . defaultAndroidStudioLocation ( host: host) {
283
+ paths. append ( path)
284
+ }
285
+ if let path = AndroidSDK . defaultDebianLocation, host == . linux {
286
+ paths. append ( path)
287
+ }
288
+ return try paths. compactMap { path in
289
+ guard fs. exists ( path. path) else {
290
+ return nil
291
+ }
292
+ return try AndroidSDK ( host: host, path: path, fs: fs)
293
+ }
294
+ }
295
+ }
296
+
297
+ fileprivate extension AndroidSDK . NDK {
298
+ /// 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`).
299
+ /// - seealso: [Configuring NDK Path](https://github.com/android/ndk-samples/wiki/Configure-NDK-Path#terminologies)
300
+ static var environmentOverrideLocation : AbsolutePath ? {
301
+ ( getEnvironmentVariable ( " ANDROID_NDK_ROOT " ) ?? getEnvironmentVariable ( " ANDROID_NDK_HOME " ) ) ? . nilIfEmpty. map { AbsolutePath ( $0) } ?? nil
302
+ }
303
+
304
+ /// 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".
305
+ /// These packages are available in non-free / multiverse and multiple versions can be installed simultaneously.
306
+ static var defaultDebianLocation : AbsolutePath ? {
307
+ AbsolutePath ( " /usr/lib/android-ndk " )
308
+ }
309
+ }
310
+
311
+ fileprivate extension AndroidSDK {
312
+ /// The location of the Android SDK based on the `ANDROID_HOME` environment variable (falling back to the deprecated but well known `ANDROID_SDK_ROOT`).
313
+ /// - seealso: [Android environment variables](https://developer.android.com/tools/variables)
314
+ static var environmentOverrideLocation : AbsolutePath ? {
315
+ ( getEnvironmentVariable ( " ANDROID_HOME " ) ?? getEnvironmentVariable ( " ANDROID_SDK_ROOT " ) ) ? . nilIfEmpty. map { AbsolutePath ( $0) } ?? nil
316
+ }
317
+
318
+ static func defaultAndroidStudioLocation( host: OperatingSystem ) throws -> AbsolutePath ? {
319
+ switch host {
209
320
case . windows:
210
321
// %LOCALAPPDATA%\Android\Sdk
211
- try FileManager . default. url ( for: . applicationSupportDirectory, in: . userDomainMask, appropriateFor: nil , create: false ) . appendingPathComponent ( " Android " ) . appendingPathComponent ( " Sdk " ) . filePath
322
+ try FileManager . default. url ( for: . applicationSupportDirectory, in: . userDomainMask, appropriateFor: nil , create: false ) . appendingPathComponent ( " Android " ) . appendingPathComponent ( " Sdk " ) . absoluteFilePath
212
323
case . macOS:
213
324
// ~/Library/Android/sdk
214
- try FileManager . default. url ( for: . libraryDirectory, in: . userDomainMask, appropriateFor: nil , create: false ) . appendingPathComponent ( " Android " ) . appendingPathComponent ( " sdk " ) . filePath
325
+ try FileManager . default. url ( for: . libraryDirectory, in: . userDomainMask, appropriateFor: nil , create: false ) . appendingPathComponent ( " Android " ) . appendingPathComponent ( " sdk " ) . absoluteFilePath
215
326
case . linux:
216
327
// ~/Android/Sdk
217
- Path . homeDirectory. join ( " Android " ) . join ( " Sdk " )
328
+ try AbsolutePath ( validating : Path . homeDirectory. join ( " Android " ) . join ( " Sdk " ) )
218
329
default :
219
330
nil
220
331
}
332
+ }
221
333
222
- if let path = defaultLocation, fs. exists ( path) {
223
- return try [ AndroidSDK ( host: host, path: path, fs: fs) ]
224
- }
225
-
226
- return [ ]
334
+ /// Location of the Android SDK installed by the `google-*` family of packages available in Debian 13 "Trixie" and Ubuntu 24.04 "Noble".
335
+ /// These packages are available in non-free / multiverse and multiple versions can be installed simultaneously.
336
+ static var defaultDebianLocation : AbsolutePath ? {
337
+ AbsolutePath ( " /usr/lib/android-sdk " )
227
338
}
228
339
}
0 commit comments