Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .github/scripts/linux_pre_build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## This source file is part of the Swift open source project
##
## Copyright (c) 2025 Apple Inc. and the Swift project authors
## Licensed under Apache License v2.0 with Runtime Library Exception
##
## See http://swift.org/LICENSE.txt for license information
## See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
##
##===----------------------------------------------------------------------===##

set -e

if command -v apt-get >/dev/null 2>&1 ; then # bookworm, noble, jammy
export DEBIAN_FRONTEND=noninteractive

apt-get update -y

# Build dependencies
apt-get install -y libsqlite3-dev libncurses-dev

# Debug symbols
apt-get install -y libc6-dbg

if [[ "$INSTALL_CMAKE" == "1" ]] ; then
apt-get install -y cmake ninja-build
fi

# Android NDK
dpkg_architecture="$(dpkg --print-architecture)"
if [[ "$SKIP_ANDROID" != "1" ]] && [[ "$dpkg_architecture" == amd64 ]] ; then
eval "$(cat /etc/lsb-release)"
case "$DISTRIB_CODENAME" in
bookworm|jammy)
: # Not available
;;
noble)
apt-get install -y google-android-ndk-r26c-installer
;;
*)
echo "Unknown distribution: $DISTRIB_CODENAME" >&2
exit 1
esac
else
echo "Skipping Android NDK installation on $dpkg_architecture" >&2
fi
elif command -v dnf >/dev/null 2>&1 ; then # rhel-ubi9
dnf update -y

# Build dependencies
dnf install -y sqlite-devel ncurses-devel

# Debug symbols
dnf debuginfo-install -y glibc
elif command -v yum >/dev/null 2>&1 ; then # amazonlinux2
yum update -y

# Build dependencies
yum install -y sqlite-devel ncurses-devel

# Debug symbols
yum install -y yum-utils
debuginfo-install -y glibc
fi
36 changes: 2 additions & 34 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,7 @@ jobs:
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
with:
linux_os_versions: '["noble", "jammy", "rhel-ubi9"]'
linux_pre_build_command: |
if command -v apt-get >/dev/null 2>&1 ; then # bookworm, noble, jammy
apt-get update -y

# Build dependencies
apt-get install -y libsqlite3-dev libncurses-dev

# Debug symbols
apt-get install -y libc6-dbg
elif command -v dnf >/dev/null 2>&1 ; then # rhel-ubi9
dnf update -y

# Build dependencies
dnf install -y sqlite-devel ncurses-devel

# Debug symbols
dnf debuginfo-install -y glibc
elif command -v yum >/dev/null 2>&1 ; then # amazonlinux2
yum update -y

# Build dependencies
yum install -y sqlite-devel ncurses-devel

# Debug symbols
yum install -y yum-utils
debuginfo-install -y glibc
fi
linux_pre_build_command: ./.github/scripts/linux_pre_build.sh
linux_build_command: 'swift test --no-parallel'
linux_swift_versions: '["nightly-main", "nightly-6.2"]'
windows_swift_versions: '["nightly-main"]'
Expand All @@ -50,13 +24,7 @@ jobs:
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
with:
linux_os_versions: '["noble"]'
linux_pre_build_command: |
apt-get update -y

# Build dependencies
apt-get install -y libsqlite3-dev libncurses-dev

