ios-simulator-build

Installation
SKILL.md

iOS simulator build

Turns the Expo app into a Release simulator app with one command, npm run build:ios:sim -- --variant preview, and proves what was built: the pinned Xcode made it, the variant is the one asked for (test-only code present in development and preview, absent in production), and the app starts and draws on a simulator this repo owns. No Xcode GUI, no Metro, no signing. Verified on 2026-10-04 with Expo SDK 57 (expo 57.0.26), React Native 0.86.3, Xcode 26.6 and the iOS 26.5 simulator runtime; the Android twin is android-emulator-build.

Rules that must hold

  1. Every simulator build runs through one script and starts from a clean prebuild. scripts/build/build-ios-sim.ts preflights every npm script it will run (exit 2, nothing built), then npx expo prebuild --platform ios --clean, the contract's post-prebuild scripts, xcodebuild, install, launch and screenshot. Why: ios/ is generated (Continuous Native Generation, owned by expo-prebuild-and-config-plugins) and a stale ios/ from another variant builds the wrong identity; a missing audit script must stop the run before the slow, destructive prebuild.
  2. Xcode is selected by version through DEVELOPER_DIR, per process; never xcode-select or sudo. scripts/ios/toolchain.ts pins XCODE_VERSION and IOS_RUNTIME; selectXcode() picks the matching /Applications/Xcode*.app and asserts xcodebuild -version. Why: a Mac often holds several Xcodes (the newest is the one named Xcode.app), and one global switch needs a password and changes every build on the machine.
  3. The variant is exported once, for the whole run. variantEnv(variant) from src/config/app-variant.ts sets APP_VARIANT = EXPO_PUBLIC_APP_VARIANT on the one environment every step gets. Why: the variant enters the binary three times, each in its own process: prebuild (Info.plist), the Constants build phase (EXConstants.bundle/app.config) and Metro (the JS bundle); a lost variable silently mixes variants.
  4. Metro's cache is keyed on the public values before any Release build. metro.config.js sets cacheVersion from the EXPO_PUBLIC_* values (the key and its proof belong to app-variants-and-environment). Why: this build runs with CI=1, and @expo/cli then skips --reset-cache in the Release bundling phase, so without the key a production build made after a preview build can ship the preview bundle.
  5. The simulator build is Release, for the simulator SDK, on one UDID, unsigned. -configuration Release -sdk iphonesimulator -destination id=<udid> ONLY_ACTIVE_ARCH=YES ARCHS=arm64 CODE_SIGNING_ALLOWED=NO, DerivedData in build/ios-sim/dd. Why: Release embeds the Hermes bundle users run; expo start, expo run:ios and Debug builds load JS from Metro with a dev menu, and no signing material is ever touched.
  6. Simulators are named <prefix>-<purpose>, created once on the pinned runtime and targeted only by UDID. Only prefixed simulators are shut down, erased or deleted, one at a time, never all or booted; Maestro runs pass --device <udid> and their own driver port; Android tooling passes adb -s <serial> and starts emulators with -avd <name>. Why: several sessions, agents and CI jobs share a Mac, and an implicit device reaches someone else's.
  7. Every tooling process runs with EXPO_NO_TELEMETRY=1 and CI=1. Why: no telemetry, and no interactive prompt that would hang an unattended run.
  8. Wait on a signal, never a fixed sleep; then check the screenshot and look at it. The ready signal is the contract's readyMarker file in the app's data container, else two identical screenshots after at least 3 s. Why: a build that "succeeded" can still launch to a black, white or empty screen; only the picture shows it.
  9. The built .app proves its variant. check-sim-app reads the artifact: the pinned Xcode (DTXcode), the simulator platform, the test-only sentinel (present in development and preview, absent in production), Info.plist and EXConstants agreeing on the variant's bundle id, usage strings in every language, the privacy manifest, no ATS exception, and for production no placeholder, no test artefact and the encryption flag. Why: ** BUILD SUCCEEDED ** says nothing about what shipped inside.
  10. A change to variants, Metro, test-only code or the build tooling is proven by a preview build and a production build made back to back, without clearing any cache. Why: only that order shows a missing cache key or a broken gate.
  11. Human steps are asked for, never worked around. Installing Xcode, accepting its licence and its first-launch setup need sudo and an admin password; the agent stops and asks. Why: a workaround (another Xcode, xcode-select) builds with the wrong SDK and hides the problem.
  12. Generated and output folders stay out of git: ios/, android/, build/ and reports/. Why: prebuild regenerates the native folders, build/ holds DerivedData and logs, reports/ the screenshots.
Installs
5
GitHub Stars
12
First Seen
13 days ago