Skip to content

Commit 88dd66a

Browse files
authored
build: add an R8 shrink-survival test module and ship consumer keep-rules (#106)
PR: #106
1 parent a9b6b5d commit 88dd66a

12 files changed

Lines changed: 691 additions & 2 deletions

File tree

CLAUDE.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,17 @@ root `check` task — see `build.gradle.kts`). Detekt is skipped on the two non-
2121
system toolchain when a module targets a non-8 toolchain; see those build scripts for the upstream issue and
2222
re-enable conditions. It runs everywhere else, including `sdk-transport-okhttp`.
2323

24+
`check` (so a plain `./gradlew build`) also runs the R8 shrink-survival guard in the test-only
25+
`sdk-shrink-test` module. That step **requires a JDK 11 toolchain** (Gradle auto-provisions one if absent)
26+
and network access to **Google's Maven repo** to fetch `com.android.tools:r8`. An offline build, or one
27+
that cannot provision JDK 11, will fail on `:sdk-shrink-test:r8Run`; scope the build (e.g. build specific
28+
modules) to skip it. See that module's `build.gradle.kts` for the pipeline.
29+
2430
## Repository Layout
2531

26-
Nine Gradle modules (see `settings.gradle.kts`). `gradle/libs.versions.toml` is the single source of truth
27-
for dependency and plugin versions. Group `org.dexpace`, version `0.0.1-alpha.1`.
32+
Ten Gradle modules (see `settings.gradle.kts`). `gradle/libs.versions.toml` is the single source of truth
33+
for dependency and plugin versions. Group `org.dexpace`, version `0.0.1-alpha.1`. (The tenth,
34+
`sdk-shrink-test`, is a test-only, unpublished R8 shrink-survival guard — not listed below.)
2835

2936
| Module | Purpose | JVM target |
3037
|---|---|---|

build.gradle.kts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,28 @@ tasks.named("check") {
8282
dependsOn(tasks.named("koverVerify"))
8383
}
8484

