Skip to content

Commit eef1820

Browse files
committed
Test Android cross compilation on Windows
This also fixes the Android support to recognize an NDK even when an SDK is not present, which might be the case for some installations not driven through Android Studio (for example, NDK as installed by the chocolatey package manager on Windows, or installed manually into some directory).
1 parent bbf6db0 commit eef1820

File tree

10 files changed

+69
-42
lines changed

10 files changed

+69
-42
lines changed

.github/scripts/windows_pre_build.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ if ($InstallCMake) {
3030
}
3131

3232
if (-not $SkipAndroid) {
33-
choco install android-ndk
33+
choco install -y android-ndk
3434

3535
Import-Module $env:ChocolateyInstall\helpers\chocolateyProfile.psm1
3636
refreshenv

.github/workflows/pull_request.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
linux_pre_build_command: ./.github/scripts/linux_pre_build.sh
1818
linux_build_command: 'swift test --no-parallel'
1919
linux_swift_versions: '["nightly-main", "nightly-6.2"]'
20+
windows_pre_build_command: 'Invoke-Program .\.github\scripts\windows_pre_build.ps1'
2021
windows_swift_versions: '["nightly-main"]'
2122
windows_build_command: 'Invoke-Program swift test --no-parallel'
2223
enable_linux_static_sdk_build: true

Sources/SWBAndroidPlatform/AndroidSDK.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,16 +266,16 @@ public import Foundation
266266
}
267267
}
268268

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

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

