Skip to content

Commit 004b5f8

Browse files
committed
Improve Android SDK/NDK discovery
Now respects the environment variable overrides and looks for the Debian/Ubuntu package location. Closes #495
1 parent b924b0e commit 004b5f8

File tree

11 files changed

+392
-118
lines changed

11 files changed

+392
-118
lines changed

.github/scripts/linux_pre_build.sh

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/bin/bash
2+
##===----------------------------------------------------------------------===##
3+
##
4+
## This source file is part of the Swift open source project
5+
##
6+
## Copyright (c) 2025 Apple Inc. and the Swift project authors
7+
## Licensed under Apache License v2.0 with Runtime Library Exception
8+
##
9+
## See http://swift.org/LICENSE.txt for license information
10+
## See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
11+
##
12+
##===----------------------------------------------------------------------===##
13+
14+
set -e
15+
16+
if command -v apt-get >/dev/null 2>&1 ; then # bookworm, noble, jammy
17+
export DEBIAN_FRONTEND=noninteractive
18+
19+
apt-get update -y
20+
21+
# Build dependencies
22+
apt-get install -y libsqlite3-dev libncurses-dev
23+
24+
# Debug symbols
25+
apt-get install -y libc6-dbg
26+
27+
if [[ "$INSTALL_CMAKE" == "1" ]] ; then
28+
apt-get install -y cmake ninja-build
29+
fi
30+
31+
# Android NDK
32+
dpkg_architecture="$(dpkg --print-architecture)"
33+
if [[ "$SKIP_ANDROID" != "1" ]] && [[ "$dpkg_architecture" == amd64 ]] ; then
34+
eval "$(cat /etc/lsb-release)"
35+
case "$DISTRIB_CODENAME" in
36+
bookworm|jammy)
37+
: # Not available
38+
;;
39+
noble)
40+
apt-get install -y google-android-ndk-r26c-installer
41+
;;
42+
*)
43+
echo "Unknown distribution: $DISTRIB_CODENAME" >&2
44+
exit 1
45+
esac
46+
else
47+
echo "Skipping Android NDK installation on $dpkg_architecture" >&2
48+
fi
49+
elif command -v dnf >/dev/null 2>&1 ; then # rhel-ubi9
50+
dnf update -y
51+
52+
# Build dependencies
53+
dnf install -y sqlite-devel ncurses-devel
54+
55+
# Debug symbols
56+
dnf debuginfo-install -y glibc
57+
elif command -v yum >/dev/null 2>&1 ; then # amazonlinux2
58+
yum update -y
59+
60+
# Build dependencies
61+
yum install -y sqlite-devel ncurses-devel
62+
63+
# Debug symbols
64+
yum install -y yum-utils
65+
debuginfo-install -y glibc
66+
fi

.github/workflows/pull_request.yml

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,7 @@ jobs:
1414
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
1515
with:
1616
linux_os_versions: '["noble", "jammy", "rhel-ubi9"]'
17-
linux_pre_build_command: |
18-
if command -v apt-get >/dev/null 2>&1 ; then # bookworm, noble, jammy
19-
apt-get update -y
20-
21-
# Build dependencies
22-
apt-get install -y libsqlite3-dev libncurses-dev
23-
24-
# Debug symbols
25-
apt-get install -y libc6-dbg
26-
elif command -v dnf >/dev/null 2>&1 ; then # rhel-ubi9
27-
dnf update -y
28-
29-
# Build dependencies
30-
dnf install -y sqlite-devel ncurses-devel
31-
32-
# Debug symbols
33-
dnf debuginfo-install -y glibc
34-
elif command -v yum >/dev/null 2>&1 ; then # amazonlinux2
35-
yum update -y
36-
37-
# Build dependencies
38-
yum install -y sqlite-devel ncurses-devel
39-
40-
# Debug symbols
41-
yum install -y yum-utils
42-
debuginfo-install -y glibc
43-
fi
17+
linux_pre_build_command: ./.github/scripts/linux_pre_build.sh
4418
linux_build_command: 'swift test --no-parallel'
4519
linux_swift_versions: '["nightly-main", "nightly-6.2"]'
4620
windows_swift_versions: '["nightly-main"]'
@@ -50,13 +24,7 @@ jobs:
5024
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
5125
with:
5226
linux_os_versions: '["noble"]'
53-
linux_pre_build_command: |
54-
apt-get update -y
55-
56-
# Build dependencies
57-
apt-get install -y libsqlite3-dev libncurses-dev
58-
59-
apt-get install -y cmake ninja-build
27+
linux_pre_build_command: SKIP_ANDROID=1 INSTALL_CMAKE=1 ./.github/scripts/linux_pre_build.sh
6028
linux_build_command: 'swift package -Xbuild-tools-swiftc -DUSE_PROCESS_SPAWNING_WORKAROUND cmake-smoke-test --disable-sandbox --cmake-path `which cmake` --ninja-path `which ninja` --extra-cmake-arg -DCMAKE_C_COMPILER=`which clang` --extra-cmake-arg -DCMAKE_CXX_COMPILER=`which clang++` --extra-cmake-arg -DCMAKE_Swift_COMPILER=`which swiftc`'
6129
linux_swift_versions: '["nightly-main"]'
6230
windows_swift_versions: '[]'

Sources/SWBAndroidPlatform/AndroidSDK.swift

Lines changed: 149 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

Sources/SWBAndroidPlatform/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors
1010

1111
add_library(SWBAndroidPlatform
1212
AndroidSDK.swift
13+
JavaProperties.swift
1314
Plugin.swift)
1415
SwiftBuild_Bundle(MODULE SWBAndroidPlatform FILES
1516
Specs/Android.xcspec)

0 commit comments

Comments
 (0)