Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,53 @@ name: iOS
# is tag- (and manual-) gated because macOS runner minutes bill ~10x Linux — the
# same gating posture as android.yml's heavier jobs and release.yml. Accuracy is
# never gated on a device toolchain.
#
# Beyond the TestFlight upload, every run also COMPILES THE SWIFT APP for the iOS
# Simulator (the "Build the app" step). Nothing else in CI builds the Swift
# target, so until v2.9.7 the app could stop compiling with every check green --
# and it had. The first Xcode build on a Mac after v2.9.7 failed with 24 errors.
# That step runs only when this workflow does, on purpose: macOS minutes are the
# cost this file's gating exists to contain. This workflow runs for a release
# (the `workflow_call` from release-auto.yml), by hand, and on the
# every-other-month TestFlight refresh cron, which is skipped unless the
# `IOS_SIGNING_READY` repo variable is "true" (unset as of 2026-10-02, so the
# cron is dormant). It never runs on a pull request. For an answer before a
# release, run it by hand (`workflow_dispatch`) on the release branch.
on:
push:
tags:
- 'v*'
workflow_dispatch:
# Invoked by `release-auto.yml` for every release it cuts. This is the trigger
# that actually fires: the auto-release tag is pushed with the built-in
# GITHUB_TOKEN, which does NOT trigger `on: push: tags` (GitHub's recursion
# guard, the same reason release-auto calls release.yml directly). The tag
# trigger above has therefore not run since v2.3.8 (2026-08-20). Every
# release from v2.3.9 to v2.9.7 was auto-tagged, and none of them built the
# iOS host at all.
workflow_call:
inputs:
tag:
description: "Release tag to build (e.g. v2.9.8)."
required: true
type: string
# Exactly the secrets the TestFlight steps read, passed by name from
# release-auto.yml rather than with `secrets: inherit`, which would hand this
# workflow every repository secret. All optional: absent, the "Detect iOS
# signing secrets" step skips the upload, as it does on a tag push.
secrets:
ASC_KEY_ID:
required: false
ASC_ISSUER_ID:
required: false
ASC_KEY_CONTENT:
required: false
MATCH_GIT_URL:
required: false
MATCH_PASSWORD:
required: false
MATCH_GIT_BASIC_AUTHORIZATION:
required: false
# v1.9.1 "Patch" — TestFlight build-refresh cadence. A TestFlight build expires
# 90 days after upload; re-build + re-upload at 06:00 UTC on the 1st of every
# other month (~60-day cadence) so external testers never lose access between
Expand Down Expand Up @@ -43,8 +85,11 @@ jobs:
# so an un-provisioned repo must not paint every release tag push red.
if: ${{ github.event_name != 'schedule' || vars.IOS_SIGNING_READY == 'true' }}
steps:
# The release tag when called by release-auto (it exists by then, as
# release-auto creates it first); otherwise the triggering ref.
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag || github.ref }}
persist-credentials: false

# v2.0.7 "Trim" — App Store submission floor. From 2026-04-28 every App Store
Expand Down Expand Up @@ -111,6 +156,41 @@ jobs:
- name: Build xcframework + generate project
run: ./scripts/build-ios-xcframework.sh

# The script must leave the tracked tree untouched. Through v2.9.7 it
# rewrote the hand-written ios/RustyNES/Info.plist on every run (an
# `info:` block in project.yml made XcodeGen treat the plist as output),
# and on a throwaway CI checkout nobody saw it.
- name: Build left the tracked tree unchanged
run: git diff --exit-code

# Compile and link the Swift app against the xcframework and the freshly
# generated UniFFI bindings. Unsigned, generic simulator: no device and
# no certificate needed, so it runs whether or not the signing secrets
# exist. This catches a Swift / UniFFI mismatch, for example a Rust type
# renamed or added in rustynes-mobile, at release time instead of on the
# first developer's Mac. It proves the app builds, not that it runs; that
# is the device run sheet's job.
- name: Build the app (iOS Simulator, unsigned)
working-directory: ios
run: |
set -o pipefail
xcodebuild build \
-project RustyNES.xcodeproj \
-scheme RustyNES \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath build/DerivedData \
CODE_SIGNING_ALLOWED=NO \
| tee xcodebuild.log

- name: Upload the xcodebuild log
if: ${{ failure() }}
uses: actions/upload-artifact@v7
with:
name: ios-app-xcodebuild-log
path: ios/xcodebuild.log
if-no-files-found: ignore

# Detect whether the maintainer-provisioned signing secrets exist. They are a
# documented manual carryover (docs/ios.md "Maintainer-manual carryovers"):
# the App Store Connect API key (ASC_*) + the fastlane match repo (MATCH_*).
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/release-auto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,31 @@ jobs:
uses: ./.github/workflows/release.yml
with:
tag: ${{ needs.prepare.outputs.tag }}

# The iOS host for the same release: the xcframework, the Swift app compiled
# for the simulator, and, once signing is provisioned, the TestFlight upload.
# Called directly for the same reason as `build` above: the tag this run
# pushed with GITHUB_TOKEN never fires ios.yml's own `push: tags` trigger,
# which is why no release from v2.3.9 to v2.9.7 built the iOS app at all.
# It runs alongside `build`, not after it, and a failure here marks the
# release run red without touching the binaries `build` attaches.
ios:
name: iOS host (xcframework + Swift build)
needs: prepare
if: needs.prepare.outputs.should_release == 'true'
permissions:
contents: read
uses: ./.github/workflows/ios.yml
with:
tag: ${{ needs.prepare.outputs.tag }}
# Only the App Store Connect + fastlane match secrets ios.yml reads for the
# TestFlight upload, by name. `secrets: inherit` would pass every repository
# secret to the called workflow (least privilege, CodeRabbit on #578). Its
# "Detect iOS signing secrets" step skips the upload when they are absent.
secrets:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,39 @@ cycle-accurate core later replaced.

