Skip to content

Experiment: Expo SDK 57 example with precompiled RN XCFrameworks + CI build timing - #324

Merged
mfazekas merged 16 commits into
feat/rive-ios-experimentalfrom
feat/expo57-prebuilt-xcframeworks
Jul 9, 2026
Merged

mfazekas merged 16 commits into
feat/rive-ios-experimentalfrom
feat/expo57-prebuilt-xcframeworks

Conversation

@mfazekas

@mfazekas mfazekas commented Jul 7, 2026 •

Copy link
Copy Markdown
Collaborator

Adds an expo57-example workspace (Expo SDK 57 / RN 0.86) and makes it the default iOS build on CI, using Expo's precompiled React Native XCFrameworks (https://expo.dev/blog/faster-ios-builds-with-precompiled-xcframeworks) plus the EAS prebuilt third-party bucket (reanimated/worklets; unofficial endpoint with automatic source fallback).

  1. build-ios now builds expo57-example with caching (exact-key short-circuit, Pods cache, incremental build-dir cache). Cold xcodebuild is ~8 min vs ~26 min median for the bare example; timing details in the comments below.
  2. test-harness-expo57-ios runs the shared harness suite against the Expo app. This needed the unstable_path asset publicPath (out-of-root shared assets) and a yarn patch on @react-native-harness/bundler-metro — its entry-point swap didn't recognize Expo's server-root-relative/virtual-entry requests, so the real app booted instead of the harness host (worth upstreaming).
  3. The bare RN 0.79 example keeps its coverage via the existing test-harness-ios/-legacy jobs, which build it themselves.
  4. expo57-example drops the create-expo-app theme boilerplate; its workspace typecheck is clean.

Verified locally (app renders, Quick Start data binding works, all 4 harness suites pass) and on CI (all jobs green).

mfazekas added 2 commits July 7, 2026 13:27
…d-timing job

Expo SDK 57 (RN 0.86) example modeled on expo55-example; iOS builds use
Expo's precompiled React Native XCFrameworks + precompiled Expo Modules
by default. The uncached build-expo57-ios CI job measures cold pod
install + xcodebuild times for comparison against existing build-ios runs.

The babel config converts builder-bob's string include matcher to a
function: SDK 57's @expo/metro-config loads the config without a
filename when computing its transform cache key, and Babel rejects
string/RegExp matchers there.
@mfazekas

mfazekas commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

First timing results from run 28862816990 (both jobs cold, same runner pool, macos-15 / Xcode 26.3):

job pod install xcodebuild (Debug, sim)
build-expo57-ios (precompiled RN core + Expo modules) 115s 1013s (16.9 min)
build-ios (bare example, RN core from source, same run) 99s 1057s (17.6 min)

Historical cold build-ios runs on main ranged 26–29 min of xcodebuild (runs 28794160443, 28573482962, 27693800321), so there's substantial runner variance; the same-run pair above is the fairest comparison.

Reading: the expo57 example compiles a much larger dependency set from source (reanimated, worklets, screens, gesture-handler, router native bits — third-party precompiled pods are EAS-only) plus RNRive/Rive, and still comes in slightly under the bare example. The precompiled RN core + Expo modules roughly offset the entire extra Expo/third-party surface. A few more runs will tighten the numbers.

@mfazekas

mfazekas commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Second build-expo57-ios sample (rerun of the same commit, attempt 2): pod install 118s, xcodebuild 1332s (22.2 min) — vs 16.9 min in attempt 1, so runner variance clearly affects this job too.

Standings across all cold samples: expo57 16.9 / 22.2 min (n=2, mean ~19.5) vs bare example 17.6–31.3 min (n=9, median ~27). Both expo57 samples sit below the bare-example median even though the expo app compiles a much larger third-party set from source.

…ld-expo57-ios

Sets EXPO_PRECOMPILED_MODULES_BASE_URL to the public bucket EAS Build
uses, so reanimated and worklets install as prebuilt XCFrameworks
instead of compiling from source. Unofficial endpoint; on 404 or outage
pod install falls back to source builds. Verified locally: both download
and link, app boots and renders the Quick Start demo.
@mfazekas

mfazekas commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

With the EAS bucket enabled (run 28877621406): pod install 115s, xcodebuild 642s (10.7 min) — down from 16.9/22.2 min without third-party prebuilds. CI log confirms RNReanimated and RNWorklets installed as prebuilt XCFrameworks; screens/safe-area-context/async-storage 404'd for this version tuple and built from source as expected.

Cold-build summary so far: bare example median ~27 min (n=9) → expo57 precompiled RN core ~17–22 min (n=2) → expo57 + EAS third-party bucket ~10.7 min (n=1).

mfazekas added 2 commits July 8, 2026 06:29
Adds harness wiring to expo57-example (rn-harness config with the
expo-router entry, jest roots pointing at the shared example tests,
harness babel preset) plus a test-harness-expo57-ios CI job that builds
the app with precompiled XCFrameworks + the EAS prebuilt bucket.

Two fixes were needed to run the harness against an Expo app:

1. Out-of-root shared assets (../example/assets) 404 through the
   harness's plain Metro server because URL normalization strips the
   leading '..' from classic asset paths; expo57's metro config now uses
   the unstable_path publicPath form, which Metro's server handles.
2. The harness entry-point resolver only swaps the app entry for the
   harness runtime when the request textually matches the configured
   entryPoint and originates from the project root. Expo apps request
   the entry via Expo's server-root-relative URL (or the
   .expo/.virtual-metro-entry alias), so the swap never fired and the
   real app booted instead of the harness host — every render() then
   timed out. Patched @react-native-harness/bundler-metro (yarn patch)
   to accept monorepo server-root origins and the Expo virtual entry.

