From f6e093944c05cdd519bec3a7fc30556388c00416 Mon Sep 17 00:00:00 2001 From: mrambossek Date: Sun, 2 Aug 2026 08:01:58 +0200 Subject: [PATCH] build: document the poisoned build cache; compare like with like MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An entire module was missing from the APK. The app died with ClassNotFoundException for app.echo_lot.protocol.EnrollmentLink while the build was green, the module's jar was correct, and :app:dependencies listed it on debugRuntimeClasspath — its code simply never reached AGP's intermediates. The cause was a poisoned Gradle build cache entry, which is why nothing obvious fixed it: clean, rm -rf */build and --rerun-tasks all leave the build cache alone. Only --no-build-cache did. Every app build made in this session shipped without core-protocol, so enrolment, sign-in and upload would all have crashed identically; several hours of "the tap does nothing" were this, misread as a UI problem. CLAUDE.md now carries the symptom, the fix, and the verification — grepping the dex for a string literal only that module defines, because grepping for a class *name* proves nothing: callers carry the name as a reference whether or not the class is packaged. That false check is what let me believe an earlier rebuild had fixed it. Also: the enrolment dialog compared the stored endpoint against the link's public URL, so re-enrolling with the same server announced itself as a move to a different one. Those are deliberately different strings now that discovery exists; the comparison uses the public name on both sides. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 11 +++++++++++ .../src/main/kotlin/app/echo_lot/app/RunViewModel.kt | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4ec7bbf..dff668c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -145,6 +145,17 @@ First build downloads AGP/Compose/Shizuku from Google Maven + Maven Central. Shizuku, **toggle Wireless debugging off/on** — Shizuku keeps running (separate process), a fresh port + mDNS record appear, and the beacon/connector recover. Plan the Shizuku-tier dev loop around this (or USB, if ever available). +- **A poisoned Gradle *build cache* entry can silently drop a whole module from the APK.** + Symptom: the app dies with `ClassNotFoundException` for a class that plainly exists, while the + build is green and `./gradlew :app:dependencies` lists the module on `debugRuntimeClasspath`. + The module's own jar is correct; its code simply never reaches AGP's intermediates. `clean`, + `rm -rf */build` and `--rerun-tasks` all fail to fix it, because **none of them touch the build + cache** — look for `compileKotlin FROM-CACHE` in the log. Fix: rebuild with `--no-build-cache`. + Verify by grepping the APK's dex for a string literal that only that module defines; grepping for + a *class name* proves nothing, because callers carry the name as a reference whether or not the + class is packaged: + `unzip -o -q app-debug.apk "classes*.dex" && grep -a "pin-sha256:" *.dex` + Suspect this whenever a runtime failure contradicts a successful build. - **Empty-jar race with the IDE.** VSCodium's Java/Kotlin extension runs its own Gradle daemon on the same project; when it overlaps a CLI build, a module's `build/libs/*.jar` can end up containing only a manifest, and Gradle then considers `jar` up-to-date. Dependent modules fail diff --git a/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunViewModel.kt b/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunViewModel.kt index e2114b3..2160fcc 100644 --- a/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunViewModel.kt +++ b/echolot-app/app/src/main/kotlin/app/echo_lot/app/RunViewModel.kt @@ -285,7 +285,11 @@ class RunViewModel(app: Application) : AndroidViewModel(app) { state = state.copy( pendingEnroll = PendingEnroll( link = link, - currentServer = settings.serverUrl, + // Compared against the link's URL, which names the server publicly — so this + // has to be the public name too. Using the endpoint made re-enrolling with the + // same server look like a move to a different one, because the endpoint and + // the public name are deliberately different strings. + currentServer = settings.serverPublicUrl, newServer = target, ) )