@@ -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+
7112android {
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-
65176tasks.named(" preBuild" ) {
66- dependsOn(buildSwift )
177+ dependsOn(stageJniLibs )
67178}
68179
69180dependencies {
@@ -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)
0 commit comments