apt-get install -y cmake ninja-build
linux_pre_build_command: SKIP_ANDROID=1 INSTALL_CMAKE=1 ./.github/scripts/linux_pre_build.sh
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`'
linux_swift_versions: '["nightly-main"]'
windows_swift_versions: '[]'
Expand Down
187 changes: 149 additions & 38 deletions Sources/SWBAndroidPlatform/AndroidSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,43 +15,54 @@ public import Foundation

@_spi(Testing) public struct AndroidSDK: Sendable {
public let host: OperatingSystem
public let path: Path
public let path: AbsolutePath
private let ndkInstallations: NDK.Installations

/// List of NDKs available in this SDK installation, sorted by version number from oldest to newest.
@_spi(Testing) public let ndks: [NDK]
@_spi(Testing) public var ndks: [NDK] {
ndkInstallations.ndks
}

public var latestNDK: NDK? {
ndks.last
public var preferredNDK: NDK? {
ndkInstallations.preferredNDK ?? ndks.last
}

init(host: OperatingSystem, path: Path, fs: any FSProxy) throws {
init(host: OperatingSystem, path: AbsolutePath, fs: any FSProxy) throws {
self.host = host
self.path = path
self.ndks = try NDK.findInstallations(host: host, sdkPath: path, fs: fs)
self.ndkInstallations = try NDK.findInstallations(host: host, sdkPath: path, fs: fs)
}

@_spi(Testing) public struct NDK: Equatable, Sendable {
public static let minimumNDKVersion = Version(23)

public let host: OperatingSystem
public let path: Path
public let path: AbsolutePath
public let version: Version
public let abis: [String: ABI]
public let deploymentTargetRange: DeploymentTargetRange

init(host: OperatingSystem, path ndkPath: Path, version: Version, fs: any FSProxy) throws {
@_spi(Testing) public init(host: OperatingSystem, path ndkPath: AbsolutePath, fs: any FSProxy) throws {
self.host = host
self.path = ndkPath
self.version = version
self.toolchainPath = try AbsolutePath(validating: path.path.join("toolchains").join("llvm").join("prebuilt").join(Self.hostTag(host)))
self.sysroot = try AbsolutePath(validating: toolchainPath.path.join("sysroot"))

let propertiesFile = ndkPath.path.join("source.properties")
guard fs.exists(propertiesFile) else {
throw Error.notAnNDK(ndkPath)
}

let metaPath = ndkPath.join("meta")
self.version = try NDK.Properties(data: Data(fs.read(propertiesFile))).revision

let metaPath = ndkPath.path.join("meta")

guard #available(macOS 14, *) else {
throw StubError.error("Unsupported macOS version")
}

if version < Self.minimumNDKVersion {
throw StubError.error("Android NDK version at path '\(ndkPath.str)' is not supported (r\(Self.minimumNDKVersion.description) or later required)")
throw Error.unsupportedVersion(path: ndkPath, minimumVersion: Self.minimumNDKVersion)
}

self.abis = try JSONDecoder().decode(ABIs.self, from: Data(fs.read(metaPath.join("abis.json"))), configuration: version).abis
Expand All @@ -65,6 +76,36 @@ public import Foundation
deploymentTargetRange = DeploymentTargetRange(min: platformsInfo.min, max: platformsInfo.max)
}

public enum Error: Swift.Error, CustomStringConvertible, Sendable {
case notAnNDK(AbsolutePath)
case unsupportedVersion(path: AbsolutePath, minimumVersion: Version)
case noSupportedVersions(minimumVersion: Version)

public var description: String {
switch self {
case let .notAnNDK(path):
"Package at path '\(path.path.str)' is not an Android NDK (no source.properties file)"
case let .unsupportedVersion(path, minimumVersion):
"Android NDK version at path '\(path.path.str)' is not supported (r\(minimumVersion.description) or later required)"
case let .noSupportedVersions(minimumVersion):
"All installed NDK versions are not supported (r\(minimumVersion.description) or later required)"
}
}
}

struct Properties {
let properties: JavaProperties
let revision: Version

init(data: Data) throws {
properties = try .init(data: data)
guard properties["Pkg.Desc"] == "Android NDK" else {
throw StubError.error("Package is not an Android NDK")
}
revision = try Version(properties["Pkg.BaseRevision"] ?? properties["Pkg.Revision"] ?? "")
}
}

struct ABIs: DecodableWithConfiguration {
let abis: [String: ABI]

Expand Down Expand Up @@ -161,15 +202,10 @@ public import Foundation
public let max: Int
}

public var toolchainPath: Path {
path.join("toolchains").join("llvm").join("prebuilt").join(hostTag)
}

public var sysroot: Path {
toolchainPath.join("sysroot")
}
public let toolchainPath: AbsolutePath
public let sysroot: AbsolutePath

private var hostTag: String? {
private static func hostTag(_ host: OperatingSystem) -> String? {
switch host {
case .windows:
// Also works on Windows on ARM via Prism binary translation.
Expand All @@ -185,44 +221,119 @@ public import Foundation
}
}

public static func findInstallations(host: OperatingSystem, sdkPath: Path, fs: any FSProxy) throws -> [NDK] {
let ndkBasePath = sdkPath.join("ndk")
public struct Installations: Sendable {
private let preferredIndex: Int?
public let ndks: [NDK]

init(preferredIndex: Int? = nil, ndks: [NDK]) {
self.preferredIndex = preferredIndex
self.ndks = ndks
}

public var preferredNDK: NDK? {
preferredIndex.map { ndks[$0] } ?? ndks.only
}
}

public static func findInstallations(host: OperatingSystem, sdkPath: AbsolutePath, fs: any FSProxy) throws -> Installations {
if let overridePath = NDK.environmentOverrideLocation {
return try Installations(ndks: [NDK(host: host, path: overridePath, fs: fs)])
}

let ndkBasePath = sdkPath.path.join("ndk")
guard fs.exists(ndkBasePath) else {
return []
return Installations(ndks: [])
}

let ndks = try fs.listdir(ndkBasePath).map({ try Version($0) }).sorted()
let supportedNdks = ndks.filter { $0 >= minimumNDKVersion }
var hadUnsupportedVersions: Bool = false
let ndks = try fs.listdir(ndkBasePath).compactMap({ subdir in
do {
return try NDK(host: host, path: AbsolutePath(validating: ndkBasePath.join(subdir)), fs: fs)
} catch Error.notAnNDK(_) {
return nil
} catch Error.unsupportedVersion(_, _) {
hadUnsupportedVersions = true
return nil
}
}).sorted(by: \.version)

// 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.
let discoveredNdks = supportedNdks.isEmpty && !ndks.isEmpty ? ndks : supportedNdks
// 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.
if ndks.isEmpty && hadUnsupportedVersions {
throw Error.noSupportedVersions(minimumVersion: Self.minimumNDKVersion)
}

return try discoveredNdks.map { ndkVersion in
let ndkPath = ndkBasePath.join(ndkVersion.description)
return try NDK(host: host, path: ndkPath, version: ndkVersion, fs: fs)
// Respect Debian alternatives
let preferredIndex: Int?
if sdkPath == AndroidSDK.defaultDebianLocation, let ndkLinkPath = AndroidSDK.NDK.defaultDebianLocation {
preferredIndex = try ndks.firstIndex(where: { try $0.path.path == fs.realpath(ndkLinkPath.path) })
} else {
preferredIndex = nil
}

return Installations(preferredIndex: preferredIndex, ndks: ndks)
}
}

public static func findInstallations(host: OperatingSystem, fs: any FSProxy) async throws -> [AndroidSDK] {
let defaultLocation: Path? = switch host {
var paths: [AbsolutePath] = []
if let path = AndroidSDK.environmentOverrideLocation {
paths.append(path)
}
if let path = try AndroidSDK.defaultAndroidStudioLocation(host: host) {
paths.append(path)
}
if let path = AndroidSDK.defaultDebianLocation, host == .linux {
paths.append(path)
}
return try paths.compactMap { path in
guard fs.exists(path.path) else {
return nil
}
return try AndroidSDK(host: host, path: path, fs: fs)
}
}
}

fileprivate extension AndroidSDK.NDK {
/// 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`).
/// - seealso: [Configuring NDK Path](https://github.com/android/ndk-samples/wiki/Configure-NDK-Path#terminologies)
static var environmentOverrideLocation: AbsolutePath? {
(getEnvironmentVariable("ANDROID_NDK_ROOT") ?? getEnvironmentVariable("ANDROID_NDK_HOME"))?.nilIfEmpty.map { AbsolutePath($0) } ?? nil
}

