Skip to content

feat: export embedded bundle from native builds - #157

Draft
floyd-soomgo wants to merge 3 commits into
feature/binary-patch-e2efrom
feature/binary-patch-export-hook
Draft

feat: export embedded bundle from native builds#157
floyd-soomgo wants to merge 3 commits into
feature/binary-patch-e2efrom
feature/binary-patch-export-hook

Conversation

@floyd-soomgo

Copy link
Copy Markdown
Member

Summary

Seventh PR of the binary differential OTA series (stacked on test(e2e): cover platform binary patch updates — merge that first).

The whole feature rests on one invariant: the patch base handed to release --binary-bundle-path must be the exact bundle bytes shipped in the store binary. Until now, producing that file was every team's CI problem. This PR ships the solution in the library: build hooks for both platforms that copy the freshly compiled bundle — before any packaging step could touch it — into an export directory, together with a manifest describing it. A release can then cross-check the base it was given against that manifest and fail loudly on a mixup, instead of shipping a patch that every client would silently fall back from.

Both hooks are opt-in — nothing changes for apps that don't apply them.

Usage

Android — one line in the app module's build.gradle:

apply from: "../../node_modules/@bravemobile/react-native-code-push/android/codepush-export.gradle"

iOS — a Run Script phase after "Bundle React Native code and images":

"${PODS_ROOT}/../../node_modules/@bravemobile/react-native-code-push/scripts/export-embedded-bundle.sh"

Each bundle-producing build then leaves <bundle> + binary-patch-base.json under the platform's build output tree (-PcodePushExportDir / CODEPUSH_EXPORT_DIR to relocate). How the exported artifacts are archived and fetched at release time stays the pipeline's choice.

Changes

  • android/codepush-export.gradle — registers a per-variant export task fed from createBundle<Variant>JsAndAssets's declared outputs (jsBundleDir + bundleAssetName), so no path is hardcoded and the resolved file is the final Hermes-compiled bundle; custom bundleAssetNames survive. Variants that don't bundle get no task at all — the check rides on the RN plugin's own gate rather than re-implementing debuggableVariants parsing. The manifest hashes the exported copy, so the record describes exactly the file a release will be handed.
  • scripts/export-embedded-bundle.sh — copies the RN bundle phase's product (${BUNDLE_NAME:-main}.jsbundle, matching RN's own naming convention); builds that produce no bundle (simulator Debug, SKIP_BUNDLING) exit silently. Versions come from the built product's Info.plist.
  • Manifestbinary-patch-base.json beside the exported bundle: baseBundleHash, binaryVersion, buildNumber, and gitSha when available (its absence never fails a build). This is a superset of the record the bundle command already writes, same filename rule, so the two metafiles stay one schema.
  • release cross-validation — when a manifest sits beside --binary-bundle-path: a SHA-256 mismatch between the file and its record fails the release (wrong-file-in-pipeline protection), a binaryVersion mismatch with --binary-version fails the release, an unparseable manifest warns and proceeds. No manifest → behavior unchanged. All branches unit-tested; the existing bundle-record warning path is untouched.
  • Example app — RN0840 wired on both platforms.
  • Packaging — both hook files ship in the npm package (scripts/ contributes only this one file); README documents integration, overrides, and a pipeline example. An Expo config plugin is noted as a follow-up.

Known limitations

  • The gradle hook is not yet compatible with Gradle's configuration cache (org.gradle.configuration-cache=true); the RN template doesn't enable it, but CC users should hold off applying the hook until a follow-up lands.
  • ext.codePushExportDir must be set before the apply from: line; -PcodePushExportDir has no such ordering constraint.

Test plan

npm run --workspace cli test    # 140 tests, incl. all cross-validation branches
npm run typecheck && npm run jest
npm pack --dry-run              # both hook files included, nothing extra from scripts/

cd Examples/RN0840/android && ./gradlew :app:assembleRelease
# exported bundle SHA-256 == assets/index.android.bundle inside the APK  ✔ (f596e1b9…5043)
# iOS Release simulator build:
# exported bundle SHA-256 == RN0840.app/main.jsbundle                    ✔ (95d4ec66…b956)
# Debug builds on both platforms export nothing and leave the release export intact ✔

A binary patch is computed against the JS bundle inside the app binary, so
releasing one means holding on to exactly the bytes that build embedded.
Producing that file was left to whoever ran the build; these hooks produce it.

An `apply from:` Gradle script gives every variant that bundles JS a task that
copies the bundle out right after it is compiled, and an Xcode build phase script
does the same for iOS. Both read the bundle from the build rather than a fixed
path, and write a `binary-patch-base.json` record beside it - the same record the
`bundle` command writes, plus the binary version, build number and commit that
only a native build knows. Builds that embed no bundle export nothing.
The base bundle is the one input a release cannot verify on its own: hand it the
bundle of a different build and the release still succeeds, only for the patch to
be unappliable on every device.

When a record exported by a build hook sits next to the base bundle, `release`
now reads it: a base file that no longer hashes to what the record describes, or
one exported from a binary version other than `--binary-version`, stops the
release before anything is built or uploaded. A base bundle with no record beside
it releases as before, and a record that cannot be read only warns.
Applies both hooks to the example app, which is what proves they copy the bundle
the binary actually ships: the exported file hashes the same as the one inside
the APK and the built .app.
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