plugins { id("com.android.application") id("org.jetbrains.kotlin.android") } android { namespace = "app.chanora.poc.audio" compileSdk = 34 defaultConfig { applicationId = "app.chanora.poc.audio" minSdk = 24 // matches DEC-004 targetSdk = 34 versionCode = 1 versionName = "0.1.0-poc" ndk { // Keep all four common ABIs so the same APK works on both // the x86_64 emulator and a physical arm64 device. abiFilters += listOf("x86_64", "arm64-v8a", "armeabi-v7a", "x86") } } sourceSets { getByName("main") { // cargo-ndk drops .so files here. jniLibs.srcDirs("src/main/jniLibs") } } buildTypes { getByName("debug") { isMinifyEnabled = false } getByName("release") { isMinifyEnabled = false } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = "17" } packaging { // Avoid duplicate-resource conflicts from native libs if any // future deps bring them in. resources.excludes += setOf("META-INF/AL2.0", "META-INF/LGPL2.1") } } dependencies { implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.7.0") } // --------------------------------------------------------------------- // Rust cdylib build via cargo-ndk. // // Each ABI maps to a Rust target triple; cargo-ndk handles linker // selection. The task copies the produced .so into the per-ABI // jniLibs directory expected by Android Gradle Plugin. // --------------------------------------------------------------------- val rustCrateDir = rootProject.projectDir.parentFile // ../ -> the spike root val rustTargetDir = file("$rustCrateDir/target") val jniLibsDir = file("$projectDir/src/main/jniLibs") val cargoBuildRust = tasks.register("cargoBuildRust") { group = "build" description = "Builds the Rust cdylib for all enabled Android ABIs via cargo ndk." val abis = listOf( Triple("x86_64", "x86_64-linux-android", "x86_64"), Triple("arm64-v8a", "aarch64-linux-android", "arm64-v8a"), Triple("armeabi-v7a", "armv7-linux-androideabi", "armeabi-v7a"), Triple("x86", "i686-linux-android", "x86") ) doLast { // Resolve cargo: try CARGO_HOME/bin first, then PATH, then the // rustup shim location. val cargoBinDir = System.getenv("CARGO_HOME")?.let { "$it/bin" } ?: "${System.getProperty("user.home")}/.cargo/bin" val cargo = listOf("$cargoBinDir/cargo", "/usr/local/bin/cargo", "/usr/bin/cargo") .firstOrNull { file(it).exists() } ?: error("cargo not found; install via rustup") val cargoNdkBin = listOf("$cargoBinDir/cargo-ndk", "/usr/local/bin/cargo-ndk") .firstOrNull { file(it).exists() } check(cargoNdkBin != null) { "cargo-ndk not installed (cargo install cargo-ndk)" } val ndkHome = System.getenv("ANDROID_NDK_HOME") ?: System.getenv("NDK_HOME") ?: "/opt/android-sdk/ndk/26.3.11579264" check(file(ndkHome).exists()) { "NDK not found at $ndkHome" } // cargo-ndk is a cargo subcommand; invoke via `cargo ndk ...`. // We must ensure cargo's PATH lookup can find cargo-ndk too. // `-p 26` selects API 26 for the NDK toolchain so cpal can // link against libaaudio (introduced in API 26). minSdk in // the Android module stays at 24 — the device just won't // execute the AAudio code path below API 26; cpal/Oboe falls // back to OpenSL ES there. val abiArgs = abis.flatMap { listOf("-t", it.first) } val cargoCmd = listOf(cargo, "ndk", "-P", "26") + abiArgs + listOf("-o", jniLibsDir.absolutePath, "build", "--release", "--manifest-path", "$rustCrateDir/Cargo.toml") println("[cargoBuildRust] running: ${cargoCmd.joinToString(" ")}") exec { commandLine(cargoCmd) environment("ANDROID_NDK_HOME", ndkHome) environment("PATH", "$cargoBinDir:${System.getenv("PATH")}") } } } // Make Android's preBuild depend on the Rust build so AGP picks up the .so files. tasks.matching { it.name == "preBuild" }.configureEach { dependsOn(cargoBuildRust) }