@@ -16,18 +16,21 @@ public import Foundation
1616@_spi ( Testing) public struct AndroidSDK : Sendable {
1717 public let host : OperatingSystem
1818 public let path : Path
19+ private let ndkInstallations : NDK . Installations
1920
2021 /// 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+ }
2225
23- public var latestNDK : NDK ? {
24- ndks. last
26+ public var preferredNDK : NDK ? {
27+ ndkInstallations . preferredNDK ?? ndks. last
2528 }
2629
2730 init ( host: OperatingSystem , path: Path , fs: any FSProxy ) throws {
2831 self . host = host
2932 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)
3134 }
3235
3336 @_spi ( Testing) public struct NDK : Equatable , Sendable {
@@ -39,10 +42,16 @@ public import Foundation
3942 public let abis : [ String : ABI ]
4043 public let deploymentTargetRange : DeploymentTargetRange
4144
42- init ( host: OperatingSystem , path ndkPath: Path , version : Version , fs: any FSProxy ) throws {
45+ @ _spi ( Testing ) public init ( host: OperatingSystem , path ndkPath: Path , fs: any FSProxy ) throws {
4346 self . host = host
4447 self . path = ndkPath
45- self . version = version
48+
49+ let propertiesFile = ndkPath. join ( " source.properties " )
50+ guard fs. exists ( propertiesFile) else {
51+ throw Error . notAnNDK ( ndkPath)
52+ }
53+
54+ self . version = try NDK . Properties ( data: Data ( fs. read ( propertiesFile) ) ) . revision
4655
4756 let metaPath = ndkPath. join ( " meta " )
4857
@@ -51,7 +60,7 @@ public import Foundation
5160 }
5261
5362 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) " )
63+ throw Error . unsupportedVersion ( path: ndkPath, minimumVersion : Self . minimumNDKVersion)
5564 }
5665
5766 self . abis = try JSONDecoder ( ) . decode ( ABIs . self, from: Data ( fs. read ( metaPath. join ( " abis.json " ) ) ) , configuration: version) . abis
@@ -65,6 +74,36 @@ public import Foundation
6574 deploymentTargetRange = DeploymentTargetRange ( min: platformsInfo. min, max: platformsInfo. max)
6675 }
6776
77+ public enum Error : Swift . Error , CustomStringConvertible , Sendable {
78+ case notAnNDK( Path )
79+ case unsupportedVersion( path: Path , minimumVersion: Version )
80+ case noSupportedVersions( minimumVersion: Version )
81+
82+ public var description : String {
83+ switch self {
84+ case let . notAnNDK( path) :
85+ " Package at path ' \( path. str) ' is not an Android NDK (no source.properties file) "
86+ case let . unsupportedVersion( path, minimumVersion) :
87+ " Android NDK version at path ' \( path. str) ' is not supported (r \( minimumVersion. description) or later required) "
88+ case let . noSupportedVersions( minimumVersion) :
89+ " All installed NDK versions are not supported (r \( minimumVersion. description) or later required) "
90+ }
91+ }
92+ }
93+
94+ struct Properties {
95+ let properties : JavaProperties
96+ let revision : Version
97+
98+ init ( data: Data ) throws {
99+ properties = try . init( data: data)
100+ guard properties [ " Pkg.Desc " ] == " Android NDK " else {
101+ throw StubError . error ( " Package is not an Android NDK " )
102+ }
103+ revision = try Version ( properties [ " Pkg.BaseRevision " ] ?? properties [ " Pkg.Revision " ] ?? " " )
104+ }
105+ }
106+
68107 struct ABIs : DecodableWithConfiguration {
69108 let abis : [ String : ABI ]
70109
@@ -185,27 +224,96 @@ public import Foundation
185224 }
186225 }
187226
188- public static func findInstallations( host: OperatingSystem , sdkPath: Path , fs: any FSProxy ) throws -> [ NDK ] {
227+ public struct Installations : Sendable {
228+ private let preferredIndex : Int ?
229+ public let ndks : [ NDK ]
230+
231+ init ( preferredIndex: Int ? = nil , ndks: [ NDK ] ) {
232+ self . preferredIndex = preferredIndex
233+ self . ndks = ndks
234+ }
235+
236+ public var preferredNDK : NDK ? {
237+ preferredIndex. map { ndks [ $0] } ?? ndks. only
238+ }
239+ }
240+
241+ public static func findInstallations( host: OperatingSystem , sdkPath: Path , fs: any FSProxy ) throws -> Installations {
242+ if let overridePath = NDK . environmentOverrideLocation {
243+ return try Installations ( ndks: [ NDK ( host: host, path: overridePath, fs: fs) ] )
244+ }
245+
189246 let ndkBasePath = sdkPath. join ( " ndk " )
190247 guard fs. exists ( ndkBasePath) else {
191- return [ ]
248+ return Installations ( ndks : [ ] )
192249 }
193250
194- let ndks = try fs. listdir ( ndkBasePath) . map ( { try Version ( $0) } ) . sorted ( )
195- let supportedNdks = ndks. filter { $0 >= minimumNDKVersion }
251+ var hadUnsupportedVersions : Bool = false
252+ let ndks = try fs. listdir ( ndkBasePath) . compactMap ( { subdir in
253+ do {
254+ return try NDK ( host: host, path: ndkBasePath. join ( subdir) , fs: fs)
255+ } catch Error . notAnNDK( _) {
256+ return nil
257+ } catch Error . unsupportedVersion( _, _) {
258+ hadUnsupportedVersions = true
259+ return nil
260+ }
261+ } ) . sorted ( by: \. version)
196262
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
263+ // 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.
264+ if ndks. isEmpty && hadUnsupportedVersions {
265+ throw Error . noSupportedVersions ( minimumVersion: Self . minimumNDKVersion)
266+ }
199267
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)
268+ // Respect Debian alternatives
269+ let preferredIndex : Int ?
270+ if sdkPath == AndroidSDK . defaultDebianLocation {
271+ preferredIndex = try ndks. firstIndex ( where: { try $0. path == fs. realpath ( Path ( " /usr/lib/android-ndk " ) ) } )
272+ } else {
273+ preferredIndex = nil
203274 }
275+
276+ return Installations ( preferredIndex: preferredIndex, ndks: ndks)
204277 }
205278 }
206279
207280 public static func findInstallations( host: OperatingSystem , fs: any FSProxy ) async throws -> [ AndroidSDK ] {
208- let defaultLocation : Path ? = switch host {
281+ var paths : [ Path ] = [ ]
282+ if let path = AndroidSDK . environmentOverrideLocation {
283+ paths. append ( path)
284+ }
285+ if let path = try AndroidSDK . defaultAndroidStudioLocation ( host: host) {
286+ paths. append ( path)
287+ }
288+ if host == . linux {
289+ paths. append ( AndroidSDK . defaultDebianLocation)
290+ }
291+ return try paths. compactMap { path in
292+ guard fs. exists ( path) else {
293+ return nil
294+ }
295+ return try AndroidSDK ( host: host, path: path, fs: fs)
296+ }
297+ }
298+ }
299+
300+ fileprivate extension AndroidSDK . NDK {
301+ /// 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`).
302+ /// - seealso: [Configuring NDK Path](https://github.com/android/ndk-samples/wiki/Configure-NDK-Path#terminologies)
303+ static var environmentOverrideLocation : Path ? {
304+ ( getEnvironmentVariable ( " ANDROID_NDK_ROOT " ) ?? getEnvironmentVariable ( " ANDROID_NDK_HOME " ) ) ? . nilIfEmpty. map ( Path . init)
305+ }
306+ }
307+
308+ fileprivate extension AndroidSDK {
309+ /// The location of the Android SDK based on the `ANDROID_HOME` environment variable (falling back to the deprecated but well known `ANDROID_SDK_ROOT`).
310+ /// - seealso: [Android environment variables](https://developer.android.com/tools/variables)
311+ static var environmentOverrideLocation : Path ? {
312+ ( getEnvironmentVariable ( " ANDROID_HOME " ) ?? getEnvironmentVariable ( " ANDROID_SDK_ROOT " ) ) ? . nilIfEmpty. map ( Path . init)
313+ }
314+
315+ static func defaultAndroidStudioLocation( host: OperatingSystem ) throws -> Path ? {
316+ switch host {
209317 case . windows:
210318 // %LOCALAPPDATA%\Android\Sdk
211319 try FileManager . default. url ( for: . applicationSupportDirectory, in: . userDomainMask, appropriateFor: nil , create: false ) . appendingPathComponent ( " Android " ) . appendingPathComponent ( " Sdk " ) . filePath
@@ -218,11 +326,11 @@ public import Foundation
218326 default :
219327 nil
220328 }
329+ }
221330
222- if let path = defaultLocation, fs. exists ( path) {
223- return try [ AndroidSDK ( host: host, path: path, fs: fs) ]
224- }
225-
226- return [ ]
331+ /// 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".
332+ /// These packages are available in non-free / multiverse and multiple versions can be installed simultaneously.
333+ static var defaultDebianLocation : Path {
334+ Path ( " /usr/lib/android-sdk " )
227335 }
228336}
0 commit comments