First Android run failed at plugin resolution: Plugin [id: 'io.gitlab.arturbosch.detekt', version: '2.0.0-alpha.3'] was not found in any of the following sources That version is published to neither Maven Central nor the plugin portal — the latest detekt anywhere is 1.23.8. It was copied from Minstrel's catalog, where it presumably resolves from a cached artifact; copying a pin without checking it exists is what made it my problem. Rather than chase a working plugin version, the analyzers now run from the CLIs ci-rust-android already ships. That was the point of putting them in the image in step 3, and going through Gradle plugins would have meant a SECOND pinned version of each tool, resolved at build time, kept in lockstep with the image's by hand. One less resolution step, and step 3's decision finally earns its keep. Also replaces the source-ordering hack while here. Kotlin has to compile after the bindings are generated, and the usual `tasks.withType<KotlinCompile>` cannot be written in this build at all — AGP 9's built-in Kotlin means that class is not on the buildscript classpath. Passing the TASK PROVIDERS to srcDir instead lets Gradle read their @OutputDirectory and infer the ordering itself, which is the idiomatic form and removes the dependsOn entirely. Good news from the failed run: the Gradle wrapper check passed, so Gradle 9.1.0 on the image's JDK 25 works — the toolchain decision from step 3 holds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
229 lines
7.8 KiB
Kotlin
229 lines
7.8 KiB
Kotlin
import javax.inject.Inject
|
|
|
|
plugins {
|
|
alias(libs.plugins.android.application)
|
|
alias(libs.plugins.compose.compiler)
|
|
}
|
|
|
|
// The Cargo workspace root — two levels up from android/app.
|
|
val workspaceRoot: Directory = layout.projectDirectory.dir("../..")
|
|
|
|
// The ABIs a release APK carries. arm64 is essentially every real device; armv7
|
|
// covers older 32-bit hardware; the two x86 targets are what emulators run on, and
|
|
// dropping them would make the app untestable on a desktop emulator (the reason
|
|
// x86_64 was added to the old Tauri lane in task 1864).
|
|
val androidAbis = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
|
|
|
|
/**
|
|
* Cross-compile `thoughtsync-ffi` for each Android ABI and drop the resulting
|
|
* `.so` into jniLibs, where AGP packages it.
|
|
*
|
|
* `ExecOperations` injected rather than `project.exec`: the latter was REMOVED in
|
|
* Gradle 9, and reaching for `project` at execution time is also what breaks the
|
|
* configuration cache this build has enabled.
|
|
*/
|
|
abstract class CargoNdkBuild : DefaultTask() {
|
|
@get:Inject
|
|
abstract val execOps: ExecOperations
|
|
|
|
@get:InputFiles
|
|
abstract val rustSources: ConfigurableFileCollection
|
|
|
|
@get:Input
|
|
abstract val abis: ListProperty<String>
|
|
|
|
@get:Input
|
|
abstract val cargoProfile: Property<String>
|
|
|
|
@get:Internal
|
|
abstract val workspaceDir: DirectoryProperty
|
|
|
|
@get:OutputDirectory
|
|
abstract val jniLibsDir: DirectoryProperty
|
|
|
|
@TaskAction
|
|
fun build() {
|
|
val args = mutableListOf("ndk")
|
|
abis.get().forEach { abi ->
|
|
args += "-t"
|
|
args += abi
|
|
}
|
|
args += listOf("-o", jniLibsDir.get().asFile.absolutePath, "build", "-p", "thoughtsync-ffi")
|
|
// --locked so an Android build cannot silently re-resolve the workspace
|
|
// lockfile the desktop lanes are gated on.
|
|
args += "--locked"
|
|
if (cargoProfile.get() == "release") args += "--release"
|
|
|
|
execOps.exec {
|
|
commandLine(listOf("cargo") + args)
|
|
workingDir = workspaceDir.get().asFile
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate the Kotlin bindings FROM the freshly built `.so`.
|
|
*
|
|
* `--library` mode reads uniffi's metadata straight out of the compiled artifact,
|
|
* so the bindings can never describe a different version of the Rust than the one
|
|
* being packaged — which is the failure the whole in-workspace generator setup
|
|
* exists to prevent.
|
|
*/
|
|
abstract class UniffiBindgen : DefaultTask() {
|
|
@get:Inject
|
|
abstract val execOps: ExecOperations
|
|
|
|
@get:InputFile
|
|
abstract val libraryFile: RegularFileProperty
|
|
|
|
@get:Internal
|
|
abstract val workspaceDir: DirectoryProperty
|
|
|
|
@get:OutputDirectory
|
|
abstract val outputDir: DirectoryProperty
|
|
|
|
@TaskAction
|
|
fun generate() {
|
|
val out = outputDir.get().asFile
|
|
out.deleteRecursively()
|
|
out.mkdirs()
|
|
execOps.exec {
|
|
commandLine(
|
|
"cargo",
|
|
"run",
|
|
"--locked",
|
|
"--features",
|
|
"bindgen",
|
|
"--bin",
|
|
"uniffi-bindgen",
|
|
"--",
|
|
"generate",
|
|
"--library",
|
|
libraryFile.get().asFile.absolutePath,
|
|
"--language",
|
|
"kotlin",
|
|
"--out-dir",
|
|
out.absolutePath,
|
|
)
|
|
workingDir = workspaceDir.get().asFile
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only the Rust that actually affects the .so. Deliberately NOT the workspace
|
|
// directory: that would make Gradle hash target/, which is gigabytes.
|
|
val rustInputs =
|
|
files(
|
|
workspaceRoot.dir("core/src"),
|
|
workspaceRoot.dir("android/ffi/src"),
|
|
workspaceRoot.file("core/Cargo.toml"),
|
|
workspaceRoot.file("android/ffi/Cargo.toml"),
|
|
workspaceRoot.file("Cargo.toml"),
|
|
workspaceRoot.file("Cargo.lock"),
|
|
)
|
|
|
|
val jniLibsOut = layout.buildDirectory.dir("rustJniLibs")
|
|
val bindingsOut = layout.buildDirectory.dir("generated/uniffi")
|
|
|
|
val cargoNdkDebug =
|
|
tasks.register<CargoNdkBuild>("cargoNdkDebug") {
|
|
description = "Cross-compile thoughtsync-ffi for the Android ABIs (debug)."
|
|
rustSources.from(rustInputs)
|
|
abis.set(androidAbis)
|
|
cargoProfile.set("debug")
|
|
workspaceDir.set(workspaceRoot)
|
|
jniLibsDir.set(jniLibsOut)
|
|
}
|
|
|
|
val generateBindings =
|
|
tasks.register<UniffiBindgen>("generateUniffiBindings") {
|
|
description = "Generate the Kotlin bindings from the compiled .so."
|
|
dependsOn(cargoNdkDebug)
|
|
// arm64 is arbitrary — every ABI carries the same uniffi metadata, and
|
|
// reading one is cheaper than reading four.
|
|
libraryFile.set(jniLibsOut.map { it.file("arm64-v8a/libthoughtsync_ffi.so") })
|
|
workspaceDir.set(workspaceRoot)
|
|
outputDir.set(bindingsOut)
|
|
}
|
|
|
|
android {
|
|
namespace = "com.fabledsword.thoughtsync"
|
|
compileSdk = 36
|
|
|
|
defaultConfig {
|
|
applicationId = "com.fabledsword.thoughtsync"
|
|
// 26 (Android 8, 2017) matches Minstrel and clears the NDK's floor with
|
|
// room to spare.
|
|
minSdk = 26
|
|
targetSdk = 36
|
|
// Injected by CI from the git tag + commit count for a release; "dev"
|
|
// locally so the About screen reads honestly rather than claiming 1.0.
|
|
val nameOverride =
|
|
(project.findProperty("THOUGHTSYNC_VERSION_NAME") as String?)?.takeIf { it.isNotBlank() }
|
|
val codeOverride =
|
|
(project.findProperty("THOUGHTSYNC_VERSION_CODE") as String?)?.toIntOrNull()
|
|
versionCode = codeOverride ?: 1
|
|
versionName = nameOverride ?: "dev"
|
|
}
|
|
|
|
buildTypes {
|
|
release {
|
|
isMinifyEnabled = false
|
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
|
// Signing is deliberately absent. A release keystore that has passed
|
|
// through an agent session or shell history is compromised by
|
|
// construction (Scribe task 2136) — it has to be generated by the
|
|
// operator and reach CI only as a secret. Until then a release build
|
|
// is unsigned and CI builds debug.
|
|
}
|
|
}
|
|
|
|
compileOptions {
|
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
targetCompatibility = JavaVersion.VERSION_17
|
|
}
|
|
|
|
buildFeatures {
|
|
compose = true
|
|
}
|
|
|
|
sourceSets {
|
|
getByName("main") {
|
|
// The .so and the bindings are build outputs, not checked-in sources.
|
|
// Passing the TASK PROVIDERS, not plain paths: Gradle reads each
|
|
// task's @OutputDirectory and infers the build ordering itself. A
|
|
// bare path would compile Kotlin before the bindings exist, and the
|
|
// usual fix — depending on KotlinCompile by type — cannot be written
|
|
// here, because AGP 9's built-in Kotlin means that class is not on
|
|
// the buildscript classpath.
|
|
jniLibs.srcDir(cargoNdkDebug)
|
|
java.srcDir(generateBindings)
|
|
}
|
|
}
|
|
|
|
packaging {
|
|
resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
|
}
|
|
}
|
|
|
|
dependencies {
|
|
implementation(libs.androidx.core.ktx)
|
|
implementation(libs.androidx.activity.compose)
|
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
|
implementation(libs.kotlinx.coroutines.android)
|
|
|
|
// Required by the uniffi bindings — see the catalog note on the @aar
|
|
// classifier; the plain jar builds fine and fails at runtime.
|
|
implementation(variantOf(libs.jna) { artifactType("aar") })
|
|
|
|
implementation(platform(libs.compose.bom))
|
|
implementation(libs.compose.ui)
|
|
implementation(libs.compose.ui.graphics)
|
|
implementation(libs.compose.material3)
|
|
implementation(libs.compose.ui.tooling.preview)
|
|
debugImplementation(libs.compose.ui.tooling)
|
|
|
|
testImplementation(libs.junit)
|
|
}
|