Skip to content

build: add an R8 shrink-survival test module and ship consumer keep-rules#106

Merged
OmarAlJarrah merged 3 commits into
mainfrom
build/r8-shrink-survival
Jun 16, 2026
Merged

build: add an R8 shrink-survival test module and ship consumer keep-rules#106
OmarAlJarrah merged 3 commits into
mainfrom
build/r8-shrink-survival

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Jun 16, 2026

Copy link
Copy Markdown
Member

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/*.pro files in sdk-core, sdk-io-okio3, sdk-transport-okhttp, sdk-transport-jdkhttp, and sdk-serde-jackson. Because they ship under META-INF/proguard/, downstream R8/AGP applies them automatically. They keep the Io seam, IoProvider, the HttpClient/AsyncHttpClient/Serde SPIs, the HTTP models + builders, the Tristate hierarchy, kotlin.Metadata, and Jackson's reflective databind/core/annotation surface. Both reference transports protect their construction surface, so neither can lose its rules unnoticed.

sdk-shrink-test regression 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 both OkHttpTransport and JdkHttpTransport (via the core HttpClient SPI), and a Tristate JSON round-trip through JacksonSerde (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/build lifecycle. The module targets JDK 11: it depends on the Java-11 sdk-transport-jdkhttp so 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 from apiValidation and 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

  • A consumer must supply an SLF4J binding (it's compileOnly in the SDK); the harness bundles slf4j-nop.
  • The initial Jackson keep-rules were too narrow — its SerializationConfig static init failed because annotation enums were stripped. The shipped sdk-serde-jackson.pro was 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

  • The build now includes an R8 step that requires a JDK 11 toolchain and the Google Maven repo (for com.android.tools:r8); this is documented in CLAUDE.md. An offline build, or one that cannot provision JDK 11, fails on :sdk-shrink-test:r8Run — scope the build to skip it.
  • R8 8.9.35 logs a non-fatal note that it can't parse Kotlin 2.3 metadata (its max is 2.1); the @Metadata annotation is retained via -keepattributes / -keep class kotlin.Metadata (proven by the passing serde round-trip), and a future R8 bump quiets the log.
  • The issue's CI smoke job is blocked on CI existing (Add CI workflows to enforce the build gates on every PR #70); hanging the check off the Gradle lifecycle means CI picks it up automatically once that lands.

Touches settings.gradle.kts, so expect a rebase against the example-module (#102) and convention-plugin (#105) PRs depending on merge order.

Closes #72

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.
@OmarAlJarrah OmarAlJarrah merged commit 88dd66a into main Jun 16, 2026
1 check passed
@OmarAlJarrah OmarAlJarrah deleted the build/r8-shrink-survival branch June 16, 2026 21:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a test-only R8 shrink-survival module with consumer keep-rules

1 participant