Support cross-compiling to Android with a Swift SDK#1818
Merged
Conversation
f63884f to
0bbd975
Compare
9f98a9b to
dd88d52
Compare
dd88d52 to
6c1b007
Compare
keith
reviewed
Jun 25, 2026
0147d03 to
f505bef
Compare
This was referenced Jun 30, 2026
Add `swift.android_sdk`, which downloads the swift.org Android Swift SDK bundle
and defines a Swift toolchain targeting {aarch64,x86_64}-unknown-linux-android.
`swift_binary(linkshared = True)` produces a JNI lib<name>.so whose entry points
are written entirely in Swift; examples/cross_compilation builds one.
rules_swift does not fetch or manage the Android NDK. C/C++ compilation and
linking go through a separately registered Android C++ cc toolchain (e.g.
@androidndk//:all from hermetic_android_toolchains), and the Swift toolchain
reads that toolchain's sysroot at analysis time. Register one alongside the
Swift toolchain.
A few NDK-integration details handled in the Swift toolchain rule:
- rules_android_ndk's CcToolchainInfo.sysroot reports the clang dir, not the
sysroot, so we derive the sysroot from the toolchain's files.
- The Swift link action drives the NDK clang directly and bypasses the cc
toolchain's sysroot/runtime-lib link features, so for Android we add --sysroot
and stage libc++_shared.so (which the NDK clang links by default) into the
link ourselves.
- select_android_runtime_lib selects libc++_shared.so from the resolved cc
toolchain for APK packaging.
Verified end to end: //examples/cross_compilation:libSwiftJNI.so builds a real
aarch64 Android .so linked through @androidndk, and a downstream app packages it
into a working APK alongside libc++_shared.so.
…Swift) The cross_compilation example previously built only the JNI .so and documented (in prose) how a downstream module would package it into an APK. Replace that recipe with a real, building android_binary that runs on a device/emulator and shows the Swift greeting on screen — a far more compelling demonstration of integrating Swift into an Android app. It's packaged the real-world way, with rules_android (android_binary) and rules_kotlin (kt_android_library). The one rules_swift-specific detail: the swift_binary(linkshared) .so arrives via DefaultInfo (not CcInfo), so it's wrapped in a cc_library to feed android_binary's per-ABI native split; libc++ is selected via select_android_runtime_lib the same way. rules_android/rules_kotlin/rules_java/rules_jvm_external are all dev_dependency deps (with a pinned Maven lock and the hermetic @AndroidSDK), so consumers of rules_swift are unaffected. CI builds the APK via a build_test under --config=android_example (scoped flags; the rest of the build is untouched). Claude-Session: https://claude.ai/code/session_01SmG1kqA3qB4WsLGU2xavuJ
Toolchain: - Keep file_prefix_map enabled for Android (hermetic working-dir remap) instead of disabling it; the worker only requires DEVELOPER_DIR for the Apple developer-dir remap, so guard that remap on DEVELOPER_DIR being set (a cross compile on a macOS host has none). Fixes non-hermetic debug info. - Extract the Android link logic into _swift_android_linkopts_cc_info keyed on os == "android"; drop the redundant -lm (the clang driver links libm anyway; -lstdc++ stays, since it selects the shared libc++_shared.so the SDK intends). - Rename the SDK repo's host_swiftc attr to paired_swiftc. - Trim verbose comments/docs across the toolchain and extensions; simplify the clang-only error message. Example (//examples/cross_compilation/android_app): - Consolidate the Swift sources, Kotlin app, and BUILD into android_app/. - Build the .so as a dep of android_binary (its native split configures it for Android) instead of a hand-written platform transition; use the platforms rules_android exposes (@rules_android//:arm64-v8a) rather than a custom one. - Drop the manual tags; mark the Android-only targets target_compatible_with os:android so the //examples/... wildcard skips them on a host and the example runs in the normal macOS CI job. Remove the dedicated cross-compilation task. - Split the example's dev dependencies into android_app/android.MODULE.bazel, include()d from the root MODULE; move the Maven lock out of the repo root; link rules_android#485 for the maven.install requirement. - Set the rules_android flags (android_platforms + hermetic JDK) globally rather than behind a --config. Claude-Session: https://claude.ai/code/session_01SmG1kqA3qB4WsLGU2xavuJ
Per review, guard the Android cross-compile on CI without a device. Analysis tests (//test:android, macOS) assert the action command lines: - the Swift compile targets aarch64-linux-android, - the link runs the NDK clang for the Android target against the NDK sysroot and links libc++ as the shared libc++_shared.so, and - libc++_shared.so is staged into the link. Artifact tests on the example (run via build_test) cover what a build can't see: - android_so_abi_check runs the NDK llvm-readelf on the built .so to assert it is an AArch64 ELF that lists libc++_shared.so in NEEDED and exports the @_cdecl JNI entry point in .dynsym; - android_apk_contents_check asserts the APK packages both .so files under lib/arm64-v8a/ and the dexed Kotlin. Claude-Session: https://claude.ai/code/session_01SmG1kqA3qB4WsLGU2xavuJ
Adds //examples/cross_compilation/android_app:run, which installs the example APK on a connected device/emulator and launches it — booting a hermetic emulator first if nothing is connected, so the example runs with nothing preinstalled (no Android Studio, no device). adb comes from @AndroidSDK; the emulator + AOSP system image are downloaded by a dev module extension (emulator.bzl). macOS/arm64 only, to keep the delta small. Demonstrates what zero-setup 'bazel run' support would cost for the example. Claude-Session: https://claude.ai/code/session_01SmG1kqA3qB4WsLGU2xavuJ
Explains why -lstdc++ is needed: the Android cc toolchain links libc++ statically by default, so the shared libc++_shared.so must be forced. Claude-Session: https://claude.ai/code/session_01SmG1kqA3qB4WsLGU2xavuJ
Fixes the gazelle-up-to-date CI check. Uses the correct @rules_cc//cc:find_cc_toolchain_bzl label (gazelle's auto-generated find_cc_toolchain doesn't exist), matching swift/toolchains/BUILD. Claude-Session: https://claude.ai/code/session_01SmG1kqA3qB4WsLGU2xavuJ
07dca6c to
1f7611c
Compare
keith
approved these changes
Jul 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Android support, part of the #1809 split, on top of the now-merged foundation
PRs #1820 (
swift_binary(linkshared)) and #1821 (Swift-SDK framework). A siblingof the WebAssembly PR (#1817); it does not depend on it.
What this adds
swift.android_sdkdownloads the swift.org Android Swift SDK bundle and, usingthe shared framework (#1821), defines a Swift toolchain targeting
{aarch64,x86_64}-unknown-linux-android.swift_binary(linkshared = True)produces a JNI
lib<name>.sowhose entry points are written entirely in Swift.examples/cross_compilation/android_appbuilds one and ships a complete,runnable Android app that loads it and shows the Swift-computed greeting on
screen — with nothing preinstalled (no Android Studio, no device), one command
boots a hermetic emulator, installs, and launches it:
The NDK comes from a cc toolchain, not from rules_swift
Per review feedback, rules_swift does not fetch or manage the NDK. C/C++
compilation and linking for Android targets go through a separately registered
Android C++ cc toolchain, and the Swift toolchain reads that toolchain's sysroot
at analysis time. So you bring an Android cc toolchain — e.g.
@androidndk//:allfrom hermetic_android_toolchains
— and register it alongside the Swift toolchain:
This keeps rules_swift out of the NDK-management business entirely (no NDK
download, no per-host repos, no bespoke C++ toolchain) and works with any
rules_android_ndk-based Android cc toolchain.
A few NDK-integration details that aren't obvious:
CcToolchainInfo.sysrootfrom rules_android_ndk's toolchain reports the clangdirectory, not the sysroot, so the Swift toolchain derives the sysroot from the
toolchain's files.
cc_common.linkwith that cc toolchain, butrules_android_ndk reports its sysroot as the clang dir and exposes an empty
dynamic_runtime_lib, so the toolchain adds--sysrootand stageslibc++_shared.soitself. It also links libc++ as the shared runtime(rules_android_ndk links it statically by default — rules_android_ndk#93).
libc++_shared.sointo an APK,select_android_runtime_lib(inswift/toolchains/android_runtime_lib.bzl) selects it from the resolved cctoolchain.
The runnable example (
examples/cross_compilation/android_app)A real
android_binary(built withrules_android+rules_kotlin, thereal-world way Android apps are packaged) completes the chain end-to-end:
MainActivity.kt → NativeBridge.greetingFromSwift()(Kotlin, JNI) →libSwiftJNI.so(swift_binary(linkshared),@_cdecl) →Greeter(
swift_library).The one rules_swift-specific detail:
swift_binary(linkshared = True)exposesits
.soviaDefaultInfo, notCcInfo, so it can't go straight intoandroid_binary.deps. Wrapping it in acc_library(a prebuilt.soinsrcsbecomes a
CcInfodynamic library) letsandroid_binary's per-ABI native splitcollect it into
lib/arm64-v8a/;libc++_shared.sogoes in the same way viaselect_android_runtime_lib.rules_android/rules_kotlin/rules_java/rules_jvm_external(and thehermetic emulator the
:runtarget downloads) are alldev_dependency = True,split into
android_app/android.MODULE.bazelandinclude()d, with a pinnedMaven lock and the hermetic
@androidsdk— so consumers ofrules_swiftareunaffected and
protobufstays at 34.CI
The example builds and tests in the normal macOS
//examples/...job (the@androidndk/@androidsdktoolchains come from hermetic_android_toolchains, devdependencies; the arm64
--android_platforms+ a hermetic JDK are set in.bazelrc). No emulator runs in CI; instead the Android path is guarded by://test:android) asserting the action command lines — theSwift compile targets
aarch64-linux-android, and the link runs the NDK clangfor the Android target against the NDK sysroot, linking the shared libc++ and
staging
libc++_shared.so.build_test) running the NDKllvm-readelfon the built.so(AArch64 ELF,libc++_shared.soinNEEDED, the@_cdeclsymbolexported in
.dynsym) and asserting the APK packages both.sos underlib/arm64-v8a/plus the dexed Kotlin.