All 4 shared suites pass locally against the expo57 app.
@mfazekas

mfazekas commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

test-harness-expo57-ios passed on its first CI run (run 28919441701): the full shared harness suite (4 suites) runs green against the Expo 57 app.

Timings: expo prebuild 6s, pod install 93s (reanimated + worklets prebuilt from the EAS bucket), xcodebuild 8m05s, harness tests 4m18s — 21m13s total, uncached. For scale: the bare test-harness-ios/-legacy jobs in the same run (cold, same runner pool) were still building/running past the 40-minute mark when this one finished.

Two fixes were needed to run the harness against an Expo app (details in commit b3957da): the unstable_path publicPath form for out-of-root shared assets, and a yarn patch on @react-native-harness/bundler-metro so the entry-point swap recognizes Expo's server-root-relative / virtual-entry requests — worth upstreaming to Callstack.

mfazekas added 4 commits July 8, 2026 08:25
The gallery only needs plain View/Text; removes components/, hooks/,
constants/, the @/ path alias, and the unused expo-symbols and
@expo/vector-icons deps. Also fixes the TS 6 baseUrl deprecation and
the pre-existing boilerplate typecheck errors — the workspace
typecheck is clean now.
Replaces the bare-example build-ios job and the experimental
build-expo57-ios job with a single build-ios job that builds
expo57-example (precompiled RN XCFrameworks + EAS prebuilt pods) with
caching: an exact-key lookup-only probe short-circuits the whole job
when inputs are unchanged, a Pods cache covers pod install, and a
prefix-fallback build-dir cache makes changed-input builds incremental.
The bare example is still built and exercised by the test-harness-ios
jobs.
Renames test-harness-expo57-ios to test-harness-ios and switches every
harness job (ios/android, experimental/legacy) to the Expo 57 example,
with prebuild + caching (iOS build cache shared with build-ios; legacy
variants use separate cache keys). Adds the android runner to the
expo57 harness config.

Verified locally against expo57 on both backends and both platforms:
all 4 shared suites pass in each combination. Gotcha: the
prebuild-generated android gradle.properties has no trailing newline,
so the legacy flag must be appended with a leading newline or it glues
onto the previous property and silently does nothing.
… feat/expo57-prebuilt-xcframeworks

# Conflicts:
#	.github/workflows/ci.yml
@mfazekas

mfazekas commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Final matrix after consolidating all harness jobs onto expo57-example (run 28925999512, all cold — cache keys changed):

job app build tests total
test-harness-ios 12m14s 2m24s 24m35s
test-harness-ios-legacy 18m06s 5m04s 38m49s
test-harness-android 12m06s 3m18s 19m25s
test-harness-android-legacy 12m39s 3m21s 20m15s

All four passed on the first harness attempt. Notes: Android cold builds are ~4.5 min slower than the bare example's (~7.5 min) — the added Expo surface costs even with prebuilt Expo modules — but the test phase is faster and stabler (3m vs 11m with retries), so totals are a wash, and warm-cache runs skip the build entirely. Android already gets prebuilt RN core (Maven, since 0.71) and prebuilt Expo modules (SDK 53+ default, 15/18 modules as Maven artifacts in our build); there is no Android equivalent of the EAS third-party bucket.

mfazekas added 7 commits July 8, 2026 10:51
build-android now builds expo57-example with the same cache key as
test-harness-android (exact-key short-circuit + cache warmer),
mirroring build-ios. The bare RN example moves to a new bare-example
workflow: weekly schedule + manual dispatch + PRs touching example/**,
building iOS and Android on both Rive backends (build-only).
Replaces the default create-expo-app icons with an icon set generated
from the official Rive R mark (GitHub org avatar, supersampled and
re-thresholded for clean edges at 1024px): app icon, light/dark splash
icons, Android adaptive foreground/background/monochrome, and favicon.
Also removes the unused React logo assets.
…ebug logs

Mitigation for ios-harness-startup-hang (#328): the harness sometimes
wedges after Metro starts but before launching the app, and plain
process cleanup between retries leaves the wedged CoreSimulator in
place, so all attempts hang identically. nuke_harness now kills
CoreSimulatorService and reboots the simulator. HARNESS_DEBUG=1 will
show the exact hang point if it recurs.
… feat/expo57-prebuilt-xcframeworks

# Conflicts:
#	.github/workflows/ci.yml
… assets

The icon is now a proper tile: dark gradient background with the React
atom orbits in React blue and the Rive R as the nucleus (Android
adaptive foreground/background and favicon follow the same design;
monochrome and splash stay plain R for legibility).

Also removes expo57-example/assets/rive/ — nothing referenced those
copies; all shared pages and tests load .riv files from
example/assets/rive through Metro, and the harness suite passes after
the removal.
Keeps the full React atom orbits but knocks them out along the (dilated)
R silhouette instead of a circular hole, so the R can be much larger
without erasing the orbit crossings. Verified legible down to 60px and
on the Android launcher.
Asset-only changes (icons, fonts, the expo-asset-embedded .riv) are
baked into the app binaries but were not part of the cache keys, so
such pushes reused stale cached builds.
@mfazekas
mfazekas merged commit afe8c80 into feat/rive-ios-experimental Jul 9, 2026
21 of 22 checks passed
@mfazekas
mfazekas deleted the feat/expo57-prebuilt-xcframeworks branch July 9, 2026 07:44
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