commit 562472657ea5ab525d9771df5e3a9d86ccbc606a
parent e31489275629cb9f98e346df9a147144bdee90e5
Author: triesap <tyson@radroots.org>
Date: Sun, 2 Aug 2026 19:47:04 +0000
build(desktop): stage the native runtime
- place the current-host library under JNA platform resources
- configure development and test native lookup paths
- verify generated Kotlin crosses the loaded native ABI
- include the library in the current-host application bundle
Diffstat:
5 files changed, 68 insertions(+), 3 deletions(-)
diff --git a/app/desktop/build.gradle.kts b/app/desktop/build.gradle.kts
@@ -32,6 +32,16 @@ val rustLibraryName = when {
}
val rustDebugLibrary = rootProject.layout.projectDirectory
.file("core/target/debug/$rustLibraryName")
+val jnaPlatformPrefix = when {
+ System.getProperty("os.name").startsWith("Mac") &&
+ System.getProperty("os.arch") == "aarch64" -> "darwin-aarch64"
+ System.getProperty("os.name").startsWith("Mac") -> "darwin-x86-64"
+ System.getProperty("os.name").startsWith("Windows") &&
+ System.getProperty("os.arch") == "aarch64" -> "win32-aarch64"
+ System.getProperty("os.name").startsWith("Windows") -> "win32-x86-64"
+ System.getProperty("os.arch") == "aarch64" -> "linux-aarch64"
+ else -> "linux-x86-64"
+}
val buildRustCore by tasks.registering(Exec::class) {
workingDir(rootProject.projectDir)
@@ -48,6 +58,7 @@ val buildRustCore by tasks.registering(Exec::class) {
}
val generatedUniFfiKotlin = layout.buildDirectory.dir("generated/uniffi/kotlin")
+val generatedNativeResources = layout.buildDirectory.dir("generated/uniffi/native-resources")
val generateUniFfiKotlin by tasks.registering(Exec::class) {
dependsOn(buildRustCore)
workingDir(rootProject.projectDir)
@@ -77,6 +88,11 @@ val generateUniFfiKotlin by tasks.registering(Exec::class) {
inputs.file(rootProject.layout.projectDirectory.file("core/crates/ffi/uniffi.toml"))
outputs.dir(generatedUniFfiKotlin)
}
+val stageUniFfiNativeLibrary by tasks.registering(Copy::class) {
+ dependsOn(buildRustCore)
+ from(rustDebugLibrary)
+ into(generatedNativeResources.map { it.dir(jnaPlatformPrefix) })
+}
dependencies {
implementation(compose.desktop.currentOs)
@@ -121,6 +137,7 @@ tasks.withType<Test>().configureEach {
kotlin {
sourceSets.main {
kotlin.srcDir(generatedUniFfiKotlin)
+ resources.srcDir(generatedNativeResources)
}
compilerOptions {
@@ -133,6 +150,23 @@ kotlin {
tasks.named("compileKotlin") {
dependsOn(generateUniFfiKotlin)
}
+tasks.named("processResources") {
+ dependsOn(stageUniFfiNativeLibrary)
+}
+tasks.withType<Test>().configureEach {
+ dependsOn(buildRustCore)
+ systemProperty(
+ "jna.library.path",
+ rustDebugLibrary.asFile.parentFile.absolutePath,
+ )
+}
+tasks.withType<JavaExec>().configureEach {
+ dependsOn(buildRustCore)
+ systemProperty(
+ "jna.library.path",
+ rustDebugLibrary.asFile.parentFile.absolutePath,
+ )
+}
compose.desktop {
application {
diff --git a/app/desktop/src/test/kotlin/org/radroots/studio/ffi/NativeLoaderTest.kt b/app/desktop/src/test/kotlin/org/radroots/studio/ffi/NativeLoaderTest.kt
@@ -0,0 +1,11 @@
+package org.radroots.studio.ffi
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class NativeLoaderTest {
+ @Test
+ fun generatedBindingLoadsTheCurrentHostLibrary() {
+ assertEquals("0.1.0-alpha.0", nativeRuntimeVersion())
+ }
+}
diff --git a/core/crates/ffi/src/lib.rs b/core/crates/ffi/src/lib.rs
@@ -12,3 +12,17 @@ pub use dto::{
pub use observer::{ObserverSubscription, StudioObserver};
uniffi::setup_scaffolding!();
+
+#[uniffi::export]
+#[must_use]
+pub fn native_runtime_version() -> String {
+ env!("CARGO_PKG_VERSION").to_owned()
+}
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn native_runtime_reports_the_crate_version() {
+ assert_eq!(super::native_runtime_version(), "0.1.0-alpha.0");
+ }
+}
diff --git a/docs/architecture/rust-ffi-boundary.md b/docs/architecture/rust-ffi-boundary.md
@@ -50,3 +50,9 @@ 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.
+
+Development and tests point JNA at `core/target/debug`. Packaging stages the
+current-host library under JNA's platform resource prefix inside the desktop
+resources, allowing the packaged JVM runtime to extract and load it without a
+machine-specific absolute path. `NativeLoaderTest` crosses the generated ABI
+and verifies the native crate version without opening storage or credentials.
diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md
@@ -378,7 +378,7 @@ async stale-result/cancellation tests, profile cache tests, and workspace tests.
### RCLD-09: UniFFI and native build integration
-Status: pending.
+Status: completed.
Scope: checkpoints 41 through 47. Add UniFFI definitions and safe DTO mapping,
expose supported AppCore commands, implement observer handles, build the native
@@ -527,7 +527,7 @@ handoff commit sequence.
- [x] 44. Build native library artifacts from Cargo.
- [x] 45. Add Gradle task for Cargo build.
- [x] 46. Add Gradle UniFFI binding generation and generated source set.
-- [ ] 47. Stage native library for development, tests, and packaged app.
+- [x] 47. Stage native library for development, tests, and packaged app.
### RCLD-10
@@ -565,7 +565,7 @@ handoff commit sequence.
- [x] RCLD-06: Account generation and import.
- [x] RCLD-07: Account lifecycle and recovery.
- [x] RCLD-08: Relay and profile runtime.
-- [ ] RCLD-09: UniFFI and native build integration.
+- [x] RCLD-09: UniFFI and native build integration.
- [ ] RCLD-10: Thin Kotlin shell and minimal UI.
- [ ] RCLD-11: End-to-end integration hardening.
- [ ] RCLD-12: Documentation and acceptance reconciliation.