Skip to content

Commit 7ebfd5e

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 105de3b commit 7ebfd5e

File tree

13 files changed

+123
-51
lines changed

13 files changed

+123
-51
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/SWBBuildService/Tools.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -569,11 +569,12 @@ private class SerializedDiagnosticsTool {
569569
return false
570570
}
571571

572-
let toolchain = core.toolchainRegistry.defaultToolchain
573-
guard let libclangPath = toolchain?.librarySearchPaths.findLibrary(operatingSystem: core.hostOperatingSystem, basename: "clang") ?? toolchain?.fallbackLibrarySearchPaths.findLibrary(operatingSystem: core.hostOperatingSystem, basename: "clang") else {
574-
throw StubError.error("unable to find libclang")
572+
guard let toolchain = core.toolchainRegistry.defaultToolchain else {
573+
throw StubError.error("unable to find libclang (no default toolchain)")
575574
}
576575

576+
let libclangPath = try toolchain.lookup(subject: .library(basename: "clang"), operatingSystem: core.hostOperatingSystem)
577+
577578
guard let libclang = Libclang(path: libclangPath.str) else {
578579
emitError("unable to open libclang: \(libclangPath)")
579580
return false

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/Settings/StackedSearchPaths.swift

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,43 @@ public final class StackedSearchPath: Sendable {
6868
}
6969

7070
extension StackedSearchPath {
71+
public func lookup(subject: StackedSearchPathLookupSubject, operatingSystem: OperatingSystem) -> Path? {
72+
lookup(subject.fileName(operatingSystem: operatingSystem))
73+
}
74+
7175
public func findExecutable(operatingSystem: OperatingSystem, basename: String) -> Path? {
72-
lookup(Path(operatingSystem.imageFormat.executableName(basename: basename)))
76+
lookup(subject: .executable(basename: basename), operatingSystem: operatingSystem)
7377
}
7478

7579
public func findLibrary(operatingSystem: OperatingSystem, basename: String) -> Path? {
76-
lookup(Path("lib\(basename).\(operatingSystem.imageFormat.dynamicLibraryExtension)"))
80+
lookup(subject: .library(basename: basename), operatingSystem: operatingSystem)
81+
}
82+
}
83+
84+
public enum StackedSearchPathLookupSubject {
85+
case executable(basename: String)
86+
case library(basename: String)
87+
88+
func fileName(operatingSystem: OperatingSystem) -> Path {
89+
switch self {
90+
case let .executable(basename):
91+
Path(operatingSystem.imageFormat.executableName(basename: basename))
92+
case let .library(basename):
93+
Path("lib\(basename).\(operatingSystem.imageFormat.dynamicLibraryExtension)")
94+
}
95+
}
96+
}
97+
98+
public enum StackedSearchPathLookupError: Error {
99+
case unableToFind(subject: StackedSearchPathLookupSubject, operatingSystem: OperatingSystem, searchPaths: [StackedSearchPath])
100+
}
101+
102+
extension StackedSearchPathLookupError: CustomStringConvertible {
103+
public var description: String {
104+
switch self {
105+
case let .unableToFind(subject, operatingSystem, searchPaths):
106+
let candidates = searchPaths.flatMap { $0.paths.map { $0.join(subject.fileName(operatingSystem: operatingSystem)).str }}
107+
return "unable to find \(subject.fileName(operatingSystem: operatingSystem)) among search paths: \(candidates.joined(separator: ", "))"
108+
}
77109
}
78110
}

Sources/SWBCore/TaskGeneration.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,10 +1308,10 @@ extension TaskOutputParserDelegate {
13081308
func readSerializedDiagnostics(at path: Path, workingDirectory: Path, workspaceContext: WorkspaceContext) -> [Diagnostic] {
13091309
do {
13101310
// Using the default toolchain's libclang regardless of context should be sufficient, since we assume serialized diagnostics to be a stable format.
1311-
let toolchain = workspaceContext.core.toolchainRegistry.defaultToolchain
1312-
guard let libclangPath = toolchain?.librarySearchPaths.findLibrary(operatingSystem: workspaceContext.core.hostOperatingSystem, basename: "clang") ?? toolchain?.fallbackLibrarySearchPaths.findLibrary(operatingSystem: workspaceContext.core.hostOperatingSystem, basename: "clang") else {
1313-
throw StubError.error("unable to find libclang")
1311+
guard let toolchain = workspaceContext.core.toolchainRegistry.defaultToolchain else {
1312+
throw StubError.error("unable to find libclang (no default toolchain)")
13141313
}
1314+
let libclangPath = try toolchain.lookup(subject: .library(basename: "clang"), operatingSystem: workspaceContext.core.hostOperatingSystem)
13151315
guard let libclang = workspaceContext.core.lookupLibclang(path: libclangPath).libclang else {
13161316
throw StubError.error("unable to open libclang: '\(libclangPath.str)'")
13171317
}

Sources/SWBCore/ToolchainRegistry.swift

Lines changed: 38 additions & 19 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"] {
@@ -412,8 +414,32 @@ extension Array where Element == Toolchain {
412414
}
413415
}
414416

417+
extension Toolchain {
418+
public func lookup(subject: StackedSearchPathLookupSubject, operatingSystem: OperatingSystem) throws(StackedSearchPathLookupError) -> Path {
419+
let searchPathsList = [librarySearchPaths, fallbackLibrarySearchPaths]
420+
for searchPaths in searchPathsList {
421+
if let library = searchPaths.lookup(subject: subject, operatingSystem: operatingSystem) {
422+
return library
423+
}
424+
}
425+
throw .unableToFind(subject: subject, operatingSystem: operatingSystem, searchPaths: searchPathsList)
426+
}
427+
}
428+
415429
/// The ToolchainRegistry manages the set of registered toolchains.
416430
public final class ToolchainRegistry: @unchecked Sendable {
431+
@_spi(Testing) public struct SearchPath: Sendable {
432+
public var path: Path
433+
public var strict: Bool
434+
public var aliases: Set<String> = []
435+
436+
public init(path: Path, strict: Bool, aliases: Set<String> = []) {
437+
self.path = path
438+
self.strict = strict
439+
self.aliases = aliases
440+
}
441+
}
442+
417443
let fs: any FSProxy
418444
let hostOperatingSystem: OperatingSystem
419445

@@ -427,17 +453,19 @@ public final class ToolchainRegistry: @unchecked Sendable {
427453

428454
public static let appleToolchainIdentifierPrefix: String = "com.apple.dt.toolchain."
429455

430-
@_spi(Testing) public init(delegate: any ToolchainRegistryDelegate, searchPaths: [(Path, strict: Bool)], fs: any FSProxy, hostOperatingSystem: OperatingSystem) async {
456+
@_spi(Testing) public init(delegate: any ToolchainRegistryDelegate, searchPaths: [SearchPath], fs: any FSProxy, hostOperatingSystem: OperatingSystem) async {
431457
self.fs = fs
432458
self.hostOperatingSystem = hostOperatingSystem
433459

434-
for (path, strict) in searchPaths {
460+
for searchPath in searchPaths {
461+
let path = searchPath.path
462+
let strict = searchPath.strict
435463
if !strict && !fs.exists(path) {
436464
continue
437465
}
438466

439467
do {
440-
try await registerToolchainsInDirectory(path, strict: strict, operatingSystem: hostOperatingSystem, delegate: delegate)
468+
try await registerToolchainsInDirectory(path, strict: strict, aliases: searchPath.aliases, operatingSystem: hostOperatingSystem, delegate: delegate)
441469
}
442470
catch let err {
443471
delegate.issue(strict: strict, path, "failed to load toolchains in \(path.str): \(err)")
@@ -462,7 +490,7 @@ public final class ToolchainRegistry: @unchecked Sendable {
462490
}
463491

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

477505
do {
478-
let toolchain = try await Toolchain(path: toolchainPath, operatingSystem: operatingSystem, fs: fs, pluginManager: delegate.pluginManager, platformRegistry: delegate.platformRegistry)
506+
let toolchain = try await Toolchain(path: toolchainPath, operatingSystem: operatingSystem, aliases: aliases, fs: fs, pluginManager: delegate.pluginManager, platformRegistry: delegate.platformRegistry)
479507
try register(toolchain)
480508
} catch let err {
481509
delegate.issue(strict: strict, toolchainPath, "failed to load toolchain: \(err)")
@@ -505,24 +533,15 @@ public final class ToolchainRegistry: @unchecked Sendable {
505533
/// Look up the toolchain with the given identifier.
506534
public func lookup(_ identifier: String) -> Toolchain? {
507535
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-
}
536+
if ["default", "xcode"].contains(lowercasedIdentifier) {
537+
return toolchainsByIdentifier[ToolchainRegistry.defaultToolchainIdentifier] ?? toolchainsByAlias[lowercasedIdentifier]
514538
} 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-
}
539+
return toolchainsByIdentifier[identifier] ?? toolchainsByAlias[lowercasedIdentifier]
521540
}
522541
}
523542

524543
public var defaultToolchain: Toolchain? {
525-
return self.lookup(ToolchainRegistry.defaultToolchainIdentifier)
544+
return self.lookup("default")
526545
}
527546

528547
public var toolchains: Set<Toolchain> {

0 commit comments

Comments
 (0)