5d0de7a6823a1c6a9b1d236cc8d02b7ea2354722
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5d0de7a682 |
android: the binding generator gets its own crate, free of the app's deps
Third Android run got further than either before it — all four ABIs cross-compiled, vendored OpenSSL and all — then the generator died: error: failed to run custom build command for `openssl-sys v0.9.117` That is the HOST build. The generator was a [[bin]] inside thoughtsync-ffi, so building it compiled that crate and therefore the core, reqwest, native-tls and openssl-sys for linux. The vendored-OpenSSL block is scoped to `cfg(target_os = "android")`, so the host build went looking for a system OpenSSL that ci-rust-android has no reason to carry. Adding libssl-dev to the image would have fixed it and been wrong: a code generator has no business linking the app's TLS stack to emit Kotlin. Splitting it into thoughtsync-uniffi-bindgen, whose only dependency is uniffi, removes the entire chain. Verified from the dependency graph rather than from a build that happened to succeed — `cargo tree -p thoughtsync-uniffi-bindgen` contains none of openssl-sys, native-tls, reqwest, thoughtsync-core or rusqlite. It stays a WORKSPACE MEMBER on purpose. Sharing one lockfile is what keeps uniffi here and uniffi linked into the .so at one version; they are two halves of one ABI, and a separate lockfile is precisely how they would drift apart. The cost is that the desktop lane now compiles ~15 generator crates it never runs — cheap next to Tauri, and better than leaving the crate unlinted. Drops the `bindgen` feature and required-features bin from thoughtsync-ffi, which existed only to keep those crates off the desktop lane and now have nothing to gate. Local fmt + clippy + test all green before pushing (107 tests). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3d3df1beb0 |
android: register generated sources through the Variant API
Second Android run failed with AGP 9 refusing the previous fix by name: You cannot add Provider instances to the Android SourceSet API. [...] Instead you should use the Sources interface in the Variant API, in particular SourceDirectories.addGeneratedDirectory AGP cannot tell from a Provider whether the directory holds generated (read-only) or hand-written (read-write) files, which is a distinction the IDE needs. `addGeneratedSourceDirectory` is the supported route and — unlike the plain-path form the error offers as an escape hatch — it carries the task dependency, so Kotlin still cannot compile before the bindings are generated and the APK cannot package a stale .so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f179928c57 |
android: run ktlint and detekt from the image, not as Gradle plugins
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> |
||
|
|
20907abf6e |
android: a Kotlin/Compose app that drives the Rust core (M12 step 5)
The skeleton, and the lane that builds it. Gradle invokes cargo-ndk to
cross-compile thoughtsync-ffi for four ABIs, generates the Kotlin bindings from
the resulting .so, and packages both.
BUILT ON MINSTREL'S TOOLCHAIN, not a fresh guess. Gradle 9.1.0 / AGP 9.0.1 /
Kotlin 2.3.21 on JDK 25 is the combination already proven in this family on
ci-android, including the JDK 22+ native-access opt-in the launcher JVM needs
and the artifact-upload action pinned by SHA (issues 2255 / 2270). It also
independently confirms the JDK 25 call made on ci-rust-android in step 3.
Gradle wiring worth noting:
* ExecOperations, not project.exec — the latter was REMOVED in Gradle 9, and
touching `project` at execution time is also what breaks the configuration
cache this build enables.
* The cargo task's inputs are the Rust SOURCES, not the workspace directory.
Declaring the directory would make Gradle hash target/, which is gigabytes.
* Bindings are generated with `--library` against the built .so, so they can
never describe a different version of the Rust than the one being packaged.
* cargo runs --locked, so an Android build cannot silently re-resolve the
lockfile the desktop lanes are gated on.
JNA is a real dependency, with the @aar classifier. The plain jar builds fine
and fails at runtime with UnsatisfiedLinkError, which is the worst way to learn
it. R8 keep rules for JNA and the bindings are in for the same reason — that
failure would otherwise appear only in a minified release.
The UI is a working board, not a debug screen: capture field, note list, empty
state, error banner, and an honest failure screen for a store that won't open.
Rules 23/24 — a surface ships at quality from the first commit. Capture uses the
IME action key because the north star is a thought captured in under a second,
and leaves the title empty so the core derives it from the first body line.
Every core call runs on Dispatchers.IO: they are blocking FFI into synchronous
SQLite, and running them on the main thread is exactly the jank going native was
meant to avoid.
The launcher icon reuses frontend/public/icon-maskable-512.png as an adaptive
foreground on the brand #F5C518 — the same asset and colour the web app already
ships, so the three surfaces wear one face.
No signing config. A release keystore that has passed through an agent session
or shell history is compromised by construction (task 2136); it has to be
generated by the operator and reach CI only as a secret. CI builds debug.
CI can only prove this BUILDS — a Linux runner cannot execute an APK, so feel
and on-device correctness remain an operator pass on an emulator.
Scribe #2739.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
f90b9203a7 |
android: bind the core to Kotlin through uniffi (M12 step 4)
`android/ffi` is to Android what `desktop/src-tauri/src/commands/` is to the
desktop: a shim over the shared core holding no logic of its own. Third workspace
member, so the desktop lane's `cargo clippy --all-targets` compiles and lints it
— which until the Android lane lands (step 5) is the only thing that does.
Three decisions worth stating.
MIRRORED RECORDS, NOT DERIVES ON THE CORE. The core's model structs are serde
shapes contracted with the shared Vue frontend, and one of them holds a
serde_json::Value, which has no uniffi representation. Hanging uniffi derives on
them would couple two unrelated consumers to one definition. The cost of
mirroring is drift — an Android client quietly missing a field the desktop
gained — so every conversion destructures the core struct exhaustively. Add a
field to core::local::models::Note and this crate stops compiling until Android
is told what to do with it.
NoteEdit IS A LIST, NOT A STRUCT OF NULLABLE FIELDS. The store's patch format
distinguishes three states: leave alone, set, and clear to null. Kotlin cannot
express the third with a nullable field — `title = null` in a data class is
indistinguishable from `title` unset — so the editor could never clear a title.
Explicit Clear* variants say it out loud and give Kotlin a sealed class.
ASYNC IS TOKIO-BACKED, AND CANCELLATION ALREADY WORKED. Exported async methods
become Kotlin suspend functions. When a coroutine is cancelled uniffi drops the
future, and no async path in the core holds the store lock across an await —
a std MutexGuard isn't Send, so the compiler has been enforcing that all along.
A cancelled sync leaves the store consistent and simply hasn't stamped
last_sync_at, which is only written after both halves of a cycle succeed.
Also here:
* core gains Db::conn(). Every consumer was writing
`db.0.lock().map_err(|e| e.to_string())?` by hand, and worse, any helper
returning the guard had to NAME rusqlite::Connection — which would have made
rusqlite a dependency of a layer whose whole point is not knowing what the
store is made of. Same trap as the update.rs test module in step 1.
* The uniffi `cli` feature is gated behind our own `bindgen` feature. It drags
in clap, askama and goblin for a three-line binary, and the desktop lane
should not compile a code generator it never runs.
* The bindgen binary lives in this workspace on purpose: generated bindings and
the linked uniffi runtime are two halves of one ABI, and compiling the
generator against the same dependency keeps them in step by construction.
That is why ci-rust-android ships no uniffi-bindgen.
Tests cover the round trip the Android skeleton needs (open a store in a
directory that does not exist yet, write a note, read it back), that a body-only
note still has a display_title, that set and clear are genuinely different
edits, and that an unlinked app reports NotLinked rather than an error.
Known and deliberate: the workspace sets panic = "abort", so a panic crossing the
FFI aborts instead of arriving in Kotlin as an exception. Same behaviour the
desktop already has; noted in the crate header rather than silently changed.
Scribe #2733.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|