Skip to content

Commit 6c1b007

Browse files
committed
Support cross-compiling to Android with a Swift SDK
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.
1 parent 2687dc0 commit 6c1b007

20 files changed

Lines changed: 943 additions & 122 deletions

File tree

.bazelci/presubmit.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@ tasks:
6262
bazel: last_green
6363
<<: *mac_common
6464

65+
macos_cross_compilation:
66+
name: "Cross-compilation (Android)"
67+
platform: macos_arm64
68+
xcode_version: "26.2"
69+
bazel: latest
70+
# The //examples/cross_compilation targets are tagged `manual` (they fetch
71+
# the Swift SDK bundle and the NDK), so they are excluded from the
72+
# `//examples/...` wildcard the other tasks build. List them explicitly here
73+
# so the Swift-SDK cross-compilation toolchains are exercised in CI.
74+
# Build-only: the trivial JNI library exercises the link path, while runtime
75+
# behavior is covered downstream by real consumers.
76+
build_targets:
77+
- "//examples/cross_compilation:libSwiftJNI.so"
78+
6579
macos_latest_shell_scripts:
6680
name: "macOS shell tests"
6781
platform: macos_arm64

.bazelrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ build --worker_sandboxing
2222

2323
build --enable_platform_specific_config
2424

25+
# Accept the Android NDK/SDK licenses for hermetic_android_toolchains' @androidndk
26+
# (the Android cc toolchain used by //examples/cross_compilation). Only consulted
27+
# when those repositories are fetched.
28+
common --repo_env=ACCEPTED_ANDROID_NDK_LICENSE_VERSION=r27c
29+
common --repo_env=ACCEPTED_ANDROID_SDK_LICENSE_VERSION=35
30+
2531
common:linux --repo_env=CC=clang
2632
build:linux --cxxopt='-std=c++17' --host_cxxopt='-std=c++17'
2733
common:linux --//test:apple_build_tests=False

