Skip to content

Commit 2d43fb2

Browse files
doublegateclaude
andcommitted
ci(ios): launch the app in a simulator, not just link it
Every iOS verification so far was compile/link-only, and a binary that links and then traps on launch is indistinguishable from a working one in a build log. ios.yml now boots a simulator, installs the built .app, launches it, and requires the process to still be alive eight seconds later. The liveness check asks the simulator via launchctl list rather than trusting simctl launch's exit code, which has already returned 0 by the time a launch-crash happens. What this does NOT prove is emulation: the app bundles no cartridge, so no ROM has run -- that is a licensing question rather than a CI one. docs/mobile-readiness.md now records the narrowed claim instead of listing the whole runtime question as open. Written blind: this environment has no macOS toolchain, so the runner is the first place any of it executes. If a step is wrong, CI is where that surfaces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent aabcb17 commit 2d43fb2

3 files changed

Lines changed: 95 additions & 3 deletions

File tree

‎.github/workflows/ios.yml‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,82 @@ jobs:
9696
-destination 'generic/platform=iOS Simulator' \
9797
CODE_SIGNING_ALLOWED=NO
9898
99+
# ---------------------------------------------------------------------------------------
100+
# Actually RUN it (`v1.30.0`).
101+
#
102+
# Everything above proves the Swift/Rust bridge COMPILES AND LINKS. It has never proved the
103+
# app starts: `docs/mobile-readiness.md` records that no iOS build has ever executed,
104+
# because this environment has no macOS toolchain and this runner is the only place one
105+
# exists. A binary that links and then traps on launch is indistinguishable from a working
106+
# one in a build log.
107+
#
108+
# This does NOT boot a ROM. The app bundles no cartridge and adding one is a licensing
109+
# question, not a CI one. What it proves is narrower, and worth stating exactly: the app
110+
# installs, launches, and is still alive several seconds later.
111+
# ---------------------------------------------------------------------------------------
112+
- name: Pick an available iPhone simulator
113+
id: sim
114+
run: |
115+
set -euo pipefail
116+
xcrun simctl list devices available
117+
udid=$(xcrun simctl list devices available \
118+
| grep -E '^[[:space:]]+iPhone' \
119+
| head -1 \
120+
| grep -oE '[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}')
121+
if [ -z "$udid" ]; then
122+
echo "::error::no available iPhone simulator on this runner"
123+
exit 1
124+
fi
125+
echo "udid=$udid" >> "$GITHUB_OUTPUT"
126+
echo "using simulator $udid"
127+
128+
- name: Build for that simulator and install
129+
working-directory: ios
130+
env:
131+
UDID: ${{ steps.sim.outputs.udid }}
132+
run: |
133+
set -euo pipefail
134+
xcrun simctl boot "$UDID" || true
135+
xcrun simctl bootstatus "$UDID" -b
136+
xcodebuild build \
137+
-project RustySNES.xcodeproj \
138+
-scheme RustySNES \
139+
-destination "platform=iOS Simulator,id=$UDID" \
140+
-derivedDataPath build \
141+
CODE_SIGNING_ALLOWED=NO
142+
app=$(find build/Build/Products -maxdepth 2 -name '*.app' | head -1)
143+
if [ -z "$app" ]; then
144+
echo "::error::no .app bundle was produced"
145+
exit 1
146+
fi
147+
echo "APP=$PWD/$app" >> "$GITHUB_ENV"
148+
xcrun simctl install "$UDID" "$app"
149+
150+
- name: Launch it, and require it to still be alive
151+
env:
152+
UDID: ${{ steps.sim.outputs.udid }}
153+
run: |
154+
set -euo pipefail
155+
bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Info.plist")
156+
echo "bundle id: $bundle_id"
157+
xcrun simctl launch "$UDID" "$bundle_id"
158+
sleep 8
159+
# Ask the SIMULATOR whether the process is still there. A crash on launch is exactly the
160+
# case that links cleanly and then dies, and `simctl launch` has already returned 0 by
161+
# then, so its exit code cannot catch it.
162+
if xcrun simctl spawn "$UDID" launchctl list | grep -q "$bundle_id"; then
163+
echo "ok: still running 8s after launch"
164+
else
165+
echo "::error::the app is not running 8s after launch -- it started and died"
166+
exit 1
167+
fi
168+
169+
- name: Shut the simulator down
170+
if: always()
171+
env:
172+
UDID: ${{ steps.sim.outputs.udid }}
173+
run: xcrun simctl shutdown "${UDID:-booted}" || true
174+
99175
# TestFlight upload is gated on distribution-signing secrets being configured at all --
100176
# skipped (not failed) when they aren't, since this repo has never had them set up. Secret
101177
# names are placeholders for the project owner to actually provision (App Store Connect API

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Added
1313

14+
- **iOS CI now launches the app, not just links it (`v1.30.0`).** `ios.yml` boots a simulator,
15+
installs the built `.app`, launches it, and requires the process to **still be alive eight seconds
16+
later** — asked of the simulator via `launchctl list`, because `simctl launch` has already
17+
returned 0 by the time a launch-crash happens, so its exit code cannot catch one.
18+
19+
Every iOS verification before this was compile/link-only, and a binary that links and then traps
20+
on launch is indistinguishable from a working one in a build log. What this does **not** prove is
21+
emulation: the app bundles no cartridge, so no ROM has run. `docs/mobile-readiness.md` now states
22+
that distinction instead of listing the whole runtime question as open.
23+
1424
- **Android CI, with the 16 KB page-alignment gate (`v1.30.0`).** `android/` had Gradle sources and
1525
a `cargo ndk` layout but **no workflow at all** — nothing built it, so nothing could regress
1626
visibly. `.github/workflows/android.yml` cross-builds `rustysnes-android` for all four ABIs,

‎docs/mobile-readiness.md‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,15 @@ here so a future go/no-go review has a concrete checklist, not a vague "more pol
265265
check, a signed release (not debug) build, a dormant-vs-live Play Billing decision for
266266
`rustysnes-monetization` if monetization is ever actually activated
267267
- **iOS:**
268-
- A real on-device or simulator *run* — every verification so far is compile/link-only (this
269-
development environment has no macOS/Xcode toolchain at all); `ios.yml`'s passing
270-
`xcodebuild` build proves the code compiles, not that it behaves correctly at runtime
268+
- A real on-device *run* with a ROM loaded. `v1.30.0` narrowed this: `ios.yml` now boots a
269+
simulator, installs the app, launches it, and **requires it to still be running eight seconds
270+
later** — a binary that links and then traps on launch is indistinguishable from a working one
271+
in a build log, and that gap is now closed. What is still *not* proven is emulation: the app
272+
bundles no cartridge (a licensing question, not a CI one), so nothing has run a ROM. State the
273+
difference precisely rather than upgrading "it launches" to "it works".
274+
275+
This development environment still has no macOS/Xcode toolchain, so the runner remains the only
276+
place any of it executes.
271277
- TestFlight distribution signing secrets provisioned (the `ios.yml` upload step is currently
272278
an explicit no-op pending these)
273279
- The App Store §4.7 self-audit (user-sourced ROM import only, no Nintendo trademark exposure

0 commit comments

Comments
 (0)