Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,17 @@ root `check` task — see `build.gradle.kts`). Detekt is skipped on the two non-
system toolchain when a module targets a non-8 toolchain; see those build scripts for the upstream issue and
re-enable conditions. It runs everywhere else, including `sdk-transport-okhttp`.

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

## Repository Layout

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

| Module | Purpose | JVM target |
|---|---|---|
Expand Down
15 changes: 15 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,28 @@ tasks.named("check") {
dependsOn(tasks.named("koverVerify"))
}

// Keep the test-only shrink-survival module out of the binary-compatibility snapshot. It ships no
// public artifact, so it needs no committed `.api` file; without this exclusion apiCheck would
// demand one (and apiDump would generate a spurious snapshot for an unpublished module). Mirrors
// how the module is also left out of the kover aggregate below.
apiValidation {
ignoredProjects += "sdk-shrink-test"
}

allprojects {
repositories {
mavenCentral()
maven {
// For maven snapshots
url = URI.create("https://oss.sonatype.org/content/repositories/snapshots/")
}
// Google's Maven repo hosts R8 (com.android.tools:r8), used only by the test-only
// sdk-shrink-test module. Restricted to that group so it is not consulted for anything else.
google {
content {
includeModule("com.android.tools", "r8")
}
}
}

// Plugin application lives in each subproject's own `plugins {}` block — the old
Expand Down
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ reactor = "3.8.5"
netty = "4.2.13.Final"
jackson = "2.18.2"
junit-jupiter = "5.10.2"
# R8 is used only by the test-only sdk-shrink-test module to verify the SDK survives consumer-side
# shrinking. It is fetched from Google's Maven repo and never enters a published artifact.
r8 = "8.9.35"
kover = "0.9.8"
binary-compatibility-validator = "0.16.3"
ktlint-plugin = "12.1.1"
Expand All @@ -32,6 +35,7 @@ jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-
jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" }
jackson-datatype-jdk8 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jdk8", version.ref = "jackson" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
r8 = { module = "com.android.tools:r8", version.ref = "r8" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
Expand Down
74 changes: 74 additions & 0 deletions sdk-core/src/main/resources/META-INF/proguard/sdk-core.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright (c) 2026 dexpace and Omar Aljarrah
#
# Licensed under the MIT License. See LICENSE in the project root.
# SPDX-License-Identifier: MIT

# Consumer ProGuard/R8 keep rules for sdk-core.
#
# R8 and the Android Gradle Plugin automatically apply any rules packaged under
# META-INF/proguard/ in a dependency jar, so a downstream application that shrinks its
# build inherits these without extra configuration. They protect the parts of the toolkit
# that a shrinker cannot prove are reachable on its own:
#
# * the SPI seams that callers wire at runtime (the I/O provider, the transport clients,
# the serde), whose implementations live in separate modules and are referenced only
# through interfaces; and
# * the immutable HTTP models and the Tristate type, which Jackson and other reflective
# serializers bind by walking constructors, accessors, and Kotlin metadata rather than
# through direct call sites the shrinker can see.

# --- SPI contracts wired at runtime --------------------------------------------------

# The single I/O seam. Io.installProvider(...) is the documented entry point and IoProvider
# is implemented in an adapter module, so keep both surfaces intact.
-keep class org.dexpace.sdk.core.io.Io { *; }
-keep class org.dexpace.sdk.core.io.IoProvider { *; }

# Transport SPIs. Concrete transports (e.g. OkHttpTransport) are reached only through these
# interfaces, so the methods a caller invokes must survive.
-keep class org.dexpace.sdk.core.client.HttpClient { *; }
-keep class org.dexpace.sdk.core.client.AsyncHttpClient { *; }

# Serde SPI. JacksonSerde and any other implementation are reached through these interfaces.
-keep class org.dexpace.sdk.core.serde.Serde { *; }
-keep class org.dexpace.sdk.core.serde.Serializer { *; }
-keep class org.dexpace.sdk.core.serde.Deserializer { *; }

# --- Immutable HTTP models and their builders ----------------------------------------

# Request / Response and their nested builders are constructed and read reflectively by
# serializers and assertion frameworks; preserving every member keeps the public surface
# (factories, builder fluents, component accessors) callable after shrinking.
-keep class org.dexpace.sdk.core.http.request.Request { *; }
-keep class org.dexpace.sdk.core.http.request.Request$RequestBuilder { *; }
-keep class org.dexpace.sdk.core.http.request.RequestBody { *; }
-keep class org.dexpace.sdk.core.http.request.Method { *; }
-keep class org.dexpace.sdk.core.http.response.Response { *; }
-keep class org.dexpace.sdk.core.http.response.Response$ResponseBuilder { *; }
-keep class org.dexpace.sdk.core.http.response.ResponseBody { *; }
-keep class org.dexpace.sdk.core.http.response.Status { *; }
-keep class org.dexpace.sdk.core.http.common.Headers { *; }
-keep class org.dexpace.sdk.core.http.common.Headers$Builder { *; }
-keep class org.dexpace.sdk.core.http.common.MediaType { *; }
-keep class org.dexpace.sdk.core.http.common.CommonMediaTypes { *; }
-keep class org.dexpace.sdk.core.http.common.Protocol { *; }

# --- Tristate ------------------------------------------------------------------------

# Tristate models the absent / null / present distinction a serializer must reconstruct from
# the wire. The custom Jackson binding (shipped by sdk-serde-jackson) checks the runtime type
# of each variant, so the sealed hierarchy and the Present payload accessor must remain.
-keep class org.dexpace.sdk.core.serde.Tristate { *; }
-keep class org.dexpace.sdk.core.serde.Tristate$Absent { *; }
-keep class org.dexpace.sdk.core.serde.Tristate$Null { *; }
-keep class org.dexpace.sdk.core.serde.Tristate$Present { *; }

# Kotlin emits @Metadata on every class; reflective Kotlin tooling (including Jackson's Kotlin
# module) reads it to recover constructor parameter names and nullability. Strip it and
# data-class binding silently degrades, so keep the annotation across the toolkit.
#
# Scope note: `-keepattributes` is a global directive. Because this file ships under
# META-INF/proguard, depending on sdk-core adds these attributes to the consumer's *entire*
# program, not just the SDK's classes.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
-keep class kotlin.Metadata { *; }
11 changes: 11 additions & 0 deletions sdk-io-okio3/src/main/resources/META-INF/proguard/sdk-io-okio3.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright (c) 2026 dexpace and Omar Aljarrah
#
# Licensed under the MIT License. See LICENSE in the project root.
# SPDX-License-Identifier: MIT

# Consumer ProGuard/R8 keep rules for sdk-io-okio3.
#
# This module's only public surface is the OkioIoProvider singleton, installed at startup
# via Io.installProvider(OkioIoProvider). A shrinker following the application from its own
# entry points cannot always see that wiring, so keep the provider and its INSTANCE field.
-keep class org.dexpace.sdk.io.OkioIoProvider { *; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright (c) 2026 dexpace and Omar Aljarrah
#
# Licensed under the MIT License. See LICENSE in the project root.
# SPDX-License-Identifier: MIT

# Consumer ProGuard/R8 keep rules for sdk-serde-jackson.
#
# JacksonSerde is the public entry point (withDefaults() / from(ObjectMapper)). The module also
# registers a custom module that teaches Jackson how to (de)serialize Tristate; both the entry
# point and that module are reached reflectively through Jackson's module-registration and
# bean-introspection machinery, so they must survive shrinking.
-keep class org.dexpace.sdk.serde.jackson.JacksonSerde { *; }
-keep class org.dexpace.sdk.serde.jackson.JacksonObjectMappers { *; }
-keep class org.dexpace.sdk.serde.jackson.TristateModule { *; }

# Jackson databind is reflection-heavy: it reads annotations, walks bean members, and resolves
# parametric types at runtime, and its own config classes initialise from annotation enum
# singletons (a stripped or renamed enum value surfaces as an NPE in SerializationConfig's static
# initialiser). It is not meaningfully shrinkable without a hand-curated configuration, so the
# conventional — and the only safe — consumer recommendation is to keep the databind, core, and
# annotation packages wholesale, retain the attributes Jackson reflects over, and keep every
# annotation enum intact.
#
# Scope note: because this file ships under META-INF/proguard, R8/AGP applies it to the consumer's
# entire program, not just the SDK's classes. The wholesale Jackson `-keep` rules below therefore
# exempt the consumer's *entire* Jackson surface from shrinking — including any Jackson the app uses
# directly, elsewhere. The `-keepattributes` directive is likewise global: it adds these attributes
# to the whole consumer build, not only to SDK classes. An app that wants tighter shrinking can
# override these rules in its own configuration.
-keepattributes Signature,*Annotation*,EnclosingMethod,InnerClasses
-keep class com.fasterxml.jackson.databind.** { *; }
-keep class com.fasterxml.jackson.core.** { *; }
-keep class com.fasterxml.jackson.annotation.** { *; }
-keep enum com.fasterxml.jackson.** { *; }
-dontwarn com.fasterxml.jackson.databind.ext.**
Loading
Loading