MODULE.bazel

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ use_repo(system_sdk, "system_sdk")
8282
swift = use_extension("//swift:extensions.bzl", "swift", dev_dependency = True)
8383
swift.toolchain(
8484
name = "swift_toolchain",
85-
swift_version = "6.3",
85+
swift_version = "6.3.2",
86+
)
87+
swift.android_sdk(
88+
toolchain_name = "swift_toolchain",
8689
)
8790
use_repo(
8891
swift,
@@ -110,6 +113,47 @@ register_toolchains(
110113
dev_dependency = True,
111114
)
112115

116+
register_toolchains(
117+
# Android Swift SDK toolchains used by //examples/cross_compilation. As with
118+
# the embedded toolchains above, we register only the host platforms used by
119+
# CI rather than `@swift_toolchain//:all`, because rules_swift cannot yet
120+
# auto-select a Linux distribution and `:all` would make the host/exec
121+
# toolchain ambiguous across distros. A consumer that builds on a single host
122+
# platform can simply register `@swift_toolchain//:all`. The Android C/C++
123+
# toolchain comes from `@androidndk//:all` (registered below), not here.
124+
"@swift_toolchain//:swift_toolchain_android_aarch64_ubuntu22.04",
125+
"@swift_toolchain//:swift_toolchain_android_aarch64_ubuntu22.04-aarch64",
126+
"@swift_toolchain//:swift_toolchain_android_aarch64_xcode",
127+
"@swift_toolchain//:swift_toolchain_android_x86_64_ubuntu22.04",
128+
"@swift_toolchain//:swift_toolchain_android_x86_64_ubuntu22.04-aarch64",
129+
"@swift_toolchain//:swift_toolchain_android_x86_64_xcode",
130+
dev_dependency = True,
131+
)
132+
133+
# The Android C/C++ cc toolchains for the cross-compile. rules_swift's Swift
134+
# Android toolchain links through these and reads the sysroot from them.
135+
bazel_dep(name = "hermetic_android_toolchains", version = "0.1.1", dev_dependency = True)
136+
137+
android_ndk = use_extension(
138+
"@hermetic_android_toolchains//:extensions.bzl",
139+
"android",
140+
dev_dependency = True,
141+
)
142+
android_ndk.sdk(
143+
build_tools_version = "35.0.0",
144+
version = "35",
145+
)
146+
android_ndk.ndk(
147+
api_level = 28,
148+
version = "r27c",
149+
)
150+
use_repo(android_ndk, "androidndk")
151+
152+
register_toolchains(
153+
"@androidndk//:all",
154+
dev_dependency = True,
155+
)
156+
113157
# Dev dependencies
114158
bazel_dep(name = "bazel_skylib_gazelle_plugin", version = "1.5.0", dev_dependency = True)
115159
bazel_dep(name = "gazelle", version = "0.46.0", dev_dependency = True)

doc/standalone_toolchain.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,109 @@ bazel run @rules_swift//tools/swift-releases -- list \
146146
main-snapshot-2024-08-01 --platform xcode --platform ubuntu22.04
147147
```
148148

149+
## Cross-compiling to Android with a Swift SDK
150+
151+
swift.org publishes "Swift SDK" artifact bundles (the bundles consumed by
152+
`swift sdk install`) that let the host compiler cross-compile for platforms it
153+
cannot target by itself. The `swift` extension can download the Android SDK and
154+
define matching Swift and C/C++ toolchains, so plain `swift_library` and
155+
`swift_binary` targets build for `{aarch64,x86_64}-unknown-linux-android` under
156+
`--platforms`.
157+
158+
Add the `android_sdk` tag, referencing the `toolchain` tag by name (the Swift
159+
module format is not stable across compiler versions, so the SDK is always
160+
downloaded for exactly the toolchain's version):
161+
162+
```bzl
163+
swift.toolchain(
164+
name = "swift_toolchain",
165+
swift_version = "6.3.2",
166+
)
167+
168+
swift.android_sdk(toolchain_name = "swift_toolchain")
169+
170+
# rules_swift provides only the Swift toolchain; register an Android C/C++ cc
171+
# toolchain too (it also provides the sysroot the Swift compiler reads). For
172+
# example, @androidndk//:all from hermetic_android_toolchains:
173+
android = use_extension("@hermetic_android_toolchains//:extensions.bzl", "android")
174+
android.sdk(version = "35", build_tools_version = "35.0.0")
175+
android.ndk(version = "r27c", api_level = 28)
176+
use_repo(android, "androidndk")
177+
178+
register_toolchains(
179+
"@swift_toolchain//:swift_toolchain_android_aarch64_xcode",
180+
"@swift_toolchain//:swift_toolchain_android_x86_64_xcode",
181+
"@androidndk//:all",
182+
)
183+
```
184+
185+
If you build on a single host platform you can register everything the
186+
extension generates in one line instead of listing the matrix:
187+
188+
```bzl
189+
register_toolchains("@swift_toolchain//:all")
190+
```
191+
192+
Avoid `:all` when you configure multiple Linux distributions, for the same
193+
reason the standalone host toolchains are registered explicitly: rules_swift
194+
cannot yet auto-select a distribution, so `:all` would make the host/exec
195+
toolchain ambiguous across them.
196+
197+
Then build with a platform carrying the matching constraints:
198+
199+
```bzl
200+
platform(
201+
name = "android-aarch64",
202+
constraint_values = [
203+
"@platforms//cpu:aarch64",
204+
"@platforms//os:android",
205+
],
206+
)
207+
```
208+
209+
```sh
210+
bazel build //my:binary --platforms=//:android-aarch64
211+
```
212+
213+
See `examples/cross_compilation` for a complete example, including building
214+
through a platform transition.
215+
216+
### JNI shared libraries
217+
218+
A plain `swift_binary` links an ordinary executable. To produce a JNI library
219+
loadable with `System.loadLibrary`, set `linkshared = True`: it produces
220+
`lib<name>.so`. Export functions with `@_cdecl`; the Android Swift SDK's
221+
`Android` module provides the JNI types, so the entry points can be written
222+
entirely in Swift. A `swift_binary(linkshared = True)` may depend on ordinary
223+
`swift_library` targets (linked statically), so the JNI entry point and the
224+
shared business logic stay in separate libraries.
225+
`examples/cross_compilation/android_app` shows the Kotlin app that loads the JNI
226+
library.
227+
228+
Details worth knowing:
229+
230+
* The Swift standard library is linked statically from the SDK.
231+
* Android binaries link against the NDK's `libc++_shared.so`, which must be
232+
packaged with the application. Select it from the registered Android cc
233+
toolchain with `select_android_runtime_lib` from
234+
`@rules_swift//swift/toolchains:android_runtime_lib.bzl` (build it under the
235+
Android platform so the cc toolchain resolves).
236+
* rules_swift does not fetch the Android NDK; the registered Android cc toolchain
237+
(e.g. `@androidndk//:all`) provides clang, the linker, and the sysroot, and the
238+
Swift toolchain reads that toolchain's sysroot at analysis time.
239+
* Checksums for the SDK bundles are bundled for a curated list of releases (see
240+
`swift/internal/extensions/swift_sdk_releases.bzl`); for other releases, pass
241+
`sha256` explicitly.
242+
243+
### Coexistence with `rules_apple`
244+
245+
A common setup cross-compiles to Android *and* builds the same app's Apple
246+
targets with `rules_apple`. The two resolve together cleanly: this line of
247+
`rules_swift` is `compatibility_level = 3` (the same as released `rules_swift`
248+
3.x), so a current `rules_apple` release works alongside it. Bazel keeps the
249+
higher of each shared transitive dependency (`apple_support`, `rules_cc`), which
250+
are backward compatible, so no extra pinning is required.
251+
149252
## Using the extension from a non-root module
150253

