From 6e48a3590d8f9709b541eb0028935dc9a81e3054 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Sun, 30 Aug 2026 13:48:58 -0400 Subject: [PATCH 1/2] feat(r8): ship keep rules for the generated messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit protobuf-javalite ships no keep rules of its own, so every Android consumer has to write one. code-android-app carried `-keep class * extends GeneratedMessageLite { *; }` and the matching $Builder rule; together they pinned 29,633 items — 94% of that app's entire keep radius — and blocked R8 from optimizing 24,909 methods, 81% of every method it was forbidden to touch. 150 of the 461 message classes and 1,215 of the 3,800 fields those rules covered come from this package. Almost all of that was unnecessary. javalite resolves fields reflectively — the schema built from newMessageInfo looks up Field objects against the generated `_` fields — but builders and message methods are reached from ordinary call sites, so R8 traces them without help. tink-android already ships exactly this narrower form for its own shaded copy of javalite. The rule is deliberately not scoped to com.codeinc.opencode.gen.**. The well-known types come from protobuf-javalite itself, and other dependencies ship generated messages with no rules of their own; a scoped rule would leave both broken under R8 full mode. Owning the rule here also means an app that adds this dependency gets working protobuf under minification without knowing any of the above. --- README.md | 5 +++++ .../META-INF/proguard/ocp-client-protocol.pro | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 src/main/resources/META-INF/proguard/ocp-client-protocol.pro diff --git a/README.md b/README.md index 80b1d72..2d1e72e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ implementation("com.flipcash:ocp-client-protocol:0.1.0") `FlipcashAPI/Package.swift` and re-exports the module, so app code still reaches these types through `import FlipcashAPI`. +The Kotlin artifact ships its own R8 keep rules, so an Android consumer needs no protobuf keep +rule of its own. + ## What it contains Four services — `Account`, `Currency`, `Messaging`, `Transaction` — plus the shared @@ -38,6 +41,8 @@ this package was generated from. proto/ contract, synced from upstream at the SHA in ocp.lock proto_deps/validate/ include-path dependency, never generated Sources/OCPClientProtocol/ generated Swift, committed (SPM ships source) +src/main/resources/ + META-INF/proguard/ R8 keep rules, shipped to Kotlin consumers build.gradle.kts Kotlin generation + publishing scripts/ sync-protos.sh pull upstream at a pinned SHA, re-namespace diff --git a/src/main/resources/META-INF/proguard/ocp-client-protocol.pro b/src/main/resources/META-INF/proguard/ocp-client-protocol.pro new file mode 100644 index 0000000..5a0c877 --- /dev/null +++ b/src/main/resources/META-INF/proguard/ocp-client-protocol.pro @@ -0,0 +1,19 @@ +# R8/ProGuard rules for the generated protobuf messages in this artifact. +# +# protobuf-javalite ships no keep rules of its own, so without this every consumer +# app has to write it. javalite resolves fields reflectively: the schema built from +# newMessageInfo looks up java.lang.reflect.Field by the generated `_` field. +# Methods and builders are reached from ordinary call sites, so R8 traces them +# without help and they are deliberately not kept here. +# +# -keepclassmembers does not keep the class, so a message type nothing references +# is still removed entirely. The rule only applies to messages that survive on +# their own merit. +# +# Deliberately not scoped to this artifact's own package. The well-known types +# (Any, Timestamp, Duration, Struct) come from protobuf-javalite itself, and other +# dependencies ship generated messages with no rules of their own; a rule scoped to +# the generated package would leave those broken under R8 full mode. +-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite { + ; +} From b8fb9715f293bf8c9ade6a0edce277861827042e Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Sun, 30 Aug 2026 21:50:16 -0400 Subject: [PATCH 2/2] docs(changelog): describe 0.3.0 for consumers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit publish.yml reads the section matching the version it is publishing, so 0.3.0 could not release without one. The entry leads with what is not in it — no contract change, the lock file unmoved, the generated Swift untouched — because a version number moving usually means the opposite, and a Swift consumer has no reason to take this one. Also points the README install snippets at 0.3.0; they still read 0.1.0. --- CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++ README.md | 4 ++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cd30de..54f9bb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,38 @@ called out explicitly even when nothing else did. release notes, so a version with no entry here does not release. Write the entry in the same PR that syncs the contract, while the diff is still in front of you. +## 0.3.0 + +No contract change. `ocp.lock` points at the same upstream commit as `0.2.0`, and the generated +Kotlin and Swift are unchanged. Swift consumers have nothing to gain from this release. + +### Added + +- The Kotlin artifact now ships R8 keep rules, at `META-INF/proguard/ocp-client-protocol.pro`: + + ```proguard + -keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite { + ; + } + ``` + + protobuf-javalite ships no keep rules of its own, so until now every Android consumer had to + write one, and the obvious `-keep class * extends GeneratedMessageLite { *; }` is far wider + than javalite needs. javalite resolves *fields* reflectively — the schema built from + `newMessageInfo` looks up `java.lang.reflect.Field` by the generated `_` field — while + builders and message methods are reached from ordinary call sites, so R8 traces those without + help. `-keepclassmembers` also does not keep the class, so a message type nothing references is + still removed entirely. + + On upgrading, an Android consumer can delete its own protobuf keep rules. Dropping the wide + pair from `code-android-app` cut 25,542 live methods and 568 live classes, and moved its R8 + optimization score from 89.3% to 96.3%. + + The rule is deliberately not scoped to `com.codeinc.opencode.gen.**`. The well-known types (`Any`, `Timestamp`, + `Duration`, `Struct`) come from protobuf-javalite itself, and other dependencies ship generated + messages with no rules of their own, so a package-scoped rule would leave those broken under R8 + full mode. Both contract packages ship identical rule text; R8 collapses them into one entry. + ## 0.2.0 Synced to [`ocp-protobuf-api@ea6418c5`](https://github.com/code-payments/ocp-protobuf-api/commit/ea6418c5561e16771d456062be2fcbd3ddeb9caf). diff --git a/README.md b/README.md index 2d1e72e..e48ff62 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ They are separate packages because the contracts are: flipcash2 does not import ## Install ```kotlin -implementation("com.flipcash:ocp-client-protocol:0.1.0") +implementation("com.flipcash:ocp-client-protocol:0.3.0") ``` ```swift -.package(url: "https://github.com/code-payments/ocp-client-protocol", from: "0.1.0") +.package(url: "https://github.com/code-payments/ocp-client-protocol", from: "0.3.0") ``` `code-android-app` pins the version in `gradle/libs.versions.toml`. `code-ios-app` pins it in