Skip to content

Support cross-compiling to Android with a Swift SDK#1818

Merged
keith merged 8 commits into
bazelbuild:mainfrom
AttilaTheFun:android-support
Jul 2, 2026
Merged

Support cross-compiling to Android with a Swift SDK#1818
keith merged 8 commits into
bazelbuild:mainfrom
AttilaTheFun:android-support

Conversation

@AttilaTheFun

@AttilaTheFun AttilaTheFun commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

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 sibling
of the WebAssembly PR (#1817); it does not depend on it.

What this adds

swift.android_sdk downloads the swift.org Android Swift SDK bundle and, using
the shared framework (#1821), 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/android_app builds 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:

bazel run //examples/cross_compilation/android_app:run

The example app running on an Android emulator, showing "Hello from Swift, Android!"

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//:all
from hermetic_android_toolchains
— and register it alongside the Swift toolchain:

register_toolchains(
    "@swift_toolchain//:swift_toolchain_android_aarch64_xcode",
    "@androidndk//:all",
)

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.sysroot from rules_android_ndk's toolchain reports the clang
    directory, not the sysroot, so the Swift toolchain derives the sysroot from the
    toolchain's files.
  • The Swift binary link runs through cc_common.link with that cc toolchain, but
    rules_android_ndk reports its sysroot as the clang dir and exposes an empty
    dynamic_runtime_lib, so the toolchain adds --sysroot and stages
    libc++_shared.so itself. It also links libc++ as the shared runtime
    (rules_android_ndk links it statically by default — rules_android_ndk#93).
  • To package libc++_shared.so into an APK, select_android_runtime_lib (in
    swift/toolchains/android_runtime_lib.bzl) selects it from the resolved cc
    toolchain.

The runnable example (examples/cross_compilation/android_app)

A real android_binary (built with rules_android + rules_kotlin, the
real-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) exposes
its .so via DefaultInfo, not CcInfo, so it can't go straight into
android_binary.deps. Wrapping it in a cc_library (a prebuilt .so in srcs
becomes a CcInfo dynamic library) lets android_binary's per-ABI native split
collect it into lib/arm64-v8a/; libc++_shared.so goes in the same way via
select_android_runtime_lib.

rules_android / rules_kotlin / rules_java / rules_jvm_external (and the
hermetic emulator the :run target downloads) are all dev_dependency = True,
split into android_app/android.MODULE.bazel and include()d, with a pinned
Maven lock and the hermetic @androidsdk — so consumers of rules_swift are
unaffected and protobuf stays at 34.

CI

The example builds and tests in the normal macOS //examples/... job (the
@androidndk/@androidsdk toolchains come from hermetic_android_toolchains, dev
dependencies; the arm64 --android_platforms + a hermetic JDK are set in
.bazelrc). No emulator runs in CI; instead the Android path is guarded by:

  • Analysis tests (//test:android) asserting the action command lines — the
    Swift compile targets aarch64-linux-android, and the link runs the NDK clang
    for the Android target against the NDK sysroot, linking the shared libc++ and
    staging libc++_shared.so.
  • Artifact tests (build_test) running the NDK llvm-readelf on the built
    .so (AArch64 ELF, libc++_shared.so in NEEDED, the @_cdecl symbol
    exported in .dynsym) and asserting the APK packages both .sos under
    lib/arm64-v8a/ plus the dexed Kotlin.

Comment thread .bazelci/presubmit.yml Outdated
Comment thread .bazelci/presubmit.yml Outdated
Comment thread examples/cross_compilation/Sources/Greeter/Greeter.swift Outdated
Comment thread examples/cross_compilation/Sources/SwiftJNI/SwiftJNI.swift Outdated
Comment thread examples/cross_compilation/BUILD.bazel Outdated
Comment thread examples/cross_compilation/android_app/BUILD.bazel Outdated
Comment thread swift/toolchains/swift_toolchain.bzl Outdated
Comment thread swift/extensions.bzl Outdated
Comment thread swift/extensions.bzl Outdated
Comment thread swift/extensions.bzl Outdated
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
@keith keith merged commit 9b15139 into bazelbuild:main Jul 2, 2026
15 checks passed
@AttilaTheFun AttilaTheFun deleted the android-support branch July 2, 2026 23:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants