From eb3287bb3c1d0cc2076c1e05284aa20a59716063 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Fri, 31 Jul 2026 18:47:04 -0400 Subject: [PATCH 1/2] 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) --- .github/workflows/ios.yml | 76 +++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 10 ++++++ docs/mobile-readiness.md | 12 +++++-- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 50415b1d..84b07861 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -96,6 +96,82 @@ jobs: -destination 'generic/platform=iOS Simulator' \ CODE_SIGNING_ALLOWED=NO + # --------------------------------------------------------------------------------------- + # Actually RUN it (`v1.30.0`). + # + # Everything above proves the Swift/Rust bridge COMPILES AND LINKS. It has never proved the + # app starts: `docs/mobile-readiness.md` records that no iOS build has ever executed, + # because this environment has no macOS toolchain and this runner is the only place one + # exists. A binary that links and then traps on launch is indistinguishable from a working + # one in a build log. + # + # This does NOT boot a ROM. The app bundles no cartridge and adding one is a licensing + # question, not a CI one. What it proves is narrower, and worth stating exactly: the app + # installs, launches, and is still alive several seconds later. + # --------------------------------------------------------------------------------------- + - name: Pick an available iPhone simulator + id: sim + run: | + set -euo pipefail + xcrun simctl list devices available + udid=$(xcrun simctl list devices available \ + | grep -E '^[[:space:]]+iPhone' \ + | head -1 \ + | grep -oE '[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}') + if [ -z "$udid" ]; then + echo "::error::no available iPhone simulator on this runner" + exit 1 + fi + echo "udid=$udid" >> "$GITHUB_OUTPUT" + echo "using simulator $udid" + + - name: Build for that simulator and install + working-directory: ios + env: + UDID: ${{ steps.sim.outputs.udid }} + run: | + set -euo pipefail + xcrun simctl boot "$UDID" || true + xcrun simctl bootstatus "$UDID" -b + xcodebuild build \ + -project RustySNES.xcodeproj \ + -scheme RustySNES \ + -destination "platform=iOS Simulator,id=$UDID" \ + -derivedDataPath build \ + CODE_SIGNING_ALLOWED=NO + app=$(find build/Build/Products -maxdepth 2 -name '*.app' | head -1) + if [ -z "$app" ]; then + echo "::error::no .app bundle was produced" + exit 1 + fi + echo "APP=$PWD/$app" >> "$GITHUB_ENV" + xcrun simctl install "$UDID" "$app" + + - name: Launch it, and require it to still be alive + env: + UDID: ${{ steps.sim.outputs.udid }} + run: | + set -euo pipefail + bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Info.plist") + echo "bundle id: $bundle_id" + xcrun simctl launch "$UDID" "$bundle_id" + sleep 8 + # Ask the SIMULATOR whether the process is still there. A crash on launch is exactly the + # case that links cleanly and then dies, and `simctl launch` has already returned 0 by + # then, so its exit code cannot catch it. + if xcrun simctl spawn "$UDID" launchctl list | grep -q "$bundle_id"; then + echo "ok: still running 8s after launch" + else + echo "::error::the app is not running 8s after launch -- it started and died" + exit 1 + fi + + - name: Shut the simulator down + if: always() + env: + UDID: ${{ steps.sim.outputs.udid }} + run: xcrun simctl shutdown "${UDID:-booted}" || true + # TestFlight upload is gated on distribution-signing secrets being configured at all -- # skipped (not failed) when they aren't, since this repo has never had them set up. Secret # names are placeholders for the project owner to actually provision (App Store Connect API diff --git a/CHANGELOG.md b/CHANGELOG.md index 1476e88f..5238cf2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 addressing before any assertion branches. Generalised in the plan — rows that hand-load `S` must restore a known-good stack before anything that returns — but it is not the oracle's fix. +- **iOS CI now launches the app, not just links it (`v1.30.0`).** `ios.yml` boots a simulator, + installs the built `.app`, launches it, and requires the process to **still be alive eight seconds + later** — asked of the simulator via `launchctl list`, because `simctl launch` has already + returned 0 by the time a launch-crash happens, so its exit code cannot catch one. + + Every iOS verification before this was compile/link-only, and a binary that links and then traps + on launch is indistinguishable from a working one in a build log. What this does **not** prove is + emulation: the app bundles no cartridge, so no ROM has run. `docs/mobile-readiness.md` now states + that distinction instead of listing the whole runtime question as open. + - **AccuracySNES: the Mesen2 oracle diagnosed, and a stale frame budget aligned.** Three `v1.28.0` items ended at "no oracle can arbitrate", so the headless runner was investigated rather than accepted as environmental. Established: the hang is genuinely pre-existing (the `v1.25.0`-era image diff --git a/docs/mobile-readiness.md b/docs/mobile-readiness.md index 3dc4bbe0..f4be0512 100644 --- a/docs/mobile-readiness.md +++ b/docs/mobile-readiness.md @@ -265,9 +265,15 @@ here so a future go/no-go review has a concrete checklist, not a vague "more pol check, a signed release (not debug) build, a dormant-vs-live Play Billing decision for `rustysnes-monetization` if monetization is ever actually activated - **iOS:** - - A real on-device or simulator *run* — every verification so far is compile/link-only (this - development environment has no macOS/Xcode toolchain at all); `ios.yml`'s passing - `xcodebuild` build proves the code compiles, not that it behaves correctly at runtime + - A real on-device *run* with a ROM loaded. `v1.30.0` narrowed this: `ios.yml` now boots a + simulator, installs the app, launches it, and **requires it to still be running eight seconds + later** — a binary that links and then traps on launch is indistinguishable from a working one + in a build log, and that gap is now closed. What is still *not* proven is emulation: the app + bundles no cartridge (a licensing question, not a CI one), so nothing has run a ROM. State the + difference precisely rather than upgrading "it launches" to "it works". + + This development environment still has no macOS/Xcode toolchain, so the runner remains the only + place any of it executes. - TestFlight distribution signing secrets provisioned (the `ios.yml` upload step is currently an explicit no-op pending these) - ~~The App Store §4.7 self-audit~~ — **done, `v1.30.0`; see §4.7 audit below.** Two of the three From a60acb969a6692765b28e236f798650e29a18ece Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Fri, 31 Jul 2026 19:35:06 -0400 Subject: [PATCH 2/2] ci(ios): stop SIGPIPE turning a live app into a launch failure The liveness check piped `launchctl list` straight into `grep -q`. That exits at its first match and closes the pipe, `launchctl` then takes SIGPIPE, and `set -o pipefail` makes that the pipeline's status -- so the step reported "it started and died" for an app that was running. The first run failed exactly that way, with the process visibly alive one line above the error. Capture the listing before searching it, and print it on a genuine failure so the next one is diagnosable from the log. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ios.yml | 13 ++++++++++++- CHANGELOG.md | 7 +++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 84b07861..56aea921 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -159,10 +159,21 @@ jobs: # Ask the SIMULATOR whether the process is still there. A crash on launch is exactly the # case that links cleanly and then dies, and `simctl launch` has already returned 0 by # then, so its exit code cannot catch it. - if xcrun simctl spawn "$UDID" launchctl list | grep -q "$bundle_id"; then + # + # The listing is captured to a variable FIRST rather than piped straight into `grep -q`. + # `grep -q` exits at its first match and closes the pipe, `launchctl` then takes SIGPIPE, + # and under `set -o pipefail` that becomes the pipeline's status -- so the check reported + # "it started and died" for an app that was running fine. It did exactly that on its + # first run, with the process visibly alive in the log above the failure. + listing=$(xcrun simctl spawn "$UDID" launchctl list) || { + echo "::error::could not read the simulator's process list" + exit 1 + } + if printf '%s\n' "$listing" | grep -q "$bundle_id"; then echo "ok: still running 8s after launch" else echo "::error::the app is not running 8s after launch -- it started and died" + printf '%s\n' "$listing" | head -40 exit 1 fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 5238cf2a..a0911858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 emulation: the app bundles no cartridge, so no ROM has run. `docs/mobile-readiness.md` now states that distinction instead of listing the whole runtime question as open. + The liveness check needed a fix of its own before it was trustworthy. Piping `launchctl list` + straight into `grep -q` made it report "it started and died" for an app that was running: `grep -q` + exits at its first match and closes the pipe, `launchctl` takes `SIGPIPE`, and `set -o pipefail` + turns that into the pipeline's status. The first run failed that way with the process visibly + alive one line above the error. The listing is now captured before it is searched, and a genuine + failure prints it. + - **AccuracySNES: the Mesen2 oracle diagnosed, and a stale frame budget aligned.** Three `v1.28.0` items ended at "no oracle can arbitrate", so the headless runner was investigated rather than accepted as environmental. Established: the hang is genuinely pre-existing (the `v1.25.0`-era image