build: add an R8 shrink-survival test module and ship consumer keep-rules#106
Merged
Conversation
A library has to survive its consumers shrinking their own apps: any class or
member the SDK reaches reflectively or through an SPI can be tree-shaken or
renamed by R8/ProGuard unless the SDK tells the shrinker to keep it. Nothing in
the build verified that, and the SDK shipped no consumer rules at all.
Ship consumer keep-rules from each module that has a reflective or runtime-wired
surface, packaged under META-INF/proguard so a downstream R8/AGP build applies
them automatically:
- sdk-core: the Io seam and IoProvider, the HttpClient/AsyncHttpClient and
Serde SPIs, the immutable HTTP models and their builders, and the Tristate
hierarchy plus the kotlin.Metadata needed for reflective binding.
- sdk-io-okio3: the OkioIoProvider entry point.
- sdk-transport-okhttp: the OkHttpTransport entry point and Builder, with
dontwarn for OkHttp's optional TLS providers.
- sdk-serde-jackson: the JacksonSerde entry point and the custom Tristate
module, plus the wholesale Jackson databind/core/annotation keeps that
reflection-heavy library needs (a stripped annotation enum otherwise fails
Jackson's config initialiser).
Add a test-only, unpublished sdk-shrink-test module that turns those rules into
a regression guard. It bundles a small consumer program with the SDK and its
real runtime dependencies (Okio, OkHttp, Jackson, Kotlin stdlib, an SLF4J
binding) into one jar, runs R8 in full mode over it using the SHIPPED keep-rules
extracted straight from the SDK jars, then runs the shrunk program. The program
performs a real in-process HTTP round-trip through OkHttpTransport and a full
Tristate JSON round-trip through JacksonSerde, so the check proves the kept
members still function after shrinking rather than merely that the classes
remain. The R8 run is wired into check, so a plain build enforces it.
The module is excluded from the binary-compatibility snapshot
(apiValidation.ignoredProjects) and from the Kover coverage aggregate, since it
publishes nothing and contributes no coverage; ktlint, detekt, and explicit-API
remain on. R8 is pinned in the version catalog and fetched from a
group-restricted Google Maven repo, never entering a published artifact.
CI wiring is deferred until the CI workflow itself lands.
Ship consumer keep-rules for sdk-transport-jdkhttp so both reference transports protect their construction surface symmetrically, and guard the new rules for real: the harness now bundles the jdkhttp transport and drives an R8-shrunk round-trip through both transports via a shared helper, rather than only okhttp. Depending on the Java-11 jdkhttp module means sdk-shrink-test now targets JDK 11 — the honest model, since the pipeline already runs on a JDK 11 (R8 is Java-11 bytecode and the shrunk program runs on the same launcher). Detekt is skipped here for the same JDK-25-toolchain reason as the other non-8 modules; ktlint and explicit-API strict mode stay on. Also: - Clarify that the harness is shrink-only (obfuscation disabled): correct the app KDoc and expand app-rules.pro on why renaming is out of scope. - r8Run now ignores the exit value and asserts exit code + sentinel in doLast, so a failing shrunk run surfaces its captured output instead of an empty JavaExec abort. - Document in CLAUDE.md that check now needs a JDK 11 toolchain and the Google Maven repo for the shrink-survival step.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds consumer keep-rules and a regression guard so that downstream applications which shrink with R8/ProGuard don't strip or break the SDK's reflective and SPI surface.
Shipped consumer keep-rules
New
META-INF/proguard/*.profiles insdk-core,sdk-io-okio3,sdk-transport-okhttp,sdk-transport-jdkhttp, andsdk-serde-jackson. Because they ship underMETA-INF/proguard/, downstream R8/AGP applies them automatically. They keep theIoseam,IoProvider, theHttpClient/AsyncHttpClient/SerdeSPIs, the HTTP models + builders, theTristatehierarchy,kotlin.Metadata, and Jackson's reflectivedatabind/core/annotationsurface. Both reference transports protect their construction surface, so neither can lose its rules unnoticed.sdk-shrink-testregression module (test-only, unpublished)Bundles a small consumer program plus the full runtime classpath into a fat jar, runs R8 using the shipped keep-rules (extracted from the SDK jars on the classpath and asserted present, so the test guards the real shipped rules rather than a private copy), then runs the shrunk program and asserts it works. R8 genuinely shrinks the bundle, and the shrunk run exercises the kept surface for real: installing
OkioIoProvider, an in-process HTTP POST round-trip through bothOkHttpTransportandJdkHttpTransport(via the coreHttpClientSPI), and aTristateJSON round-trip throughJacksonSerde(the most shrink-fragile, reflection-heavy path). So it proves kept members still function after tree-shaking, not merely that class names survive.R8 is run in shrink-only mode (obfuscation disabled): the guard is that dead-code elimination doesn't strip the SDK's reflective/SPI surface. Obfuscation survival is out of scope here because renaming would also rename the bundled third-party libraries (OkHttp, Okio, Jackson), each of which ships its own consumer keep-rules that an obfuscating consumer applies; the SDK's own
-keep ... { *; }rules already pin names against renaming.Wired into the
check/buildlifecycle. The module targets JDK 11: it depends on the Java-11sdk-transport-jdkhttpso it can drive that transport through R8, and the whole pipeline already runs on a JDK 11 (R8 is Java-11 bytecode and the shrunk program runs on the same launcher), so a consumer that uses the jdkhttp transport is an 11+ consumer by construction. The module is excluded fromapiValidationand the Kover aggregate (matching the example module); ktlint and explicit-API strict mode stay on. Detekt is skipped here for the same JDK-25-system-toolchain reason as the other non-Java-8 modules.Real gaps this surfaced while being made to pass
compileOnlyin the SDK); the harness bundlesslf4j-nop.SerializationConfigstatic init failed because annotation enums were stripped. The shippedsdk-serde-jackson.prowas widened to keep the Jackson annotation/enum surface, which is the conventional recommendation for that library and exactly the regression class this module now guards.Notes
com.android.tools:r8); this is documented inCLAUDE.md. An offline build, or one that cannot provision JDK 11, fails on:sdk-shrink-test:r8Run— scope the build to skip it.@Metadataannotation is retained via-keepattributes/-keep class kotlin.Metadata(proven by the passing serde round-trip), and a future R8 bump quiets the log.Closes #72