Sources/SWBAndroidPlatform/Plugin.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public let initializePlugin: PluginInitializationFunction = { manager in
2626

2727
@_spi(Testing) public final class AndroidPlugin: Sendable {
2828
private let androidSDKInstallations = AsyncCache<OperatingSystem, [AndroidSDK]>()
29+
private let androidOverrideNDKInstallation = AsyncCache<OperatingSystem, AndroidSDK.NDK?>()
2930

3031
func cachedAndroidSDKInstallations(host: OperatingSystem) async throws -> [AndroidSDK] {
3132
try await androidSDKInstallations.value(forKey: host) {
@@ -34,8 +35,22 @@ public let initializePlugin: PluginInitializationFunction = { manager in
3435
}
3536
}
3637

37-
@_spi(Testing) public func effectiveInstallation(host: OperatingSystem) async throws -> (sdk: AndroidSDK, ndk: AndroidSDK.NDK)? {
38+
func cachedAndroidOverrideNDKInstallation(host: OperatingSystem) async throws -> AndroidSDK.NDK? {
39+
try await androidOverrideNDKInstallation.value(forKey: host) {
40+
if let overridePath = AndroidSDK.NDK.environmentOverrideLocation {
41+
return try AndroidSDK.NDK(host: host, path: overridePath, fs: localFS)
42+
}
43+
return nil
44+
}
45+
}
46+
47+
@_spi(Testing) public func effectiveInstallation(host: OperatingSystem) async throws -> (sdk: AndroidSDK?, ndk: AndroidSDK.NDK)? {
3848
guard let androidSdk = try? await cachedAndroidSDKInstallations(host: host).first else {
49+
// No SDK, but we might still have a standalone NDK from the env var override
50+
if let overrideNDK = try? await cachedAndroidOverrideNDKInstallation(host: host) {
51+
return (nil, overrideNDK)
52+
}
53+
3954
return nil
4055
}
4156

@@ -63,9 +78,9 @@ struct AndroidEnvironmentExtension: EnvironmentExtension {
6378
func additionalEnvironmentVariables(context: any EnvironmentExtensionAdditionalEnvironmentVariablesContext) async throws -> [String: String] {
6479
switch context.hostOperatingSystem {
6580
case .windows, .macOS, .linux:
66-
if let latest = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first {
67-
let sdkPath = latest.path.path.str
68-
let ndkPath = latest.preferredNDK?.path.path.str
81+
if let (sdk, ndk) = try? await plugin.effectiveInstallation(host: context.hostOperatingSystem) {
82+
let sdkPath = sdk?.path.path.str
83+
let ndkPath = ndk.path.path.str
6984
return [
7085
"ANDROID_HOME": sdkPath,
7186
"ANDROID_SDK_ROOT": sdkPath,
@@ -220,7 +235,7 @@ struct AndroidToolchainRegistryExtension: ToolchainRegistryExtension {
220235
let plugin: AndroidPlugin
221236

222237
func additionalToolchains(context: any ToolchainRegistryExtensionAdditionalToolchainsContext) async throws -> [Toolchain] {
223-
guard let toolchainPath = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first?.preferredNDK?.toolchainPath else {
238+
guard let toolchainPath = try? await plugin.effectiveInstallation(host: context.hostOperatingSystem)?.ndk.toolchainPath else {
224239
return []
225240
}
226241

Sources/SWBCore/Core.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -269,35 +269,35 @@ public final class Core: Sendable {
269269
self.stopAfterOpeningLibClang = UserDefaults.stopAfterOpeningLibClang
270270

271271
self.toolchainPaths = {
272-
var toolchainPaths = [(Path, strict: Bool)]()
272+
var toolchainPaths = [ToolchainRegistry.SearchPath]()
273273

274274
switch developerPath {
275275
case .xcode(let path):
276-
toolchainPaths.append((path.join("Toolchains"), strict: path.str.hasSuffix(".app/Contents/Developer")))
276+
toolchainPaths.append(.init(path: path.join("Toolchains"), strict: path.str.hasSuffix(".app/Contents/Developer")))
277277
case .swiftToolchain(let path, xcodeDeveloperPath: let xcodeDeveloperPath):
278278
if hostOperatingSystem == .windows {
279-
toolchainPaths.append((path.join("Toolchains"), strict: true))
279+
toolchainPaths.append(.init(path: path.join("Toolchains"), strict: true, aliases: ["default"]))
280280
} else {
281-
toolchainPaths.append((path, strict: true))
281+
toolchainPaths.append(.init(path: path, strict: true))
282282
}
283283
if let xcodeDeveloperPath {
284-
toolchainPaths.append((xcodeDeveloperPath.join("Toolchains"), strict: xcodeDeveloperPath.str.hasSuffix(".app/Contents/Developer")))
284+
toolchainPaths.append(.init(path: xcodeDeveloperPath.join("Toolchains"), strict: xcodeDeveloperPath.str.hasSuffix(".app/Contents/Developer")))
285285
}
286286
}
287287

288288
// FIXME: We should support building the toolchain locally (for `inferiorProductsPath`).
289289

290-
toolchainPaths.append((Path("/Library/Developer/Toolchains"), strict: false))
290+
toolchainPaths.append(.init(path: Path("/Library/Developer/Toolchains"), strict: false))
291291

292292
if let homeString = getEnvironmentVariable("HOME")?.nilIfEmpty {
293293
let userToolchainsPath = Path(homeString).join("Library/Developer/Toolchains")
294-
toolchainPaths.append((userToolchainsPath, strict: false))
294+
toolchainPaths.append(.init(path: userToolchainsPath, strict: false))
295295
}
296296

297297
if let externalToolchainDirs = getEnvironmentVariable("EXTERNAL_TOOLCHAINS_DIR") ?? environment["EXTERNAL_TOOLCHAINS_DIR"] {
298298
let envPaths = externalToolchainDirs.split(separator: Path.pathEnvironmentSeparator)
299299
for envPath in envPaths {
300-
toolchainPaths.append((Path(envPath), strict: false))
300+
toolchainPaths.append(.init(path: Path(envPath), strict: false))
301301
}
302302
}
303303

@@ -367,7 +367,7 @@ public final class Core: Sendable {
367367
}()
368368

369369
/// The list of toolchain search paths.
370-
@_spi(Testing) public var toolchainPaths: [(Path, strict: Bool)]
370+
@_spi(Testing) public var toolchainPaths: [ToolchainRegistry.SearchPath]
371371

372372
/// The platform registry.
373373
let _platformRegistry: UnsafeDelayedInitializationSendableWrapper<PlatformRegistry> = .init()

Sources/SWBCore/Settings/Settings.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ fileprivate struct PreOverridesSettings {
4848
if let toolchain = core.toolchainRegistry.lookup("default") {
4949
self.defaultToolchain = toolchain
5050
} else {
51-
core.delegate.error("missing required default toolchain")
51+
core.delegate.error("missing required default toolchain (\(core.toolchainRegistry.toolchains.count) loaded toolchain(s): \(core.toolchainRegistry.toolchains.map { $0.identifier }.joined(separator: " "))")
5252
self.defaultToolchain = nil
5353
}
5454

Sources/SWBCore/ToolchainRegistry.swift

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public final class Toolchain: Hashable, Sendable {
105105
self.testingLibraryPlatformNames = testingLibraryPlatformNames
106106
}
107107

108-
convenience init(path: Path, operatingSystem: OperatingSystem, fs: any FSProxy, pluginManager: any PluginManager, platformRegistry: PlatformRegistry?) async throws {
108+
convenience init(path: Path, operatingSystem: OperatingSystem, aliases additionalAliases: Set<String>, fs: any FSProxy, pluginManager: any PluginManager, platformRegistry: PlatformRegistry?) async throws {
109109
let data: PropertyListItem
110110

111111
do {
@@ -216,6 +216,8 @@ public final class Toolchain: Hashable, Sendable {
216216
aliases = Toolchain.deriveAliases(path: path, identifier: identifier)
217217
}
218218

219+
aliases.formUnion(additionalAliases)
220+
219221
// Framework Search Paths
220222
var frameworkSearchPaths = Array<String>()
221223
if let infoFrameworkSearchPaths = items["FallbackFrameworkSearchPaths"] {
@@ -414,6 +416,18 @@ extension Array where Element == Toolchain {
414416

415417
/// The ToolchainRegistry manages the set of registered toolchains.
416418
public final class ToolchainRegistry: @unchecked Sendable {
419+
@_spi(Testing) public struct SearchPath: Sendable {
420+
public var path: Path
421+
public var strict: Bool
422+
public var aliases: Set<String> = []
423+
424+
public init(path: Path, strict: Bool, aliases: Set<String> = []) {
425+
self.path = path
426+
self.strict = strict
427+
self.aliases = aliases
428+
}
429+
}
430+
417431
let fs: any FSProxy
418432
let hostOperatingSystem: OperatingSystem
419433

@@ -427,17 +441,19 @@ public final class ToolchainRegistry: @unchecked Sendable {
427441

428442
public static let appleToolchainIdentifierPrefix: String = "com.apple.dt.toolchain."
429443

430-
@_spi(Testing) public init(delegate: any ToolchainRegistryDelegate, searchPaths: [(Path, strict: Bool)], fs: any FSProxy, hostOperatingSystem: OperatingSystem) async {
444+
@_spi(Testing) public init(delegate: any ToolchainRegistryDelegate, searchPaths: [SearchPath], fs: any FSProxy, hostOperatingSystem: OperatingSystem) async {
431445
self.fs = fs
432446
self.hostOperatingSystem = hostOperatingSystem
433447

434-
for (path, strict) in searchPaths {
448+
for searchPath in searchPaths {
449+
let path = searchPath.path
450+
let strict = searchPath.strict
435451
if !strict && !fs.exists(path) {
436452
continue
437453
}
438454

439455
do {
440-
try await registerToolchainsInDirectory(path, strict: strict, operatingSystem: hostOperatingSystem, delegate: delegate)
456+
try await registerToolchainsInDirectory(path, strict: strict, aliases: searchPath.aliases, operatingSystem: hostOperatingSystem, delegate: delegate)
441457
}
442458
catch let err {
443459
delegate.issue(strict: strict, path, "failed to load toolchains in \(path.str): \(err)")
@@ -462,7 +478,7 @@ public final class ToolchainRegistry: @unchecked Sendable {
462478
}
463479

464480
/// Register all the toolchains in the given directory.
465-
private func registerToolchainsInDirectory(_ path: Path, strict: Bool, operatingSystem: OperatingSystem, delegate: any ToolchainRegistryDelegate) async throws {
481+
private func registerToolchainsInDirectory(_ path: Path, strict: Bool, aliases: Set<String>, operatingSystem: OperatingSystem, delegate: any ToolchainRegistryDelegate) async throws {
466482
let toolchainPaths: [Path] = try fs.listdir(path)
467483
.sorted()
468484
.map { path.join($0) }
@@ -475,7 +491,7 @@ public final class ToolchainRegistry: @unchecked Sendable {
475491
guard toolchainPath.basenameWithoutSuffix != "swift-latest" else { continue }
476492

477493
do {
478-
let toolchain = try await Toolchain(path: toolchainPath, operatingSystem: operatingSystem, fs: fs, pluginManager: delegate.pluginManager, platformRegistry: delegate.platformRegistry)
494+
let toolchain = try await Toolchain(path: toolchainPath, operatingSystem: operatingSystem, aliases: aliases, fs: fs, pluginManager: delegate.pluginManager, platformRegistry: delegate.platformRegistry)
479495
try register(toolchain)
480496
} catch let err {
481497
delegate.issue(strict: strict, toolchainPath, "failed to load toolchain: \(err)")
@@ -505,19 +521,10 @@ public final class ToolchainRegistry: @unchecked Sendable {
505521
/// Look up the toolchain with the given identifier.
506522
public func lookup(_ identifier: String) -> Toolchain? {
507523
let lowercasedIdentifier = identifier.lowercased()
508-
if hostOperatingSystem == .macOS {
509-
if ["default", "xcode"].contains(lowercasedIdentifier) {
510-
return toolchainsByIdentifier[ToolchainRegistry.defaultToolchainIdentifier] ?? toolchainsByAlias[lowercasedIdentifier]
511-
} else {
512-
return toolchainsByIdentifier[identifier] ?? toolchainsByAlias[lowercasedIdentifier]
513-
}
524+
if ["default", "xcode"].contains(lowercasedIdentifier) {
525+
return toolchainsByIdentifier[ToolchainRegistry.defaultToolchainIdentifier] ?? toolchainsByAlias[lowercasedIdentifier]
514526
} else {
515-
// On non-Darwin, assume if there is only one registered toolchain, it is the default.
516-
if ["default", "xcode"].contains(lowercasedIdentifier) || identifier == ToolchainRegistry.defaultToolchainIdentifier {
517-
return toolchainsByIdentifier[ToolchainRegistry.defaultToolchainIdentifier] ?? toolchainsByAlias[lowercasedIdentifier] ?? toolchainsByIdentifier.values.only
518-
} else {
519-
return toolchainsByIdentifier[identifier] ?? toolchainsByAlias[lowercasedIdentifier]
520-
}
527+
return toolchainsByIdentifier[identifier] ?? toolchainsByAlias[lowercasedIdentifier]
521528
}
522529
}
523530

Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import Foundation
1414
@_spi(Testing) import SWBAndroidPlatform
1515
import SWBTestSupport
16-
import SWBUtil
16+
@_spi(Testing) import SWBUtil
1717
import Testing
1818

1919
@Suite
@@ -415,7 +415,11 @@ fileprivate struct AndroidSDKTests {
415415
return ndkVersionPath
416416
}
417417
let host = try ProcessInfo.processInfo.hostOperatingSystem()
418-
try await block(host, fs, sdkPath, ndkVersionPaths.map { try AbsolutePath(validating: $0) })
418+
419+
// Clear the environment to avoid influence from Android SDK/NDK environment overrides
420+
try await withEnvironment([:], clean: true) {
421+
try await block(host, fs, sdkPath, ndkVersionPaths.map { try AbsolutePath(validating: $0) })
422+
}
419423
}
420424

421425
private func withNDKVersion(fs: PseudoFS = PseudoFS(), sdkPath: AbsolutePath = .root, version: Version, _ block: (OperatingSystem, any FSProxy, AbsolutePath, AbsolutePath) async throws -> ()) async throws {

Tests/SWBCoreTests/CoreTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,8 +460,8 @@ import SWBServiceCore
460460
}
461461
let toolchainPaths = try #require(core?.toolchainPaths)
462462
for expectedPathString in expectedPathStrings {
463-
#expect(toolchainPaths.contains(where: { paths in
464-
paths.0 == Path(expectedPathString) && paths.strict == false
463+
#expect(toolchainPaths.contains(where: { searchPath in
464+
searchPath.path == Path(expectedPathString) && searchPath.strict == false
465465
}), "Unable to find \(expectedPathString)")
466466
}
467467

Tests/SWBCoreTests/ToolchainRegistryTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ import SWBServiceCore
107107
return
108108
}
109109
let delegate = TestDataDelegate(pluginManager: core.pluginManager)
110-
let registry = await ToolchainRegistry(delegate: delegate, searchPaths: [(tmpDirPath, strict: strict)], fs: fs, hostOperatingSystem: core.hostOperatingSystem)
110+
let registry = await ToolchainRegistry(delegate: delegate, searchPaths: [.init(path: tmpDirPath, strict: strict)], fs: fs, hostOperatingSystem: core.hostOperatingSystem)
111111
try perform(registry, delegate.warnings, delegate.errors)
112112
}
113113
}

0 commit comments

Comments
 (0)