85+
// Keep the test-only shrink-survival module out of the binary-compatibility snapshot. It ships no
86+
// public artifact, so it needs no committed `.api` file; without this exclusion apiCheck would
87+
// demand one (and apiDump would generate a spurious snapshot for an unpublished module). Mirrors
88+
// how the module is also left out of the kover aggregate below.
89+
apiValidation {
90+
ignoredProjects += "sdk-shrink-test"
91+
}
92+
8593
allprojects {
8694
repositories {
8795
mavenCentral()
8896
maven {
8997
// For maven snapshots
9098
url = URI.create("https://oss.sonatype.org/content/repositories/snapshots/")
9199
}
100+
// Google's Maven repo hosts R8 (com.android.tools:r8), used only by the test-only
101+
// sdk-shrink-test module. Restricted to that group so it is not consulted for anything else.
102+
google {
103+
content {
104+
includeModule("com.android.tools", "r8")
105+
}
106+
}
92107
}
93108

94109
// Plugin application lives in each subproject's own `plugins {}` block — the old

gradle/libs.versions.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ reactor = "3.8.5"
99
netty = "4.2.13.Final"
1010
jackson = "2.18.2"
1111
junit-jupiter = "5.10.2"
12+
# R8 is used only by the test-only sdk-shrink-test module to verify the SDK survives consumer-side
13+
# shrinking. It is fetched from Google's Maven repo and never enters a published artifact.
14+
r8 = "8.9.35"
1215
kover = "0.9.8"
1316
binary-compatibility-validator = "0.16.3"
1417
ktlint-plugin = "12.1.1"
@@ -32,6 +35,7 @@ jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-
3235
jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" }
3336
jackson-datatype-jdk8 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jdk8", version.ref = "jackson" }
3437
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
38+
r8 = { module = "com.android.tools:r8", version.ref = "r8" }
3539

3640
[plugins]
3741
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Copyright (c) 2026 dexpace and Omar Aljarrah
2+
#
3+
# Licensed under the MIT License. See LICENSE in the project root.
4+
# SPDX-License-Identifier: MIT
5+
6+
# Consumer ProGuard/R8 keep rules for sdk-core.
7+
#
8+
# R8 and the Android Gradle Plugin automatically apply any rules packaged under
9+
# META-INF/proguard/ in a dependency jar, so a downstream application that shrinks its
10+
# build inherits these without extra configuration. They protect the parts of the toolkit
11+
# that a shrinker cannot prove are reachable on its own:
12+
#
13+
# * the SPI seams that callers wire at runtime (the I/O provider, the transport clients,
14+
# the serde), whose implementations live in separate modules and are referenced only
15+
# through interfaces; and
16+
# * the immutable HTTP models and the Tristate type, which Jackson and other reflective
17+
# serializers bind by walking constructors, accessors, and Kotlin metadata rather than
18+
# through direct call sites the shrinker can see.
19+
20+
# --- SPI contracts wired at runtime --------------------------------------------------
21+
22+
# The single I/O seam. Io.installProvider(...) is the documented entry point and IoProvider
23+
# is implemented in an adapter module, so keep both surfaces intact.
24+
-keep class org.dexpace.sdk.core.io.Io { *; }
25+
-keep class org.dexpace.sdk.core.io.IoProvider { *; }
26+
27+
# Transport SPIs. Concrete transports (e.g. OkHttpTransport) are reached only through these
28+
# interfaces, so the methods a caller invokes must survive.
29+
-keep class org.dexpace.sdk.core.client.HttpClient { *; }
30+
-keep class org.dexpace.sdk.core.client.AsyncHttpClient { *; }
31+
32+
# Serde SPI. JacksonSerde and any other implementation are reached through these interfaces.
33+
-keep class org.dexpace.sdk.core.serde.Serde { *; }
34+
-keep class org.dexpace.sdk.core.serde.Serializer { *; }
35+
-keep class org.dexpace.sdk.core.serde.Deserializer { *; }
36+
37+
# --- Immutable HTTP models and their builders ----------------------------------------
38+
39+
# Request / Response and their nested builders are constructed and read reflectively by
40+
# serializers and assertion frameworks; preserving every member keeps the public surface
41+
# (factories, builder fluents, component accessors) callable after shrinking.
42+
-keep class org.dexpace.sdk.core.http.request.Request { *; }
43+
-keep class org.dexpace.sdk.core.http.request.Request$RequestBuilder { *; }
44+
-keep class org.dexpace.sdk.core.http.request.RequestBody { *; }
45+
-keep class org.dexpace.sdk.core.http.request.Method { *; }
46+
-keep class org.dexpace.sdk.core.http.response.Response { *; }
47+
-keep class org.dexpace.sdk.core.http.response.Response$ResponseBuilder { *; }
48+
-keep class org.dexpace.sdk.core.http.response.ResponseBody { *; }
49+
-keep class org.dexpace.sdk.core.http.response.Status { *; }
50+
-keep class org.dexpace.sdk.core.http.common.Headers { *; }
51+
-keep class org.dexpace.sdk.core.http.common.Headers$Builder { *; }
52+
-keep class org.dexpace.sdk.core.http.common.MediaType { *; }
53+
-keep class org.dexpace.sdk.core.http.common.CommonMediaTypes { *; }
54+
-keep class org.dexpace.sdk.core.http.common.Protocol { *; }
55+
56+
# --- Tristate ------------------------------------------------------------------------
57+
58+
# Tristate models the absent / null / present distinction a serializer must reconstruct from
59+
# the wire. The custom Jackson binding (shipped by sdk-serde-jackson) checks the runtime type
60+
# of each variant, so the sealed hierarchy and the Present payload accessor must remain.
61+
-keep class org.dexpace.sdk.core.serde.Tristate { *; }
62+
-keep class org.dexpace.sdk.core.serde.Tristate$Absent { *; }
63+
-keep class org.dexpace.sdk.core.serde.Tristate$Null { *; }
64+
-keep class org.dexpace.sdk.core.serde.Tristate$Present { *; }
65+
66+
# Kotlin emits @Metadata on every class; reflective Kotlin tooling (including Jackson's Kotlin
67+
# module) reads it to recover constructor parameter names and nullability. Strip it and
68+
# data-class binding silently degrades, so keep the annotation across the toolkit.
69+
#
70+
# Scope note: `-keepattributes` is a global directive. Because this file ships under
71+
# META-INF/proguard, depending on sdk-core adds these attributes to the consumer's *entire*
72+
# program, not just the SDK's classes.
73+
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
74+
-keep class kotlin.Metadata { *; }
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright (c) 2026 dexpace and Omar Aljarrah
2+
#
3+
# Licensed under the MIT License. See LICENSE in the project root.
4+
# SPDX-License-Identifier: MIT
5+
6+
# Consumer ProGuard/R8 keep rules for sdk-io-okio3.
7+
#
8+
# This module's only public surface is the OkioIoProvider singleton, installed at startup
9+
# via Io.installProvider(OkioIoProvider). A shrinker following the application from its own
10+
# entry points cannot always see that wiring, so keep the provider and its INSTANCE field.
11+
-keep class org.dexpace.sdk.io.OkioIoProvider { *; }
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright (c) 2026 dexpace and Omar Aljarrah
2+
#
3+
# Licensed under the MIT License. See LICENSE in the project root.
4+
# SPDX-License-Identifier: MIT
5+
6+
# Consumer ProGuard/R8 keep rules for sdk-serde-jackson.
7+
#
8+
# JacksonSerde is the public entry point (withDefaults() / from(ObjectMapper)). The module also
9+
# registers a custom module that teaches Jackson how to (de)serialize Tristate; both the entry
10+
# point and that module are reached reflectively through Jackson's module-registration and
11+
# bean-introspection machinery, so they must survive shrinking.
12+
-keep class org.dexpace.sdk.serde.jackson.JacksonSerde { *; }
13+
-keep class org.dexpace.sdk.serde.jackson.JacksonObjectMappers { *; }
14+
-keep class org.dexpace.sdk.serde.jackson.TristateModule { *; }
15+
16+
# Jackson databind is reflection-heavy: it reads annotations, walks bean members, and resolves
17+
# parametric types at runtime, and its own config classes initialise from annotation enum
18+
# singletons (a stripped or renamed enum value surfaces as an NPE in SerializationConfig's static
19+
# initialiser). It is not meaningfully shrinkable without a hand-curated configuration, so the
20+
# conventional — and the only safe — consumer recommendation is to keep the databind, core, and
21+
# annotation packages wholesale, retain the attributes Jackson reflects over, and keep every
22+
# annotation enum intact.
23+
#
24+
# Scope note: because this file ships under META-INF/proguard, R8/AGP applies it to the consumer's
25+
# entire program, not just the SDK's classes. The wholesale Jackson `-keep` rules below therefore
26+
# exempt the consumer's *entire* Jackson surface from shrinking — including any Jackson the app uses
27+
# directly, elsewhere. The `-keepattributes` directive is likewise global: it adds these attributes
28+
# to the whole consumer build, not only to SDK classes. An app that wants tighter shrinking can
29+
# override these rules in its own configuration.
30+
-keepattributes Signature,*Annotation*,EnclosingMethod,InnerClasses
31+
-keep class com.fasterxml.jackson.databind.** { *; }
32+
-keep class com.fasterxml.jackson.core.** { *; }
33+
-keep class com.fasterxml.jackson.annotation.** { *; }
34+
-keep enum com.fasterxml.jackson.** { *; }
35+
-dontwarn com.fasterxml.jackson.databind.ext.**

0 commit comments

Comments
 (0)