app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit e31489275629cb9f98e346df9a147144bdee90e5
parent 11b6337eba8948a2f01635cbf63b8cfdd59a3cf8
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 19:44:56 +0000

build(desktop): generate UniFFI Kotlin bindings

- run the pinned UniFFI generator from the Rust workspace
- place generated Kotlin under ignored desktop build output
- compile the generated package with its pinned JNA runtime
- avoid generated close and exception-name collisions

Diffstat:
Mapp/desktop/build.gradle.kts | 40++++++++++++++++++++++++++++++++++++++++
Mcore/Cargo.lock | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/ffi/src/commands.rs | 12++++++------
Mcore/crates/ffi/src/observer.rs | 14+++++++-------
Mcore/crates/ffi/uniffi.toml | 2+-
Mcore/tools/uniffi-bindgen/Cargo.toml | 3+++
Mcore/tools/uniffi-bindgen/src/main.rs | 4+++-
Mdocs/architecture/rust-ffi-boundary.md | 6++++++
Mdocs/implementation/nostr-runtime-rcld.md | 2+-
Mgradle/libs.versions.toml | 2++
10 files changed, 125 insertions(+), 16 deletions(-)

diff --git a/app/desktop/build.gradle.kts b/app/desktop/build.gradle.kts @@ -47,9 +47,41 @@ val buildRustCore by tasks.registering(Exec::class) { outputs.file(rustDebugLibrary) } +val generatedUniFfiKotlin = layout.buildDirectory.dir("generated/uniffi/kotlin") +val generateUniFfiKotlin by tasks.registering(Exec::class) { + dependsOn(buildRustCore) + workingDir(rootProject.projectDir) + commandLine( + "cargo", + "run", + "--manifest-path", + rustManifest.asFile.absolutePath, + "-p", + "radroots-studio-uniffi-bindgen", + "--", + "generate", + "--library", + "--language", + "kotlin", + "--metadata-no-deps", + "--no-format", + "--config", + rootProject.layout.projectDirectory + .file("core/crates/ffi/uniffi.toml") + .asFile.absolutePath, + "--out-dir", + generatedUniFfiKotlin.get().asFile.absolutePath, + rustDebugLibrary.asFile.absolutePath, + ) + inputs.file(rustDebugLibrary) + inputs.file(rootProject.layout.projectDirectory.file("core/crates/ffi/uniffi.toml")) + outputs.dir(generatedUniFfiKotlin) +} + dependencies { implementation(compose.desktop.currentOs) implementation(libs.compose.foundation) + implementation(libs.jna) testImplementation(kotlin("test-junit")) testImplementation(libs.compose.ui.test.junit4) @@ -87,6 +119,10 @@ tasks.withType<Test>().configureEach { } kotlin { + sourceSets.main { + kotlin.srcDir(generatedUniFfiKotlin) + } + compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } @@ -94,6 +130,10 @@ kotlin { jvmToolchain(21) } +tasks.named("compileKotlin") { + dependsOn(generateUniFfiKotlin) +} + compose.desktop { application { mainClass = "org.radroots.studio.desktop.MainKt" diff --git a/core/Cargo.lock b/core/Cargo.lock @@ -48,6 +48,12 @@ dependencies = [ ] [[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] name = "anyhow" version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -542,6 +548,45 @@ dependencies = [ ] [[package]] +name = "clap" +version = "4.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1719,6 +1764,9 @@ dependencies = [ [[package]] name = "radroots-studio-uniffi-bindgen" version = "0.1.0-alpha.0" +dependencies = [ + "uniffi", +] [[package]] name = "rand" @@ -2255,6 +2303,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2617,7 +2671,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a782a48d72cfd7a2d65cfc7c691dbf5375c43104b3c195f7eccc716dcc3540c8" dependencies = [ "anyhow", + "camino", "cargo_metadata", + "clap", "uniffi_bindgen", "uniffi_core", "uniffi_macros", diff --git a/core/crates/ffi/src/commands.rs b/core/crates/ffi/src/commands.rs @@ -17,13 +17,13 @@ use crate::{AccountDto, AppSnapshotDto}; #[derive(Debug, uniffi::Error)] pub enum StudioError { - Failure { code: String, message: String }, + Failure { code: String, safe_message: String }, } impl Display for StudioError { fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { match self { - Self::Failure { message, .. } => formatter.write_str(message), + Self::Failure { safe_message, .. } => formatter.write_str(safe_message), } } } @@ -34,7 +34,7 @@ impl From<SafeError> for StudioError { fn from(error: SafeError) -> Self { Self::Failure { code: format!("{:?}", error.code()), - message: error.message().as_str().to_owned(), + safe_message: error.message().as_str().to_owned(), } } } @@ -339,21 +339,21 @@ fn runtime() -> &'static tokio::runtime::Runtime { fn path_unavailable() -> StudioError { StudioError::Failure { code: "StorageUnavailable".to_owned(), - message: "The application data directory is unavailable.".to_owned(), + safe_message: "The application data directory is unavailable.".to_owned(), } } fn runtime_unavailable() -> StudioError { StudioError::Failure { code: "InvalidApplicationState".to_owned(), - message: "The application runtime is unavailable.".to_owned(), + safe_message: "The application runtime is unavailable.".to_owned(), } } fn confirmation_expired() -> StudioError { StudioError::Failure { code: "InvalidApplicationState".to_owned(), - message: "The account removal confirmation is no longer valid.".to_owned(), + safe_message: "The account removal confirmation is no longer valid.".to_owned(), } } diff --git a/core/crates/ffi/src/observer.rs b/core/crates/ffi/src/observer.rs @@ -29,7 +29,7 @@ pub struct ObserverSubscription { #[uniffi::export] impl ObserverSubscription { - pub fn close(&self) { + pub fn unsubscribe(&self) { let handle = self .handle .lock() @@ -48,7 +48,7 @@ impl ObserverSubscription { impl Drop for ObserverSubscription { fn drop(&mut self) { - self.close(); + self.unsubscribe(); } } @@ -86,7 +86,7 @@ impl StudioAppCore { })) } - pub fn close(&self) { + pub fn shutdown(&self) { if self.inner.closed.swap(true, Ordering::AcqRel) { return; } @@ -107,7 +107,7 @@ impl StudioAppCore { fn closed_error() -> StudioError { StudioError::Failure { code: "InvalidApplicationState".to_owned(), - message: "The application runtime is closed.".to_owned(), + safe_message: "The application runtime is closed.".to_owned(), } } @@ -168,7 +168,7 @@ mod tests { .bootstrap() .expect("idempotent bootstrap"); assert_eq!(observer.snapshots.lock().expect("snapshots").len(), 1); - subscription.close(); + subscription.unsubscribe(); core.inner.adapter.core().sign_out().expect("sign out"); assert_eq!(observer.snapshots.lock().expect("snapshots").len(), 1); } @@ -182,8 +182,8 @@ mod tests { .subscribe(Box::new(ArcObserver(observer.clone()))) .expect("subscribe"); - core.close(); - core.close(); + core.shutdown(); + core.shutdown(); assert!(core.subscribe(Box::new(ArcObserver(observer))).is_err()); assert!(core.inner.observers.lock().expect("observers").is_empty()); diff --git a/core/crates/ffi/uniffi.toml b/core/crates/ffi/uniffi.toml @@ -1,3 +1,3 @@ -[bindings.kotlin] +[crates.radroots_studio_ffi.bindings.kotlin] package_name = "org.radroots.studio.ffi" cdylib_name = "radroots_studio_ffi" diff --git a/core/tools/uniffi-bindgen/Cargo.toml b/core/tools/uniffi-bindgen/Cargo.toml @@ -7,5 +7,8 @@ license.workspace = true repository.workspace = true publish = false +[dependencies] +uniffi = { workspace = true, features = ["cli"] } + [lints] workspace = true diff --git a/core/tools/uniffi-bindgen/src/main.rs b/core/tools/uniffi-bindgen/src/main.rs @@ -1,6 +1,8 @@ #![doc = "Pinned `UniFFI` binding generator entry point."] -fn main() {} +fn main() { + uniffi::uniffi_bindgen_main(); +} #[cfg(test)] mod tests { diff --git a/docs/architecture/rust-ffi-boundary.md b/docs/architecture/rust-ffi-boundary.md @@ -44,3 +44,9 @@ Gradle exposes the same operation as `:app:desktop:buildRustCore`. The task tracks Rust manifests, lockfile, sources, and migrations as inputs and the current-host debug dynamic library as its output. It does not redirect Cargo's target directory. + +`:app:desktop:generateUniFfiKotlin` runs the repository-pinned UniFFI 0.32 +generator against that dynamic library and writes Kotlin to +`app/desktop/build/generated/uniffi/kotlin`. The main Kotlin source set reads +that generated directory, and `compileKotlin` depends on generation. Generated +bindings are ignored build output and are never committed. diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md @@ -526,7 +526,7 @@ handoff commit sequence. - [x] 43. Expose observer callback and deregistration through UniFFI. - [x] 44. Build native library artifacts from Cargo. - [x] 45. Add Gradle task for Cargo build. -- [ ] 46. Add Gradle UniFFI binding generation and generated source set. +- [x] 46. Add Gradle UniFFI binding generation and generated source set. - [ ] 47. Stage native library for development, tests, and packaged app. ### RCLD-10 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml @@ -1,10 +1,12 @@ [versions] kotlin = "2.4.10" compose = "1.11.1" +jna = "5.17.0" [libraries] compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose" } compose-ui-test-junit4 = { module = "org.jetbrains.compose.ui:ui-test-junit4", version.ref = "compose" } +jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }