Skip to content

Commit cef1ced

Browse files
committed
Drive Swift cross-compilation and jniLibs staging from Gradle
The jextract task cross-compiles the package graph with the Swift Android SDK (--disable-sandbox so the swift-java callbacks sub-build can run Gradle), generated Java bindings and a patched SwiftKitCore source tree are added to the source set, and all required .so files are staged into jniLibs. The Unsigned annotation is replaced with an Android-compatible copy since the upstream source depends on jdk.jfr.
1 parent f800bfe commit cef1ced

2 files changed

Lines changed: 156 additions & 11 deletions

File tree

‎Demo/app/build.gradle.kts‎

Lines changed: 120 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,111 @@ plugins {
44
alias(libs.plugins.kotlin.compose)
55
}
66

7+
// ---------------------------------------------------------------------------
8+
// swift-java (jextract, JNI mode) integration
9+
//
10+
// The Swift package graph (AndroidBluetooth + AndroidBluetoothBridge +
11+
// BluetoothDemoBridge) is cross-compiled to native `.so`s with the official
12+
// Swift Android SDK; the `JExtractSwiftPlugin` emits the Java JNI bindings as
13+
// a side effect of `swift build`. Gradle then adds the generated Java +
14+
// SwiftKitCore runtime sources to the source set and stages every required
15+
// `.so` into `jniLibs`.
16+
// ---------------------------------------------------------------------------
17+
18+
val swiftPackageRoot: File = rootProject.projectDir
19+
val androidTriple = "aarch64-unknown-linux-android28"
20+
val swiftAbi = "arm64-v8a"
21+
val userHome: String = System.getProperty("user.home")
22+
23+
// Configuration the Swift package is cross-compiled with, independent of the
24+
// Android build type. Override with `-PswiftBuildConfig=release`.
25+
val swiftBuildConfig: String = (findProperty("swiftBuildConfig") as String?) ?: "debug"
26+
27+
// Locates both the toolchain and the matching Android SDK artifactbundle.
28+
val swiftToolchainVersion: String = (findProperty("swiftToolchainVersion") as String?) ?: "6.3.3"
29+
30+
// The Android SDK's prebuilt Swift modules require the matching swift.org
31+
// toolchain. Override with `-PswiftBin=/path/to/swift` if installed elsewhere.
32+
val swiftBin: String = (findProperty("swiftBin") as String?)
33+
?: "$userHome/Library/Developer/Toolchains/swift-$swiftToolchainVersion-RELEASE.xctoolchain/usr/bin/swift"
34+
35+
// swift-java's `enableJavaCallbacks` feature runs its own internal Gradle
36+
// sub-build to compile the generated Java callback interfaces, requiring a
37+
// modern JDK — independent of whatever JDK runs *this* Gradle build.
38+
// Override with `-PcallbacksJdkHome=/path/to/jdk`.
39+
val callbacksJdkHome: String = (findProperty("callbacksJdkHome") as String?)
40+
?: "/opt/homebrew/opt/openjdk@25"
41+
42+
// SwiftPM plugin output convention: `outputs/<package-dir-lowercased>/<TargetName>/...`.
43+
// Both bridge targets generate Java: the AndroidBluetooth library's callback
44+
// bridge and the demo's UI-facing bridge.
45+
val generatedLibraryJavaDir = File(swiftPackageRoot, ".build/plugins/outputs/androidbluetooth/AndroidBluetoothBridge/destination/JExtractSwiftPlugin/src/generated/java")
46+
val generatedDemoJavaDir = File(swiftPackageRoot, ".build/plugins/outputs/demo/BluetoothDemoBridge/destination/JExtractSwiftPlugin/src/generated/java")
47+
val swiftKitCoreDir = File(swiftPackageRoot, ".build/checkouts/swift-java/SwiftKitCore/src/main/java")
48+
val swiftBuildDir = File(swiftPackageRoot, ".build/$androidTriple/$swiftBuildConfig")
49+
val swiftAndroidRuntimeDir = File(
50+
(findProperty("swiftAndroidRuntimeDir") as String?)
51+
?: "$userHome/Library/org.swift.swiftpm/swift-sdks/swift-${swiftToolchainVersion}-RELEASE_android.artifactbundle/swift-android/swift-resources/usr/lib/swift-aarch64/android"
52+
)
53+
54+
// Supplies libc++_shared.so, which every Swift Android binary links against.
55+
val swiftAndroidSysroot = File(
56+
(findProperty("swiftAndroidSysroot") as String?)
57+
?: "$userHome/Library/org.swift.swiftpm/swift-sdks/swift-${swiftToolchainVersion}-RELEASE_android.artifactbundle/swift-android/ndk-sysroot"
58+
)
59+
60+
// Cross-compile the Swift package + generate the JNI Java bindings.
61+
val jextract = tasks.register<Exec>("jextract") {
62+
workingDir = swiftPackageRoot
63+
environment("JAVA_HOME", callbacksJdkHome)
64+
commandLine(
65+
swiftBin, "build",
66+
"--swift-sdk", androidTriple,
67+
"-c", swiftBuildConfig,
68+
"--disable-sandbox"
69+
)
70+
outputs.dir(generatedLibraryJavaDir)
71+
outputs.dir(generatedDemoJavaDir)
72+
outputs.file(File(swiftBuildDir, "libSwiftAndroidApp.so"))
73+
// `swift build` is incremental itself; let it decide what is stale.
74+
outputs.upToDateWhen { false }
75+
}
76+
77+
// SwiftKitCore is consumed as source (swift-java is not published to Maven),
78+
// minus two annotations that pull in `jdk.jfr` (Java Flight Recorder), which
79+
// is unavailable on Android. `Unsigned` (which the generated bindings DO
80+
// reference) is replaced by an Android-compatible copy in `src/main/java`.
81+
val patchedSwiftKitCoreDir = layout.buildDirectory.dir("swiftkitcore-java")
82+
val stageSwiftKitCore = tasks.register<Sync>("stageSwiftKitCore") {
83+
dependsOn(jextract)
84+
from(swiftKitCoreDir)
85+
exclude(
86+
"org/swift/swiftkit/core/annotations/ThreadSafe.java",
87+
"org/swift/swiftkit/core/annotations/Unsigned.java"
88+
)
89+
into(patchedSwiftKitCoreDir)
90+
}
91+
92+
// Stage the cross-compiled libraries, the swift-java runtime, the Swift
93+
// Android runtime and libc++_shared into jniLibs so they end up in the APK.
94+
val stageJniLibs = tasks.register<Copy>("stageJniLibs") {
95+
dependsOn(jextract)
96+
into(layout.projectDirectory.dir("src/main/jniLibs/$swiftAbi"))
97+
from(swiftBuildDir) {
98+
// Every dynamic library product built by the package graph — anything
99+
// less leaves a dangling `dlopen` at runtime.
100+
include("*.so")
101+
}
102+
from(swiftAndroidRuntimeDir) {
103+
include("*.so")
104+
// Test-only runtime libraries are not needed by the app.
105+
exclude("*Testing*", "libXCTest.so")
106+
}
107+
from(File(swiftAndroidSysroot, "usr/lib/aarch64-linux-android")) {
108+
include("libc++_shared.so")
109+
}
110+
}
111+
7112
android {
8113
namespace = "com.pureswift.swiftandroid"
9114
compileSdk = 35
@@ -16,7 +121,7 @@ android {
16121
versionName = "1.0"
17122
ndk {
18123
//noinspection ChromeOsAbiSupport
19-
abiFilters += listOf("arm64-v8a")
124+
abiFilters += listOf(swiftAbi)
20125
}
21126
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
22127
}
@@ -40,6 +145,12 @@ android {
40145
buildFeatures {
41146
compose = true
42147
}
148+
149+
// Generated JNI bindings + the (patched) SwiftKitCore Java runtime.
150+
sourceSets["main"].java.srcDir(generatedLibraryJavaDir)
151+
sourceSets["main"].java.srcDir(generatedDemoJavaDir)
152+
sourceSets["main"].java.srcDir(patchedSwiftKitCoreDir)
153+
43154
packaging {
44155
resources {
45156
excludes += listOf("/META-INF/{AL2.0,LGPL2.1}")
@@ -54,16 +165,16 @@ android {
54165
}
55166
}
56167

57-
// Compile native Swift code for the demo app with `skip android build`.
58-
val buildSwift by tasks.registering(Exec::class) {
59-
group = "build"
60-
description = "Build native Swift sources for Android"
61-
workingDir(rootProject.projectDir)
62-
commandLine("bash", "build-swift.sh")
168+
// Ensure Swift is built + bindings generated + libs staged before Java or
169+
// Kotlin compiles (Kotlin also consumes the generated Java bindings).
170+
tasks.withType<JavaCompile>().configureEach {
171+
dependsOn(jextract, stageSwiftKitCore)
172+
}
173+
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
174+
dependsOn(jextract, stageSwiftKitCore)
63175
}
64-
65176
tasks.named("preBuild") {
66-
dependsOn(buildSwift)
177+
dependsOn(stageJniLibs)
67178
}
68179

69180
dependencies {
@@ -75,8 +186,6 @@ dependencies {
75186
implementation(libs.androidx.ui)
76187
implementation(libs.androidx.ui.graphics)
77188
implementation(libs.androidx.ui.tooling.preview)
78-
implementation(libs.androidx.recyclerview)
79-
implementation(libs.androidx.navigation.runtime)
80189
implementation(libs.androidx.material3)
81190
implementation(libs.material)
82191
testImplementation(libs.junit)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
package org.swift.swiftkit.core.annotations;
16+
17+
import java.lang.annotation.Documented;
18+
import java.lang.annotation.Retention;
19+
import java.lang.annotation.RetentionPolicy;
20+
import java.lang.annotation.Target;
21+
22+
import static java.lang.annotation.ElementType.*;
23+
24+
/**
25+
* Value is of an unsigned numeric type.
26+
* <p>
27+
* Android-compatible copy of SwiftKitCore's {@code Unsigned} annotation: the
28+
* upstream source carries {@code jdk.jfr} annotations that are unavailable on
29+
* Android, so the original is excluded from the source set and replaced by
30+
* this one.
31+
*/
32+
@Documented
33+
@Target({TYPE_USE, PARAMETER, FIELD, METHOD})
34+
@Retention(RetentionPolicy.RUNTIME)
35+
public @interface Unsigned {
36+
}

0 commit comments

Comments
 (0)