build: document the poisoned build cache; compare like with like

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 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-02 08:01:58 +02:00
co-authored by Claude Opus 5
parent bc384531e6
commit f6e093944c
2 changed files with 16 additions and 1 deletions
+11
View File
@@ -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 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 port + mDNS record appear, and the beacon/connector recover. Plan the Shizuku-tier dev loop
around this (or USB, if ever available). 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 - **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 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 containing only a manifest, and Gradle then considers `jar` up-to-date. Dependent modules fail
@@ -285,7 +285,11 @@ class RunViewModel(app: Application) : AndroidViewModel(app) {
state = state.copy( state = state.copy(
pendingEnroll = PendingEnroll( pendingEnroll = PendingEnroll(
link = link, 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, newServer = target,
) )
) )