diff --git a/.github/scripts/linux_pre_build.sh b/.github/scripts/linux_pre_build.sh new file mode 100755 index 00000000..8adf2410 --- /dev/null +++ b/.github/scripts/linux_pre_build.sh @@ -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 diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c39112b4..1f1e70d1 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -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"]' @@ -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: '[]' diff --git a/Sources/SWBAndroidPlatform/AndroidSDK.swift b/Sources/SWBAndroidPlatform/AndroidSDK.swift index cb66619d..3b934838 100644 --- a/Sources/SWBAndroidPlatform/AndroidSDK.swift +++ b/Sources/SWBAndroidPlatform/AndroidSDK.swift @@ -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 @@ -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] @@ -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. @@ -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") } } diff --git a/Sources/SWBAndroidPlatform/CMakeLists.txt b/Sources/SWBAndroidPlatform/CMakeLists.txt index fdb0bfa7..72d8cd9b 100644 --- a/Sources/SWBAndroidPlatform/CMakeLists.txt +++ b/Sources/SWBAndroidPlatform/CMakeLists.txt @@ -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) diff --git a/Sources/SWBAndroidPlatform/JavaProperties.swift b/Sources/SWBAndroidPlatform/JavaProperties.swift new file mode 100644 index 00000000..0a6059d7 --- /dev/null +++ b/Sources/SWBAndroidPlatform/JavaProperties.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import Foundation +internal import SWBUtil + +/// A simple representation of a Java properties file. +/// +/// See `java.util.Properties` for a description of the file format. This parser is a simplified version that doesn't handle line continuations, etc., because our use case is narrow. +struct JavaProperties { + private let properties: [String: String] + + init(data: Data) throws { + properties = Dictionary(uniqueKeysWithValues: String(decoding: data, as: UTF8.self).split(whereSeparator: { $0.isNewline }).map(String.init).map { + let (key, value) = $0.split("=") + return (key.trimmingCharacters(in: .whitespaces), value.trimmingCharacters(in: .whitespaces)) + }) + } + + subscript(_ propertyName: String) -> String? { + properties[propertyName] + } +} diff --git a/Sources/SWBAndroidPlatform/Plugin.swift b/Sources/SWBAndroidPlatform/Plugin.swift index 88fa5f76..3eb0ca1d 100644 --- a/Sources/SWBAndroidPlatform/Plugin.swift +++ b/Sources/SWBAndroidPlatform/Plugin.swift @@ -11,7 +11,7 @@ //===----------------------------------------------------------------------===// public import SWBUtil -import SWBCore +public import SWBCore import SWBMacro import Foundation @@ -24,7 +24,7 @@ import Foundation manager.register(AndroidToolchainRegistryExtension(plugin: plugin), type: ToolchainRegistryExtensionPoint.self) } -final class AndroidPlugin: Sendable { +@_spi(Testing) public final class AndroidPlugin: Sendable { private let androidSDKInstallations = AsyncCache() func cachedAndroidSDKInstallations(host: OperatingSystem) async throws -> [AndroidSDK] { @@ -33,6 +33,18 @@ final class AndroidPlugin: Sendable { try await AndroidSDK.findInstallations(host: host, fs: localFS) } } + + @_spi(Testing) public func effectiveInstallation(host: OperatingSystem) async throws -> (sdk: AndroidSDK, ndk: AndroidSDK.NDK)? { + guard let androidSdk = try? await cachedAndroidSDKInstallations(host: host).first else { + return nil + } + + guard let androidNdk = androidSdk.preferredNDK else { + return nil + } + + return (androidSdk, androidNdk) + } } struct AndroidPlatformSpecsExtension: SpecificationsExtension { @@ -52,9 +64,13 @@ struct AndroidEnvironmentExtension: EnvironmentExtension { switch context.hostOperatingSystem { case .windows, .macOS, .linux: if let latest = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first { + let sdkPath = latest.path.path.str + let ndkPath = latest.preferredNDK?.path.path.str return [ - "ANDROID_SDK_ROOT": latest.path.str, - "ANDROID_NDK_ROOT": latest.ndks.last?.path.str, + "ANDROID_HOME": sdkPath, + "ANDROID_SDK_ROOT": sdkPath, + "ANDROID_NDK_ROOT": ndkPath, + "ANDROID_NDK_HOME": ndkPath, ].compactMapValues { $0 } } default: @@ -80,10 +96,10 @@ struct AndroidPlatformExtension: PlatformInfoExtension { } } -struct AndroidSDKRegistryExtension: SDKRegistryExtension { - let plugin: AndroidPlugin +@_spi(Testing) public struct AndroidSDKRegistryExtension: SDKRegistryExtension { + @_spi(Testing) public let plugin: AndroidPlugin - func additionalSDKs(context: any SDKRegistryExtensionAdditionalSDKsContext) async throws -> [(path: Path, platform: SWBCore.Platform?, data: [String: PropertyListItem])] { + public func additionalSDKs(context: any SDKRegistryExtensionAdditionalSDKsContext) async throws -> [(path: Path, platform: SWBCore.Platform?, data: [String: PropertyListItem])] { let host = context.hostOperatingSystem guard let androidPlatform = context.platformRegistry.lookup(name: "android") else { return [] @@ -108,11 +124,7 @@ struct AndroidSDKRegistryExtension: SDKRegistryExtension { "AR": .plString(host.imageFormat.executableName(basename: "llvm-ar")), ] - guard let androidSdk = try? await plugin.cachedAndroidSDKInstallations(host: host).first else { - return [] - } - - guard let androidNdk = androidSdk.latestNDK else { + guard let (_, androidNdk) = try await plugin.effectiveInstallation(host: host) else { return [] } @@ -150,7 +162,7 @@ struct AndroidSDKRegistryExtension: SDKRegistryExtension { swiftSettings = [:] } - return [(androidNdk.sysroot, androidPlatform, [ + return [(androidNdk.sysroot.path, androidPlatform, [ "Type": .plString("SDK"), "Version": .plString("0.0.0"), "CanonicalName": .plString("android"), @@ -187,7 +199,7 @@ struct AndroidToolchainRegistryExtension: ToolchainRegistryExtension { let plugin: AndroidPlugin func additionalToolchains(context: any ToolchainRegistryExtensionAdditionalToolchainsContext) async throws -> [Toolchain] { - guard let toolchainPath = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first?.latestNDK?.toolchainPath else { + guard let toolchainPath = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first?.preferredNDK?.toolchainPath else { return [] } @@ -197,13 +209,13 @@ struct AndroidToolchainRegistryExtension: ToolchainRegistryExtension { displayName: "Android", version: Version(0, 0, 0), aliases: [], - path: toolchainPath, + path: toolchainPath.path, frameworkPaths: [], libraryPaths: [], defaultSettings: [:], overrideSettings: [:], defaultSettingsWhenPrimary: [:], - executableSearchPaths: [toolchainPath.join("bin")], + executableSearchPaths: [toolchainPath.path.join("bin")], testingLibraryPlatformNames: [], fs: context.fs) ] diff --git a/Sources/SWBUtil/FSProxy.swift b/Sources/SWBUtil/FSProxy.swift index ddd3c5a9..d6a6ee71 100644 --- a/Sources/SWBUtil/FSProxy.swift +++ b/Sources/SWBUtil/FSProxy.swift @@ -879,9 +879,13 @@ public class PseudoFS: FSProxy, @unchecked Sendable { public func realpath(_ path: Path) throws -> Path { // TODO: Update this to actually return the link target when we support - // symlinks; for now it just returns the input, which seems reasonably - // correct. - return path + // symlinks; for now it just returns the input (or the link target if it's a symlink), + // which seems reasonably correct for simple cases. + do { + return try readlink(path) + } catch { + return path + } } public func readlink(_ path: Path) throws -> Path { diff --git a/Sources/SWBUtil/Path.swift b/Sources/SWBUtil/Path.swift index 1edf33b7..28294f29 100644 --- a/Sources/SWBUtil/Path.swift +++ b/Sources/SWBUtil/Path.swift @@ -1006,6 +1006,10 @@ public struct RelativePath: Hashable, Equatable, Serializable, Sendable { } extension AbsolutePath { + public static var root: AbsolutePath { + AbsolutePath(.root)! + } + public var dirname: AbsolutePath { AbsolutePath(path.dirname)! } diff --git a/Sources/SWBUtil/URL.swift b/Sources/SWBUtil/URL.swift index 725f7ac5..9195baa6 100644 --- a/Sources/SWBUtil/URL.swift +++ b/Sources/SWBUtil/URL.swift @@ -18,7 +18,7 @@ extension URL { /// This should always be used whenever the file path equivalent of a URL is needed. DO NOT use ``path`` or ``path(percentEncoded:)``, as these deal in terms of the path portion of the URL representation per RFC8089, which on Windows would include a leading slash. /// /// - throws: ``FileURLError`` if the URL does not represent a file or its path is otherwise not representable. - public var filePath: Path { + public var absoluteFilePath: AbsolutePath { get throws { guard isFileURL else { throw FileURLError.notRepresentable(self) @@ -27,12 +27,16 @@ extension URL { guard let cString else { throw FileURLError.notRepresentable(self) } - let fp = Path(String(cString: cString)) - precondition(fp.isAbsolute, "path '\(fp.str)' is not absolute") - return fp + return try AbsolutePath(validating: String(cString: cString)) } } } + + public var filePath: Path { + get throws { + try absoluteFilePath.path + } + } } fileprivate enum FileURLError: Error, CustomStringConvertible { diff --git a/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift b/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift index 37329d94..b567c294 100644 --- a/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift +++ b/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift @@ -24,22 +24,74 @@ fileprivate struct AndroidSDKTests { // It's OK if `installations` is an empty set, the host system might have no Android SDK/NDK installed for installation in installations { #expect(installation.host == host) - #expect(installation.latestNDK == installation.ndks.last) } } - @Test func abis_r22() async throws { - try await withNDKVersion(version: Version("22.1.7171670")) { host, fs, ndkVersionPath in - let error = #expect(throws: StubError.self) { - try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs) + @Test(.skipHostOS(.windows, "This test inherently relies on Unix-style paths")) + func debian() async throws { + let fs = PseudoFS() + let sdkPath = try AbsolutePath(validating: "/usr/lib/android-sdk") + try fs.createDirectory(sdkPath.path, recursive: true) + try await withNDKVersions(fs: fs, sdkPath: sdkPath, versions: Version("24"), Version("25"), Version("26")) { host, fs, sdkPath, ndkVersionPaths in + for ndkVersionPath in ndkVersionPaths { + try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("abis.json")) { contents in + contents <<< + """ + { + } + """ + } + + try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("platforms.json")) { contents in + contents <<< + """ + { + "min": 21, + "max": 35, + "aliases": { + } + } + """ + } } - #expect(error?.description == "Android NDK version at path '\(ndkVersionPath.str)' is not supported (r23 or later required)") + + try fs.symlink(Path("/usr/lib/android-ndk"), target: ndkVersionPaths[1].path) + + let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: sdkPath, fs: fs) + let installation = try #require(installations.preferredNDK) + #expect(installations.ndks.count == 3) + #expect(installation != installations.ndks.first) + #expect(installation == installations.ndks[1]) + #expect(installation != installations.ndks.last) + #expect(installation.host == host) + #expect(installation.path == ndkVersionPaths[1]) + #expect(try installation.version == Version("25")) + #expect(installation.deploymentTargetRange.min == 21) + #expect(installation.deploymentTargetRange.max == 35) + + #expect(installation.abis.isEmpty) + } + } + + @Test func unsupportedVersions() async throws { + try await withNDKVersion(version: Version("22.1.7171670")) { host, fs, sdkPath, ndkVersionPath in + let error = try #require(throws: AndroidSDK.NDK.Error.self) { + try AndroidSDK.NDK.findInstallations(host: host, sdkPath: sdkPath, fs: fs) + } + #expect(error.description == "All installed NDK versions are not supported (r23 or later required)") + } + + try await withNDKVersion(version: Version("22.1.7171670")) { host, fs, sdkPath, ndkVersionPath in + let error = try #require(throws: AndroidSDK.NDK.Error.self) { + try AndroidSDK.NDK(host: host, path: ndkVersionPath, fs: fs) + } + #expect(error.description == "Android NDK version at path '\(ndkVersionPath.path.str)' is not supported (r23 or later required)") } } @Test func abis_r26_3() async throws { - try await withNDKVersion(version: Version("26.3.11579264")) { host, fs, ndkVersionPath in - try await fs.writeFileContents(ndkVersionPath.join("meta").join("abis.json")) { contents in + try await withNDKVersion(version: Version("26.3.11579264")) { host, fs, sdkPath, ndkVersionPath in + try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("abis.json")) { contents in contents <<< """ { @@ -83,7 +135,7 @@ fileprivate struct AndroidSDKTests { """ } - try await fs.writeFileContents(ndkVersionPath.join("meta").join("platforms.json")) { contents in + try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("platforms.json")) { contents in contents <<< """ { @@ -115,8 +167,8 @@ fileprivate struct AndroidSDKTests { """ } - let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs) - let installation = try #require(installations.only) + let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: sdkPath, fs: fs) + let installation = try #require(installations.ndks.only) #expect(installation.host == host) #expect(installation.path == ndkVersionPath) #expect(try installation.version == Version("26.3.11579264")) @@ -180,8 +232,8 @@ fileprivate struct AndroidSDKTests { } @Test func abis_r27() async throws { - try await withNDKVersion(version: Version("27.0.11718014")) { host, fs, ndkVersionPath in - try await fs.writeFileContents(ndkVersionPath.join("meta").join("abis.json")) { contents in + try await withNDKVersion(version: Version("27.0.11718014")) { host, fs, sdkPath, ndkVersionPath in + try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("abis.json")) { contents in contents <<< """ { @@ -239,7 +291,7 @@ fileprivate struct AndroidSDKTests { """ } - try await fs.writeFileContents(ndkVersionPath.join("meta").join("platforms.json")) { contents in + try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("platforms.json")) { contents in contents <<< """ { @@ -272,8 +324,8 @@ fileprivate struct AndroidSDKTests { """ } - let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs) - let installation = try #require(installations.only) + let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: sdkPath, fs: fs) + let installation = try #require(installations.ndks.only) #expect(installation.host == host) #expect(installation.path == ndkVersionPath) #expect(try installation.version == Version("27.0.11718014")) @@ -349,13 +401,26 @@ fileprivate struct AndroidSDKTests { } } - private func withNDKVersion(version: Version, _ block: (OperatingSystem, any FSProxy, Path) async throws -> ()) async throws { - let fs = PseudoFS() - let ndkPath = Path.root.join("ndk") - let ndkVersionPath = ndkPath.join(version.description) - try fs.createDirectory(ndkPath, recursive: true) - try fs.createDirectory(ndkVersionPath.join("meta"), recursive: true) + private func withNDKVersions(fs: PseudoFS = PseudoFS(), sdkPath: AbsolutePath = .root, versions: Version..., block: (OperatingSystem, any FSProxy, AbsolutePath, [AbsolutePath]) async throws -> ()) async throws { + let ndkPath = sdkPath.path.join("ndk") + let ndkVersionPaths = try await versions.asyncMap { version in + let ndkVersionPath = ndkPath.join(version.description) + try fs.createDirectory(ndkPath, recursive: true) + try fs.createDirectory(ndkVersionPath.join("meta"), recursive: true) + try await fs.writeFileContents(ndkVersionPath.join("source.properties")) { + $0 <<< "Pkg.Desc = Android NDK\n" + $0 <<< "Pkg.Revision = \(version.description)-beta1\n" + $0 <<< "Pkg.BaseRevision = \(version.description)\n" + } + return ndkVersionPath + } let host = try ProcessInfo.processInfo.hostOperatingSystem() - try await block(host, fs, ndkVersionPath) + try await block(host, fs, sdkPath, ndkVersionPaths.map { try AbsolutePath(validating: $0) }) + } + + private func withNDKVersion(fs: PseudoFS = PseudoFS(), sdkPath: AbsolutePath = .root, version: Version, _ block: (OperatingSystem, any FSProxy, AbsolutePath, AbsolutePath) async throws -> ()) async throws { + try await withNDKVersions(fs: fs, sdkPath: sdkPath, versions: version) { host, fs, sdkPath, ndkVersionPaths in + try await block(host, fs, sdkPath, ndkVersionPaths[0]) + } } } diff --git a/Tests/SWBAndroidPlatformTests/SWBAndroidPlatformTests.swift b/Tests/SWBAndroidPlatformTests/SWBAndroidPlatformTests.swift index 74ef48e4..117588f9 100644 --- a/Tests/SWBAndroidPlatformTests/SWBAndroidPlatformTests.swift +++ b/Tests/SWBAndroidPlatformTests/SWBAndroidPlatformTests.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Testing +@_spi(Testing) import SWBAndroidPlatform import SWBProtocol import SWBTestSupport import SWBTaskExecution @@ -94,6 +95,12 @@ fileprivate struct AndroidBuildOperationTests: CoreBasedTests { ), ]) let core = try await getCore() + let androidExtension = try await #require(core.pluginManager.extensions(of: SDKRegistryExtensionPoint.self).compactMap { $0 as? AndroidSDKRegistryExtension }.only) + let (_, androidNdk) = try #require(await androidExtension.plugin.effectiveInstallation(host: core.hostOperatingSystem)) + if androidNdk.version < Version(27) && arch == "riscv64" { + return // riscv64 support was introduced in NDK r27 + } + let tester = try await BuildOperationTester(core, testProject, simulated: false) let projectDir = tester.workspace.projects[0].sourceRoot