Skip to content

Keep name-resolved resources from the Android resource shrinker - #100

Merged
ofalvai merged 1 commit into
bitrise-io:masterfrom
adamsp:keep-name-resolved-resources
Nov 3, 2026
Merged

ofalvai merged 1 commit into
bitrise-io:masterfrom
adamsp:keep-name-resolved-resources

Conversation

@adamsp

@adamsp adamsp commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Uh, sorry about the Claude slop below. This just adds a codepush_keep.xml so that AGP 9's on-by-default R8 resource shrinking keeps the resources CodePush looks up using getIdentifier. I've added one for us locally, but figured I'd upstream it. This fixes two issues which we encountered:

  • users with a local CodePush bundle find themselves in a crash loop on launch after receiving a build built with AGP 9 (we encountered this after updating to RN 0.88 / Expo 58)
  • the CodePushServerUrl is missing which means users can no longer receive code pushes (😱)

Problem

CodePush looks several resources up by name through Resources.getIdentifier:

  • CODE_PUSH_APK_BUILD_TIME, generated by codepush.gradle as a resValue and read in CodePush.getBinaryResourcesModifiedTime
  • CodePush<Property> strings and bools from the app's own resources (CodePushServerUrl, CodePushPublicKey, CodePushDeploymentKey, ...) via getCustomPropertyFromStringsIfExist / getBooleanCustomPropertyFromStringsIfExist

The Android resource shrinker cannot trace getIdentifier calls. The legacy shrinker compensated in safe mode by keeping any resource whose name appeared as a string constant in dex, but the optimized shrinker that Android Gradle plugin 9 enables by default (android.r8.optimizedResourceShrinking=true) dropped that heuristic. Expo SDK 58 / React Native 0.88 apps pick this plugin up, and any app with shrinkResources true then loses these resources from its release build.

The consequences:

  • Crash loop on upgrade. getBinaryResourcesModifiedTime runs whenever a downloaded package exists on disk, and the missing resource makes it throw:

    com.microsoft.codepush.react.CodePushUnknownException: Error in getting binary resources modified time
    Caused by: android.content.res.Resources$NotFoundException: String resource ID #0x0
        at com.microsoft.codepush.react.CodePush.getBinaryResourcesModifiedTime
        at com.microsoft.codepush.react.CodePush.isPackageBundleLatest
        at com.microsoft.codepush.react.CodePush.getJSBundleFileInternal
    

    Every user who upgrades the store build while holding a previously downloaded update crashes on cold start, before clearUpdates() gets a chance to run. A fresh install is unaffected, which makes it easy to miss in release testing.

  • Silent loss of the server URL. With CodePushServerUrl removed, the runtime falls back to the hardcoded default and never contacts the configured server.

The resource shrinker emits no warnings for any of this. The only trace is build/outputs/mapping/release/resources.txt listing the resources as is not reachable.

Fix

Ship a tools:keep in the library's own res/raw. Library resources merge into the consuming app before shrinking, so every consumer is covered without changes on their side. This is the same approach Firebase uses for its name-resolved strings (firebase_common_keep.xml).

<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@string/CODE_PUSH_APK_BUILD_TIME,@string/CodePush*,@bool/CodePush*" />

The wildcards cover every CodePush<Property> the runtime can ask for. Names an app does not define are ignored by the shrinker. The keep file itself is shrunk out of the final APK as unreachable, which is expected.

Verification

Reproduced in a React Native 0.88 / Expo SDK 58 app with minifyEnabled true and shrinkResources true. Before the keep, resources.txt reported:

string:CODE_PUSH_APK_BUILD_TIME:... is not reachable.
string:CodePushServerUrl:... is not reachable.

With an equivalent keep file in the app module, the same build reports both as reachable from keep xml file, aapt2 dump resources shows the values in resources.arsc, and the upgrade crash no longer reproduces.

🤖 Generated with Claude Code

CodePush reads CODE_PUSH_APK_BUILD_TIME and the CodePush<Property>
strings and bools through Resources.getIdentifier. The resource shrinker
cannot trace those lookups, and the optimized shrinker that Android
Gradle plugin 9 turns on by default (android.r8.optimizedResourceShrinking)
no longer keeps resources whose names appear as string constants in dex,
which the legacy shrinker's safe mode did.

With shrinkResources enabled on such a build:

- CODE_PUSH_APK_BUILD_TIME is removed, so getBinaryResourcesModifiedTime
  throws Resources$NotFoundException ("String resource ID #0x0") whenever
  a downloaded package exists on disk. Every user who upgrades the app
  with a previously downloaded update crash-loops on cold start, before
  clearUpdates() can run.
- CodePushServerUrl is removed, so the runtime silently falls back to
  the default server URL and never reaches the configured one.

Ship a tools:keep in the library's res/raw so every consumer is covered
when the AAR's resources merge into the app, the same way Firebase keeps
its own name-resolved strings (firebase_common_keep.xml).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@ofalvai ofalvai left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the thorough investigation @adamsp (and congrats on PR #100).
I'm merging this, and #105 too, as a follow-up. This made me realize we could have detected this issue in tests 🤦

@ofalvai
ofalvai merged commit ab7e284 into bitrise-io:master Nov 3, 2026
6 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.

2 participants