mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 11:48:24 +00:00
R8 cannot see reflection. A keep rule that stops matching — a class moved to
another package, a rule dropped in a merge, a DTO renamed — produces a green
build and an APK that fails on a user's device: ClassNotFoundException from
WorkManager, JSON with one-letter property names, a settings file that no longer
parses. Nothing in the build says a word about it.
tools/r8-verify/reflection-contract.txt lists every name resolved from outside
the DEX, each with the mechanism that reads it: JNI symbols and the Rust
GetMethodID callback, Jackson's reflectively-bound DTOs, enum constants
persisted into DataStore, WorkManager's stored worker class names, the Cast
OptionsProvider named in a manifest <meta-data> value. 31 checks.
verify_reflection_contract.py asserts each against what R8 actually emitted, in
under a second, with no device. Wired into create-release.yml for both flavors
before assets are collected, so a break fails the release instead of shipping.
It reads mapping.txt AND usage.txt, because neither is sufficient:
- mapping.txt records only what CHANGED. A class kept intact by
`-keep ... { *; }` appears with an empty body and an unrenamed member has no
line at all, so absence there means "preserved". A first version of this
read absence as "missing" and reported 11 false failures against a build I
had already verified by hand.
- usage.txt is the other half: a constant R8 deleted leaves no trace in
mapping.txt at all.
It also checks the two files came from the same R8 run. mapping.txt is written
during packaging, not at minify time, so a minify-only rebuild leaves a stale one
beside a fresh usage.txt — which is exactly the state this session ended up in,
and the mixed directory reads as perfectly coherent. The invariant is that a
class R8 deleted cannot also be a class R8 named; on the mixed directory it
flagged 15 such contradictions, all of them the classes whose rule had changed
between the two runs, and zero on a consistent build.
Verified by breaking it on purpose. Deleting the real NWC keep rule from both
proguard files and rebuilding produced 4 failures on genuine R8 output — three
DTOs renamed (PayInvoiceParams -> xud) and NwcTransaction shrunk away entirely,
which in production is a silent break in wallet transaction lists. Restoring the
rule and rebuilding: 31/31 pass. Eight fixture cases cover every failure mode
(class renamed, method renamed, field renamed, constant renamed, constant
deleted, class deleted, stale directory, healthy baseline).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEoxktEZyTrAS33vVZBiwm