## [Unreleased]

### Fixed

- **The iOS app compiles again.** The first Xcode build on a Mac (Xcode 27)
failed with 24 errors in v2.9.7's Swift. The app's own `NesButton` collided
with the type UniFFI generates from `rustynes-mobile`, now renamed
`NesButtonBit`. The FDS BIOS prompt also caught `MobileError.missingFdsBios`,
but the generated case is `MissingFdsBios`.
- **iOS games no longer open to a black screen.** The frame loop started only
after the renderer was built, and the only retry of a build deferred at
first layout ran off that same loop, so no frame ever ran until the app was
backgrounded and foregrounded. The loop now starts first.
- **No iOS launch crash without the iCloud capability.** Save-state sync is
opt-in, but the app checked the iCloud account at launch anyway, and
`CKContainer` traps uncatchably in a build without the container
entitlement. CloudKit is now untouched while sync is off, and a container is
created only when the entitlement is present.
- **`scripts/build-ios-xcframework.sh` no longer overwrites
`ios/RustyNES/Info.plist`** (an `info:` block in `ios/project.yml` made
XcodeGen regenerate it, dropping the ROM document types, fonts and
orientations). It also builds the Rust and C code for the app's iOS 17.0
floor instead of the installed SDK's version.
- **A macOS clone no longer starts dirty.** `tests/roms/accuracycoin/README.md`
and `tests/roms/AccuracyCoin/README.md` were one file on a case-insensitive
filesystem; the lowercase one is now `RUNTIME.md`.

### Changed

- **CI builds the iOS app at release time.** `ios.yml` now compiles the Swift
app for the iOS Simulator, and fails if the build script modified a tracked
file. `release-auto.yml` calls `ios.yml` for every release: its tag trigger
never fires for auto-pushed tags, so no release from v2.3.9 to v2.9.7 had
built the iOS host. macOS jobs still never run on pull requests.

## [2.9.7] - 2026-09-30 - "Tandem" (the desktop's features on the web and on phones, full release binaries, and an A12 fix found by real games)

The eighth release of the v2.9.x line and the fourth of the line to v3.0.0:
Expand Down
2 changes: 1 addition & 1 deletion crates/rustynes-test-harness/tests/accuracycoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const MIN_PASS_RATE: f64 = 0.60;
/// The exact number of `AccuracyCoin` tests the shipped headless build
/// passes, re-blessed at the 2026-09 upstream re-sync (upstream `69c8860`;
/// the ROM has since moved to `46199ae4`, which changes no verdict — see
/// `tests/roms/accuracycoin/README.md`).
/// `tests/roms/accuracycoin/RUNTIME.md`).
///
/// 143 of 144 assigned. The catalog grew 141 -> 144 assigned tests and
/// **nothing that passed before stopped passing**: upstream removed no
Expand Down
18 changes: 14 additions & 4 deletions docs/ios.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,20 @@ device `.a` (`cargo run -p rustynes-mobile --bin uniffi-bindgen -- generate
--library … --language swift`; rename the modulemap to `module.modulemap`) ->
assemble the headers dir (`rustynes_mobileFFI.h` + `rustynes_ios.h` +
`module.modulemap`) -> `xcodebuild -create-xcframework` -> `xcodegen generate`.
CI (`.github/workflows/ios.yml`) runs this on `macos-latest`, **gated to tag
pushes (`v*`) + manual dispatch** because macOS minutes bill ~10x — the host
`ci.yml` remains the accuracy / determinism authority and is never gated on a
device toolchain. `fastlane` (`match` read-only signing + `gym` + `pilot`) uploads
CI (`.github/workflows/ios.yml`) runs this on `macos-latest`, **never on a pull
request**, because macOS minutes bill ~10x — the host `ci.yml` remains the
accuracy / determinism authority and is never gated on a device toolchain. It
runs for a release, by hand, and on the TestFlight refresh cron described below
(dormant until the `IOS_SIGNING_READY` repo variable is set). "For a release"
means `release-auto.yml`, which calls `ios.yml` (`workflow_call`) for every
release it cuts. Its `push: tags` trigger never fires for those, because the
auto-release tag is pushed with `GITHUB_TOKEN`. So no release from v2.3.9 to
v2.9.7 built the iOS host. The call is unreleased as of this writing: it was
added after v2.9.7 and first runs on the next release. Manual dispatch remains
for an on-demand run, for example on a release branch before it merges. Each run then **compiles the Swift app** for the generic iOS Simulator,
unsigned. It is the only place CI builds the Swift target, and v2.9.7 shipped
with 24 Swift compile errors because nothing did. The run also fails if the
script modified any tracked file. `fastlane` (`match` read-only signing + `gym` + `pilot`) uploads
to TestFlight via an App Store Connect API key. The upload is **gated on the
signing secrets being present** (a "Detect iOS signing secrets" step): until they
are provisioned, the xcframework build still runs (proving the iOS host compiles)
Expand Down
2 changes: 1 addition & 1 deletion ios/RustyNES/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ final class AppModel: ObservableObject {
netplay.attach(core: core)
// Reconcile this game's cloud save-states (pull any newer-remote slots).
cloudSaveStates.setCurrentGame(sha: entry.sha)
} catch MobileError.missingFdsBios {
} catch MobileError.MissingFdsBios {
// v2.9.7: ask for disksys.rom once, then open this disk again.
pendingFdsEntry = entry
needsFdsBios = true
Expand Down
Loading
Loading