Skip to content

Latest commit

 

History

History
90 lines (69 loc) · 5.29 KB

File metadata and controls

90 lines (69 loc) · 5.29 KB

Native build sync

ios/ and android/ are generated state, not durable source. They drift from their durable inputs whenever a branch switch, a config plugin edit, or a new native source file lands — and the failure is confusing: Xcode cannot resolve a Swift type that is plainly sitting in modules/vescape-core/ios/.

scripts/native-sync.ts fingerprints the durable inputs, compares them with the last successful sync, and runs only the sync step that is actually stale. It is wired into the blessed run commands, so there is nothing extra to remember:

bun run ios       # native:sync ios && expo run:ios --device
bun run android   # device picker (phones only) -> native:sync android && expo run:android
bun run native:sync ios   # sync only, no app run

bun run android:release is the same Android flow at --variant release: the JS is Hermes-compiled and bundled into the APK instead of served by Metro, so the app runs at production speed. It still reads .env.local, so the application id keeps the .dev suffix and the server URL stays whatever local dev points at — it installs straight over the debug build (both sign with the debug keystore locally; only Fastlane supplies the upload keystore). Use it whenever a debug build is too slow to judge, camera and map motion especially. Rebuilding is the only way to pick up a JS change.

Output always names the reason before it does anything (+ added, - removed, ~ changed):

native-sync ios: pod install refreshes the Pods project because:
  ~ modules/vescape-core/ios#layout

Three scopes

Shared assets (Android only) — content hashes of everything copy:shared reads out of shared/ (alert audio, cell presets), plus a check that each expected copy is actually on disk. Drift or a deleted copy re-runs copyShared(). Steps run in order, so this lands before any prebuild.

Prebuild (both platforms) — content hashes of app.config.ts, package.json, bun.lock, plugins/, patches/, every modules/*/expo-module.config.json and modules/*/package.json, plus targets/ on iOS (@bacons/apple-targets copies those into the Xcode project) and watch/ on Android (withWearMirror copies the Wear OS Mirror into android/wearos/). Any change here means the generated native project is stale, so the script runs expo prebuild --platform <p>.

Because watch/ is a prebuild input, editing the Wear Mirror is enough: the next bun run android or bun run wear:* re-runs prebuild, and the plugin re-copies the source. Nothing hand-copies android/wearos/.

Pods (iOS only) — content hashes of modules/*/ios/*.podspec, plus a hash of the sorted file path list under each modules/*/ios/. CocoaPods compiles whatever the podspec globbed at pod install time, so a newly added Swift file stays invisible to Xcode until Pods are regenerated. Editing an already-compiled file needs no refresh, which is why this scope hashes paths, not contents — normal Swift work never pays for a pod install.

On iOS the two scopes chain: a prebuild regenerates the Podfile, and expo prebuild does not reliably install from it, so every iOS sync ends with pod install. It is a fast no-op when Pods already match.

The same drift hits the Swift test target

bun run test:ios builds modules/vescape-core/Package.swift, which globs ios/ instead of listing files, so a new test needs no manifest edit. SwiftPM memoizes the evaluated manifest keyed on Package.swift's contents, though, and a new file leaves those contents identical — the stale memo would keep the old source list and the new test would silently never run. scripts/test-ios.ts therefore reuses podsFingerprint() (the same sorted-path-list signal Pods drift on) and drops the SwiftPM manifest memo when the layout moves. Its own fingerprint lives in .expo/test-ios/.

Why the scopes are platform-lopsided

Android has no Pods equivalent: Gradle autolinking resolves Kotlin sources through a directory glob at build time, so adding a file under modules/*/android/ needs no regeneration step.

iOS has no shared-asset copy: shared/ is the single source of truth for alert audio and cell presets, and iOS reads it through committed symlinks under modules/vescape-core/ios/ (the podspec globs alerts/*.wav and cell-presets.json). Gradle cannot follow a symlink out of the module, so Android is the only platform that needs real copies — and those copies are generated state: gitignored, and regenerated by copy:shared.

Never commit the copies under modules/vescape-core/android/src/main/res/raw/ (or assets/data/, test/resources/data/). They duplicate shared/ byte for byte and go stale silently. Build paths that skip native-sync get them from the postinstall hook, and CI/release call copy:shared explicitly.

Cache

The last successful fingerprint lives in .expo/native-sync/<platform>.json (gitignored, so it is per-machine and per-checkout). It is written only after the sync command exits 0, and it fingerprints the tree after the sync, because prebuild can rewrite its own inputs. A missing cache — a fresh clone, or after bun run clear — is treated as unknown state and triggers a prebuild.

To change what counts as a durable native input, edit the input lists at the top of scripts/native-sync.ts. Never hand-edit ios/ or android/ to fix drift.