Appearance
macOS · iOS build environment
Bootstrapping the Mac so it can build, run and verify the merchant app on iOS. Companion to Windows Build Environment (the Android side) and Parity Verification (what to check once it runs).
Why this exists
iOS native moved from R2 into R1 on 2026-07-26 (ADR-0011 amendment). Every change is now verified on Android native, iOS native and the Web PWA before it is done, so the Mac is part of the normal loop, not a release-time afterthought.
No Apple Developer Program membership is needed to develop
This is the fact that made moving iOS into R1 free:
| Capability | Free (Personal Team) | Needs $99/yr |
|---|---|---|
| iOS Simulator | ✅ no Apple account at all | |
| Install on your own iPhone | ✅ Personal Team signing | |
| Profile validity | 7 days, then the app refuses to launch — re-run to re-sign | 1 year |
| Apps per device | 3 | unlimited |
| TestFlight / App Store | ✅ | |
| Push notifications | ✅ | |
| Associated Domains (iOS universal links) | ✅ — this is why qrsetu:// is the iOS interim scheme |
Free signing is fine for development and parity verification, and unusable for sustained parallel QA. Buy the membership when distribution is needed, not before.
Prerequisites
Per the CLAUDE.md toolchain rule — verify each is present at the right version before running any build step.
A clean macOS has no shell profile — do this first
~/.zshrc and ~/.zprofile do not exist on a fresh Mac. The nvm and Homebrew installers only append to a profile that already exists, so without these two lines they report "profile not found", wire themselves into nothing, and every following command fails with command not found. This costs an hour if you meet it blind.
bash
touch ~/.zshrc ~/.zprofile1 · Xcode
Install from the App Store, then:
bash
sudo xcodebuild -license accept
xcode-select -p # must print /Applications/Xcode.app/Contents/Developer
xcodebuild -version # must be 16.1+ (RN 0.86's floor)At Xcode's first-launch platform prompt, select iOS and macOS only. Deselect watchOS, tvOS and visionOS — you will never build for them and they cost 20–30 GB, which is real money on a 256 GB machine.
Re-check xcode-select -p after ANY Command Line Tools install
Installing the CLT — including as a side effect of installing Homebrew — often repoints the active developer directory at /Library/Developer/CommandLineTools. pod install and expo prebuild then fail with errors that look like Expo bugs and are not. Fix:
bash
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer2 · Node 22, via nvm
nvm does not ship with macOS:
bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
source ~/.zshrc
command -v nvm # must print "nvm"nvm is a shell function, not a binary — which nvm prints nothing even when it works. Always use command -v nvm.
Then, from the repo root (nvm install with no argument reads .nvmrc, which contains 22):
bash
nvm install && nvm use && node -v3 · Homebrew, then the build tools
bash
command -v brew || /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Apple Silicon installs to /opt/homebrew, which is NOT on PATH by default:
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
brew --version
brew install gh cocoapods watchmanThe Homebrew installer asks for your macOS password (sudo) and may pull the Command Line Tools — see the xcode-select warning above.
4 · GitHub access
The repo is private, so authentication is needed even to clone. GitHub has not accepted passwords for git over HTTPS since 2021 — use the CLI's browser flow, which stores and rotates nothing:
bash
gh auth login # GitHub.com → HTTPS → Yes (authenticate git) → Login with a web browser
gh auth statusA fine-grained PAT scoped to this repo is the fallback. Never embed it in the remote URL — it lands in .git/config in plaintext and in your shell history; let the macOS Keychain credential helper hold it.
First build
Do not follow a half-remembered sequence here — the full walkthrough, including the iPhone X iOS-version floor, free code signing, and device install, is iOS Build & Device Testing. Start there once the prerequisites above are in place.
ios/ is generated — never edit it
apps/mobile/ios/ and apps/mobile/android/ are both gitignored. Anything you change inside them is destroyed by the next expo prebuild and never reaches another machine or CI.
So all native configuration lives in app.json (or a config plugin):
expo.ios.bundleIdentifier—in.digious.qrsetu, permanent once publishedexpo.ios.buildNumber— the iOS counterpart ofandroid.versionCode; both are explicit inapp.jsonso the file stays the single source of truth and CI owns the bumpexpo.plugins— splash, Sentry, and anything else needing native changes
This is the same lesson the Android gradle.properties incident taught: a native file edited by hand is a change that exists on exactly one machine.
No build-ios.mjs
Android has scripts/build-android.mjs because Windows has a real pathology to fight — C:-drive artefact spill and Gradle OOM, needing arm64-only builds and capped parallelism. macOS has none of those problems. expo run:ios and xcodebuild work as shipped, so a wrapper script would be pure maintenance debt. Use the Expo commands directly.
Versioning
app.json is the single source of truth for both platforms:
| Field | Platform | Role |
|---|---|---|
expo.version | both | Marketing version (1.0.0) |
expo.android.versionCode | Android | Integer, must increase per Play upload |
expo.ios.buildNumber | iOS | String, must increase per App Store upload |
CI owns the bump. Do not hand-edit these to make a local build work — see the "Versioning" section of apps/mobile/README.md.
Measuring IPA size
The app-size budget applies to iOS too. After a release build, record the IPA size alongside the APK figure in the mobile README's size table. The RN/Hermes native runtime dominates on both platforms, so the numbers should track each other; a sudden divergence means a native module landed on one platform only — which is itself a parity bug.
Troubleshooting (environment only)
Build- and device-level problems are troubleshooted in iOS Build & Device Testing. Only toolchain issues belong here.
Freeing disk space is an environment action with a device-level consequence
Reclaiming space on the Mac can make the iPhone vanish from Xcode's destination list, because the pairing records, signing certificate and device-support symbols all live under paths a size-sorted cleanup will happily delete. Clean only from the allowlist in Cleanup is allowlist-only, and if the phone has already disappeared, work the recovery ladder rather than reinstalling anything. QRS-232.
xcodebuild reports the CommandLineTools path — point it at the real Xcode: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer.
pod not found after brew install cocoapods — open a new shell so the Homebrew path is picked up, or eval "$(/opt/homebrew/bin/brew shellenv)".
Node version drifts between shells — this repo pins Node via .nvmrc; run nvm use in each new terminal, or enable nvm's automatic switching.