@@ -15,43 +15,54 @@ public import Foundation
1515
1616@_spi ( Testing)   public  struct  AndroidSDK :  Sendable  { 
1717    public  let  host :  OperatingSystem 
18-     public  let  path :  Path 
18+     public  let  path :  AbsolutePath 
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
27-     init ( host:  OperatingSystem ,  path:  Path ,  fs:  any  FSProxy )  throws  { 
30+     init ( host:  OperatingSystem ,  path:  AbsolutePath ,  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  { 
3437        public  static  let  minimumNDKVersion   =  Version ( 23 ) 
3538
3639        public  let  host :  OperatingSystem 
37-         public  let  path :  Path 
40+         public  let  path :  AbsolutePath 
3841        public  let  version :  Version 
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:  AbsolutePath ,  fs:  any  FSProxy )  throws  { 
4346            self . host =  host
4447            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+             } 
4655
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 " ) 
4859
4960            guard  #available( macOS 14 ,  * )  else  { 
5061                throw  StubError . error ( " Unsupported macOS version " ) 
5162            } 
5263
5364            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) 
5566            } 
5667
5768            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
6576            deploymentTargetRange =  DeploymentTargetRange ( min:  platformsInfo. min,  max:  platformsInfo. max) 
6677        } 
6778
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+ 
68109        struct  ABIs :  DecodableWithConfiguration  { 
69110            let  abis :  [ String :  ABI ] 
70111
@@ -161,15 +202,10 @@ public import Foundation
161202            public  let  max :  Int 
162203        } 
163204
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 
171207
172-         private  var   hostTag :  String ?   { 
208+         private  static   func   hostTag( _ host :   OperatingSystem )   ->  String ?   { 
173209            switch  host { 
174210            case  . windows: 
175211                // Also works on Windows on ARM via Prism binary translation.
@@ -185,44 +221,119 @@ public import Foundation
185221            } 
186222        } 
187223
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 " ) 
190244            guard  fs. exists ( ndkBasePath)  else  { 
191-                 return  [ ] 
245+                 return  Installations ( ndks :   [ ] ) 
192246            } 
193247
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) 
196259
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+             } 
199264
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 
203271            } 
272+ 
273+             return  Installations ( preferredIndex:  preferredIndex,  ndks:  ndks) 
204274        } 
205275    } 
206276
207277    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 { 
209320        case  . windows: 
210321            // %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 
212323        case  . macOS: 
213324            // ~/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 
215326        case  . linux: 
216327            // ~/Android/Sdk
217-             Path . homeDirectory. join ( " Android " ) . join ( " Sdk " ) 
328+             try   AbsolutePath ( validating :   Path . homeDirectory. join ( " Android " ) . join ( " Sdk " ) ) 
218329        default : 
219330            nil 
220331        } 
332+     } 
221333
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 " ) 
227338    } 
228339} 
0 commit comments