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
- Every simulator build runs through one script and starts from a clean prebuild.
scripts/build/build-ios-sim.tspreflights every npm script it will run (exit 2, nothing built), thennpx expo prebuild --platform ios --clean, the contract's post-prebuild scripts, xcodebuild, install, launch and screenshot. Why:ios/is generated (Continuous Native Generation, owned byexpo-prebuild-and-config-plugins) and a staleios/from another variant builds the wrong identity; a missing audit script must stop the run before the slow, destructive prebuild. - Xcode is selected by version through
DEVELOPER_DIR, per process; neverxcode-selectorsudo.scripts/ios/toolchain.tspinsXCODE_VERSIONandIOS_RUNTIME;selectXcode()picks the matching/Applications/Xcode*.appand assertsxcodebuild -version. Why: a Mac often holds several Xcodes (the newest is the one namedXcode.app), and one global switch needs a password and changes every build on the machine. - The variant is exported once, for the whole run.
variantEnv(variant)fromsrc/config/app-variant.tssetsAPP_VARIANT=EXPO_PUBLIC_APP_VARIANTon 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. - Metro's cache is keyed on the public values before any Release build.
metro.config.jssetscacheVersionfrom theEXPO_PUBLIC_*values (the key and its proof belong toapp-variants-and-environment). Why: this build runs withCI=1, and@expo/clithen skips--reset-cachein the Release bundling phase, so without the key a production build made after a preview build can ship the preview bundle. - 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 inbuild/ios-sim/dd. Why: Release embeds the Hermes bundle users run;expo start,expo run:iosand Debug builds load JS from Metro with a dev menu, and no signing material is ever touched. - 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, neverallorbooted; Maestro runs pass--device <udid>and their own driver port; Android tooling passesadb -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. - Every tooling process runs with
EXPO_NO_TELEMETRY=1andCI=1. Why: no telemetry, and no interactive prompt that would hang an unattended run. - Wait on a signal, never a fixed sleep; then check the screenshot and look at it. The ready signal is the contract's
readyMarkerfile 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. - The built
.appproves its variant.check-sim-appreads the artifact: the pinned Xcode (DTXcode), the simulator platform, the test-only sentinel (present in development and preview, absent in production), Info.plist andEXConstantsagreeing 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. - 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.
- Human steps are asked for, never worked around. Installing Xcode, accepting its licence and its first-launch setup need
sudoand an admin password; the agent stops and asks. Why: a workaround (another Xcode,xcode-select) builds with the wrong SDK and hides the problem. - Generated and output folders stay out of git:
ios/,android/,build/andreports/. Why: prebuild regenerates the native folders,build/holds DerivedData and logs,reports/the screenshots.