/// 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".
/// These packages are available in non-free / multiverse and multiple versions can be installed simultaneously.
static var defaultDebianLocation: AbsolutePath? {
AbsolutePath("/usr/lib/android-ndk")
}
}

fileprivate extension AndroidSDK {
/// The location of the Android SDK based on the `ANDROID_HOME` environment variable (falling back to the deprecated but well known `ANDROID_SDK_ROOT`).
/// - seealso: [Android environment variables](https://developer.android.com/tools/variables)
static var environmentOverrideLocation: AbsolutePath? {
(getEnvironmentVariable("ANDROID_HOME") ?? getEnvironmentVariable("ANDROID_SDK_ROOT"))?.nilIfEmpty.map { AbsolutePath($0) } ?? nil
}

static func defaultAndroidStudioLocation(host: OperatingSystem) throws -> AbsolutePath? {
switch host {
case .windows:
// %LOCALAPPDATA%\Android\Sdk
try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Android").appendingPathComponent("Sdk").filePath
try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Android").appendingPathComponent("Sdk").absoluteFilePath
case .macOS:
// ~/Library/Android/sdk
try FileManager.default.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Android").appendingPathComponent("sdk").filePath
try FileManager.default.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Android").appendingPathComponent("sdk").absoluteFilePath
case .linux:
// ~/Android/Sdk
Path.homeDirectory.join("Android").join("Sdk")
try AbsolutePath(validating: Path.homeDirectory.join("Android").join("Sdk"))
default:
nil
}
}

if let path = defaultLocation, fs.exists(path) {
return try [AndroidSDK(host: host, path: path, fs: fs)]
}

return []
/// Location of the Android SDK installed by the `google-*` family of packages available in Debian 13 "Trixie" and Ubuntu 24.04 "Noble".
/// These packages are available in non-free / multiverse and multiple versions can be installed simultaneously.
static var defaultDebianLocation: AbsolutePath? {
AbsolutePath("/usr/lib/android-sdk")
}
}
1 change: 1 addition & 0 deletions Sources/SWBAndroidPlatform/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors

add_library(SWBAndroidPlatform
AndroidSDK.swift
JavaProperties.swift
Plugin.swift)
SwiftBuild_Bundle(MODULE SWBAndroidPlatform FILES
Specs/Android.xcspec)
Expand Down
Loading