Commit Graph
55 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5 dee71dffb3 android: teach the linters this codebase's conventions, and fix two real nits
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m33s
Android / Kotlin + Rust (debug APK) (push) Failing after 4m49s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m59s
Fourth run got the whole native pipeline through — cargo-ndk built all four
ABIs and uniffi generated the Kotlin — and then failed on style.

Two genuine mistakes, fixed:
  * BoardViewModel's constructor parameter needed its own line.
  * PaddingValues was written fully-qualified inline, which ktlint read as a
    method chain. Importing it is what the rule was actually asking for, and
    what the line should have said anyway.

The other ten were the tools not knowing this codebase:
  * @Composable functions are PascalCase by universal Compose convention.
    Exempted in BOTH .editorconfig (ktlint) and config/detekt.yml — they have to
    agree or one of them is always wrong.
  * MagicNumber on `private val Brand = Color(0xFFF5C518)`. The rule asks for a
    well-named constant; that line IS one. ignorePropertyDeclaration.
  * TooGenericExceptionCaught in the ViewModel and Application. Deliberate and
    already commented: a note that fails to save must become a visible error
    banner rather than a crash, and the store failing to open must still let the
    app start so it can explain itself. Scoped to those two paths, not disabled
    globally — everywhere else the rule is right.

Verified locally this time, both linters clean, using the SAME pinned CLIs from
ci-android:36 that the lane runs. ktlint and detekt are a formatter and a static
analyzer — the same category as cargo fmt and clippy, which is the precedent
ci-requirements already sets. No build was run locally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:04:40 -04:00
bvandeusenandClaude Opus 5 5d0de7a682 android: the binding generator gets its own crate, free of the app's deps
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m27s
Android / Kotlin + Rust (debug APK) (push) Failing after 3m51s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m55s
Desktop (Tauri) / Update manifest (push) Successful in 5s
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>
2026-08-18 15:54:10 -04:00
bvandeusenandClaude Opus 5 3d3df1beb0 android: register generated sources through the Variant API
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m16s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m40s
Android / Kotlin + Rust (debug APK) (push) Failing after 3m5s
Desktop (Tauri) / Update manifest (push) Successful in 5s
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>
2026-08-18 15:43:57 -04:00
bvandeusenandClaude Opus 5 f179928c57 android: run ktlint and detekt from the image, not as Gradle plugins
Android / Kotlin + Rust (debug APK) (push) Failing after 1m47s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m6s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m2s
Desktop (Tauri) / Update manifest (push) Successful in 5s
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>
2026-08-18 15:35:17 -04:00
bvandeusenandClaude Opus 5 20907abf6e android: a Kotlin/Compose app that drives the Rust core (M12 step 5)
Android / Kotlin + Rust (debug APK) (push) Failing after 1m20s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m24s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m0s
Desktop (Tauri) / Update manifest (push) Successful in 5s
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>
2026-08-18 15:24:30 -04:00