151254
The extension is intended for the root module — it fails if a non-root
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
load("//examples/embedded:transition.bzl", "transition_binary")
2+
load("//swift:swift.bzl", "swift_binary", "swift_library")
3+
4+
package(default_visibility = ["//visibility:public"])
5+
6+
# The platform covered by the Android Swift SDK toolchain registered by this
7+
# repository's dev `MODULE.bazel` (via the `swift` extension's `android_sdk`
8+
# tag). Build the targets below with `--platforms` set to this.
9+
platform(
10+
name = "android-aarch64",
11+
constraint_values = [
12+
"@platforms//cpu:aarch64",
13+
"@platforms//os:android",
14+
],
15+
)
16+
17+
# A plain library dependency, compiled for whichever platform depends on it,
18+
# demonstrating that a normal `swift_library` is reused unchanged across targets.
19+
swift_library(
20+
name = "Greeter",
21+
srcs = ["Sources/Greeter/Greeter.swift"],
22+
module_name = "Greeter",
23+
tags = ["manual"],
24+
)
25+
26+
# ---------------------------------------------------------------------------
27+
# Android: a JNI shared library, loaded by Kotlin via `System.loadLibrary`.
28+
# ---------------------------------------------------------------------------
29+
30+
# `linkshared` produces `libSwiftJNI.so`. The JNI entry point is written in
31+
# Swift (it `import`s the SDK's `Android` module for the JNI types) and calls
32+
# into the `Greeter` library. See `android_app/` for the APK that loads it.
33+
swift_binary(
34+
name = "SwiftJNI",
35+
srcs = ["Sources/SwiftJNI/SwiftJNI.swift"],
36+
linkshared = True,
37+
tags = ["manual"],
38+
deps = [":Greeter"],
39+
)
40+
41+
transition_binary(
42+
name = "libSwiftJNI.so",
43+
binary = ":SwiftJNI",
44+
platform = ":android-aarch64",
45+
)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Cross-compilation example (Android)
2+
3+
Builds plain `swift_library` / `swift_binary` targets for Android using the
4+
Swift SDK toolchain registered by this repository's `MODULE.bazel` (via the
5+
`swift` extension's `android_sdk` tag). See `doc/standalone_toolchain.md` for the
6+
toolchain setup.
7+
8+
All targets are tagged `manual` because they download the Swift SDK bundle and
9+
require the Android cc toolchain to be registered.
10+
11+
### The Android C/C++ toolchain
12+
13+
rules_swift provides only the Swift Android toolchain; C/C++ compilation and
14+
linking (and the NDK sysroot the Swift compiler reads) come from a separately
15+
registered Android cc toolchain. This example uses `@androidndk//:all` from
16+
keith's
17+
[`hermetic_android_toolchains`](https://github.com/keith/hermetic_android_toolchains),
18+
wired in this repository's `MODULE.bazel`:
19+
20+
```starlark
21+
android = use_extension("@hermetic_android_toolchains//:extensions.bzl", "android")
22+
android.sdk(version = "35", build_tools_version = "35.0.0")
23+
android.ndk(version = "r27c", api_level = 28)
24+
use_repo(android, "androidndk")
25+
register_toolchains("@androidndk//:all")
26+
```
27+
28+
Any rules_android_ndk-based Android cc toolchain works the same way. To package
29+
the NDK's `libc++_shared.so` into an APK, use the `select_android_runtime_lib`
30+
rule from `@rules_swift//swift/toolchains:android_runtime_lib.bzl` (it reads the
31+
resolved cc toolchain); see `android_app/README.md`.
32+
33+
## Targets
34+
35+
| Target | Output | Demonstrates |
36+
|---|---|---|
37+
| `:Greeter` | `.swiftmodule` + `.a` | A normal `swift_library` reused across targets |
38+
| `:libSwiftJNI.so` | `libSwiftJNI.so` | An Android **JNI shared library** (`swift_binary(linkshared)`) that calls `:Greeter` |
39+
40+
```sh
41+
# Android JNI shared library:
42+
bazel build //examples/cross_compilation:libSwiftJNI.so
43+
```
44+
45+
`:libSwiftJNI.so` builds `:SwiftJNI` under the `:android-aarch64` platform; you
46+
can equivalently pass `--platforms=//examples/cross_compilation:android-aarch64`
47+
on the command line.
48+
49+
## Android app
50+
51+
`android_app/` contains the Kotlin app (and a documented packaging recipe) that
52+
loads `libSwiftJNI.so` and calls into it, completing the
53+
Kotlin → Swift (JNI `.so`) → Swift library chain. Packaging an APK uses
54+
`rules_android` + an Android SDK in the consuming module; see
55+
`android_app/README.md`.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// A plain `swift_library` used as a normal dependency of the Android JNI
2+
/// entry point. Nothing in here is platform-specific; it is compiled for
3+
/// whichever platform the depending target is built for.
4+
public struct Greeter {
5+
private let subject: String
6+
7+
public init(subject: String) {
8+
self.subject = subject
9+
}
10+
11+
public func greeting() -> String {
12+
return "Hello from Swift, \(subject)!"
13+
}
14+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Android
2+
import Greeter
3+
4+
// The JNI entry point, written entirely in Swift. `import Android` provides the
5+
// JNI types (`JNIEnv`, `jclass`, `jstring`, ...) from the Android Swift SDK, so
6+
// no C shim is needed. `@_cdecl` gives the function the exact symbol name JNI
7+
// looks up: `Java_<package>_<class>_<method>`, with `.`/`_` escaped per the JNI
8+
// spec. It is the Kotlin-callable native implementation of:
9+
//
10+
// package com.example.swiftjni
11+
// class NativeBridge { external fun greetingFromSwift(): String }
12+
//
13+
// and it delegates to the `Greeter` `swift_library` (a normal dependency),
14+
// completing the Kotlin -> Swift (JNI .so) -> Swift library call chain.
15+
@_cdecl("Java_com_example_swiftjni_NativeBridge_greetingFromSwift")
16+
public func greetingFromSwift(
17+
_ env: UnsafeMutablePointer<JNIEnv?>,
18+
_ clazz: jclass
19+
) -> jstring? {
20+
let message = Greeter(subject: "Android").greeting()
21+
return message.withCString { cString in
22+
env.pointee!.pointee.NewStringUTF(env, cString)
23+
}
24+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="com.example.swiftjni">
4+
5+
<uses-sdk
6+
android:minSdkVersion="28"
7+
android:targetSdkVersion="34" />
8+
9+
<application android:label="Swift JNI Example">
10+
<activity
11+
android:name=".MainActivity"
12+
android:exported="true">
13+
<intent-filter>
14+
<action android:name="android.intent.action.MAIN" />
15+
<category android:name="android.intent.category.LAUNCHER" />
16+
</intent-filter>
17+
</activity>
18+
</application>
19+
</manifest>

0 commit comments

Comments
 (0)