diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 50415b1d..56aea921 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -96,6 +96,93 @@ 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. + # + # 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 + + - 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..a0911858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,23 @@ 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. + + 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 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