Skip to content

feat(r8): ship keep rules for the generated messages - #5

Merged
bmc08gt merged 3 commits into
mainfrom
chore/ship-r8-keep-rules
Aug 31, 2026
Merged

feat(r8): ship keep rules for the generated messages#5
bmc08gt merged 3 commits into
mainfrom
chore/ship-r8-keep-rules

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

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. Between them 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 none of that was needed. javalite resolves fields reflectively: the schema built from
newMessageInfo looks up Field objects against the generated <name>_ fields. Builders and
message methods are reached from ordinary call sites, so R8 traces them without a rule.
tink-android already ships exactly this narrower form for its own shaded copy of javalite.

-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
    <fields>;
}

-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.

Why it is 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 —
firebase-perf is one. A rule scoped to the generated package would leave both broken under
R8 full mode. Measured against the app, the rule's radius spans four artifacts:

Artifact Fields kept
flipcash2-client-protocol 1,721
ocp-client-protocol 1,193
firebase-perf 138
protobuf-javalite 20

Both contract packages ship identical rule text and R8 collapses them into a single entry, so
the duplication costs nothing and either artifact alone is sufficient.

Effect on the consumer

Built through protoLocalRoot, with the app's own protobuf rules removed:

live methods    230,686 -> 205,144   (-25,542)
live classes     42,338 ->  41,770      (-568)
live fields      96,939 ->  95,692    (-1,247)

obfuscation      89.0% -> 96.1%
optimization     89.3% -> 96.3%
shrinking        89.1% -> 96.1%

Owning the rule here also means an app that adds this dependency gets working protobuf under
minification without knowing any of the above.

Consumer side: code-payments/code-android-app#1370

Release

Ships as 0.3.0. Merged main in and added the ## 0.3.0 changelog section — publish.yml
uses it as the release notes and refuses to publish a version without one — and pointed the
README install snippets at it. No contract change, so the Swift tag carries nothing new.

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 `<name>_` 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.
* origin/main:
  chore(contract): sync ocp protos to ea6418c5 and add a changelog (#4)
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.
@bmc08gt
bmc08gt merged commit 7c37ecc into main Aug 31, 2026
3 checks passed
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.

1 participant