onionwire/android/sdk/build.gradle.kts
apk-dev 672f2c9990
All checks were successful
ci / test (pull_request) Successful in 3m22s
feat(android): SDK AAR (UniFFI) + Compose APK, Arti rustls feature split
Adds the Android product alongside the existing Linux TUI, in one repo.

SDK — crates/onionwire-sdk is a UniFFI facade over the very same `onionwire`
crate the TUI runs on. Identity, invite, friend upsert, send/receive, rotate
and wipe all delegate; no protocol is reimplemented, so an Android peer and a
Linux peer interoperate. It is its own Cargo workspace because Arti's TLS
backends are non-additive: the TUI keeps native-tls (OpenSSL), Android needs
rustls + static-sqlite (no OpenSSL, no system libsqlite3 in the NDK).

android/ — Gradle project. :sdk produces the AAR (Kotlin bindings generated at
build time + libonionwire_sdk.so via cargo-ndk), :app is a Kotlin/Compose/M3
messenger depending on :sdk only. minSdk 26, targetSdk/compileSdk 36,
INTERNET-only, data in filesDir, backups excluded.

Root Cargo.toml grows `native-tls` (default) and `rustls` features so exactly
one Arti TLS backend is selected per build graph. The default build is
unchanged: same backend, ratatui still a normal dependency, src/tui.rs
untouched.

Also: Store::self_fingerprint/set_petname + Node wrappers (additive only),
scripts/build-android-local.sh, README sections, .gitignore for local SDK paths.
2026-09-10 18:20:19 -04:00

163 lines
5.8 KiB
Kotlin

import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
// ---------------------------------------------------------------------------
// Rust: build crates/onionwire-sdk for the requested Android ABIs and generate
// the UniFFI Kotlin bindings from the host cdylib.
//
// Nothing here touches the Linux TUI crate's feature graph: the SDK crate is
// its own Cargo workspace that selects Arti's rustls backend.
// ---------------------------------------------------------------------------
val repoRoot: File = rootProject.projectDir.parentFile
val sdkCrateDir: File = repoRoot.resolve("crates/onionwire-sdk")
/** NDK version used for the Rust link step. Must match an installed NDK. */
val onionwireNdkVersion: String = (findProperty("onionwire.ndk") as String?)
?: "28.2.13676358"
/** Compile against this API level. Keep in sync with `minSdk`. */
val onionwireApiLevel: Int = ((findProperty("onionwire.api") as String?) ?: "26").toInt()
val androidSdkDir: File = sequenceOf(
System.getenv("ANDROID_HOME"),
System.getenv("ANDROID_SDK_ROOT"),
).filterNotNull().map(::File).firstOrNull()
?: rootProject.file("local.properties")
.takeIf { it.exists() }
?.readLines()
?.firstOrNull { it.startsWith("sdk.dir=") }
?.substringAfter('=')
?.trim()
?.let(::File)
?: error("ANDROID_HOME / ANDROID_SDK_ROOT unset and no sdk.dir in android/local.properties")
val ndkDir: File = androidSdkDir.resolve("ndk/$onionwireNdkVersion")
require(ndkDir.isDirectory) {
"NDK $onionwireNdkVersion not found at $ndkDir — run: sdkmanager --install \"ndk;$onionwireNdkVersion\""
}
val cargoExe: String = (findProperty("onionwire.cargo") as String?)
?: System.getenv("CARGO")
?: File(System.getProperty("user.home"), ".cargo/bin/cargo").takeIf { it.canExecute() }?.absolutePath
?: "cargo"
/** `-Ponionwire.abis=arm64-v8a,x86_64` to build more than one ABI. */
val onionwireAbis: List<String> = (findProperty("onionwire.abis") as String?)
?.split(',')?.map(String::trim)?.filter(String::isNotEmpty)
?: listOf("arm64-v8a")
val jniLibsDir = layout.buildDirectory.dir("rustJniLibs")
val uniffiKotlinDir = layout.buildDirectory.dir("generated/uniffi")
/** Host cdylib. UniFFI's `--library` mode dlopens it, so it must be host-arch. */
val cargoHostLib by tasks.registering(Exec::class) {
group = "onionwire"
description = "Builds libonionwire_sdk.so for the host (used only to emit bindings)."
workingDir = sdkCrateDir
environment("ANDROID_HOME", androidSdkDir.absolutePath)
commandLine(cargoExe, "build", "--release", "--lib")
}
val uniffiBindgen by tasks.registering(Exec::class) {
group = "onionwire"
description = "Generates the Kotlin UniFFI bindings into build/generated/uniffi."
dependsOn(cargoHostLib)
workingDir = sdkCrateDir
inputs.file(sdkCrateDir.resolve("src/lib.rs"))
outputs.dir(uniffiKotlinDir)
environment("ANDROID_HOME", androidSdkDir.absolutePath)
doFirst { uniffiKotlinDir.get().asFile.mkdirs() }
commandLine(
cargoExe, "run", "--release", "--bin", "uniffi-bindgen", "--",
"generate",
"--library", "target/release/libonionwire_sdk.so",
"--language", "kotlin",
"--no-format",
"--config", "uniffi.toml",
"--out-dir", uniffiKotlinDir.get().asFile.absolutePath,
)
}
val cargoBuildAndroid by tasks.registering(Exec::class) {
group = "onionwire"
description = "Cross-compiles crates/onionwire-sdk for ${onionwireAbis.joinToString()}."
workingDir = sdkCrateDir
inputs.dir(sdkCrateDir.resolve("src"))
inputs.file(sdkCrateDir.resolve("Cargo.toml"))
inputs.file(sdkCrateDir.resolve("Cargo.lock"))
inputs.dir(repoRoot.resolve("src"))
inputs.file(repoRoot.resolve("Cargo.toml"))
outputs.dir(jniLibsDir)
environment("ANDROID_HOME", androidSdkDir.absolutePath)
environment("ANDROID_NDK_HOME", ndkDir.absolutePath)
environment("NDK_HOME", ndkDir.absolutePath)
commandLine(
buildList {
add(cargoExe); add("ndk")
onionwireAbis.forEach { add("-t"); add(it) }
add("--platform"); add(onionwireApiLevel.toString())
add("-o"); add(jniLibsDir.get().asFile.absolutePath)
add("build"); add("--release"); add("--lib")
}
)
}
// ---------------------------------------------------------------------------
android {
namespace = "com.siriusdevops.onionwire.sdk"
compileSdk = 36
ndkVersion = onionwireNdkVersion
defaultConfig {
minSdk = onionwireApiLevel
consumerProguardFiles("consumer-rules.pro")
ndk {
abiFilters += onionwireAbis
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
sourceSets["main"].jniLibs.srcDir(jniLibsDir)
sourceSets["main"].kotlin.srcDir(uniffiKotlinDir)
buildTypes {
release {
isMinifyEnabled = false
}
}
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
dependencies {
// UniFFI's Kotlin backend talks to the Rust cdylib through JNA direct
// mapping, and the async bindings need coroutines. Both are part of the
// public API surface, so they are `api` — a consumer of the AAR needs them
// on its own compile classpath. JNA's `aar` variant is what carries
// libjnidispatch.so for each ABI.
api(variantOf(libs.jna) { artifactType("aar") })
api(libs.kotlinx.coroutines.core)
}
tasks.withType<KotlinCompile>().configureEach {
dependsOn(uniffiBindgen)
}
tasks.matching { it.name == "preBuild" }.configureEach {
dependsOn(cargoBuildAndroid, uniffiBindgen)
}