fix(ios): the iOS app builds, launches and plays -- plus release-time iOS CI - #578
Conversation
…llide The repository tracks two directories whose names differ only in case: tests/roms/accuracycoin/ (the runtime AccuracyCoin.nes, its MIT LICENSE and a README) and tests/roms/AccuracyCoin/ (SOURCE_CATALOG.tsv, the sub-test ROMs, the mirror build and a README). On a case-insensitive filesystem (macOS APFS by default, Windows NTFS) the two are one directory, so the two README.md paths fold onto the same file. A fresh `gh repo clone` warns "the following paths have collided", checks out only one of the pair, and the working tree starts dirty: the uppercase README.md shows as modified because it holds the lowercase file's bytes. The two README.md files were the only colliding pair: the LICENSE and .nes files exist on only one side, so they coexist in the folded directory. `git ls-files | tr A-Z a-z | sort | uniq -d` is now empty. The fix renames the lowercase README to RUNTIME.md, which is the smallest change that removes the collision. Merging the two directories was rejected because about 20 sites hard-code tests/roms/accuracycoin/AccuracyCoin.nes (the harness, the netplay determinism tests, pgo_trainer and the trace tools). The new file states why it is not called README.md. The one code comment that named the old path (tests/accuracycoin.rs:64) now points at RUNTIME.md. The cost is that GitHub no longer renders a README when browsing the lowercase directory. On macOS, `git add` with core.ignorecase=true recorded the new file under the uppercase directory. The index entry was written explicitly with `git update-index --cacheinfo` so the lowercase path that Linux CI expects is the one tracked; new files under either directory need the same check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…face The first compile of the iOS app on a Mac (Xcode 27.0, iOS 27.0 simulator SDK) failed with 24 errors from two causes. CI never saw either: the iOS job builds the Rust xcframework and runs `xcodegen generate`, but never compiles the Swift target, so both shipped in v2.9.7 green. 1. Two types named `NesButton`. `rustynes-mobile` exports `pub enum NesButton` (`#[derive(uniffi::Enum)]`, the press/release convenience for `NesController::set_button`), which UniFFI emits into ios/Generated/RustyNESCore.swift as `public enum NesButton`. The app declares its own `enum NesButton: UInt8` in NesButtons.swift, the single-bit mask values the touch overlay and gamepad mapper OR into the `set_buttons(port, mask)` wire byte. Both live in one module, so every use was "ambiguous for type lookup" and the declaration an "invalid redeclaration". The app's enum is renamed `NesButtonBit` (it is a bit value, which the old name never said). The bridge's type, and so the Kotlin binding and the Android app, are untouched. The rename is word-bounded, so `NesButtonMask` and the file name `NesButtons.swift` keep their names. The app does not call the bridge's `setButton`; it builds the mask itself. 2. `catch MobileError.missingFdsBios` (AppModel.swift, v2.9.7 FDS BIOS prompt). UniFFI's Swift generator keeps a Rust error enum's variant names verbatim, so the case is `MobileError.MissingFdsBios`, like the existing `RomLoad` / `SaveState` / `InvalidPort` cases. As written, the pattern did not compile; corrected to the generated name. Verified: `xcodebuild -scheme RustyNES -destination 'platform=iOS Simulator,name=iPhone 18 Pro'` reports BUILD SUCCEEDED, and the app launches and runs on the simulator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ios/project.yml gave the RustyNES target an `info: path: RustyNES/Info.plist` block with no `info.properties`. XcodeGen treats `info.path` as an output: it GENERATES that file from `properties`, so every run of scripts/build-ios-xcframework.sh (which ends in `xcodegen generate`) replaced the tracked 65-key plist with a bare template. The overwrite dropped CFBundleDocumentTypes (the .nes/.fds/.nsf/.rns/.rnm document types), UTImportedTypeDeclarations, UIAppFonts, CFBundleDisplayName, CFBundleLocalizations, UILaunchScreen, the orientation lists and CADisableMinimumFrameDurationOnPhone (the 120 Hz ProMotion unlock). It showed up as -232 lines of `git diff` after a local build. In CI the job runs on a throwaway checkout, so any Xcode build or fastlane archive there ran on the stripped plist and nobody saw the diff. The block is removed. `INFOPLIST_FILE: RustyNES/Info.plist` in the target's settings already points Xcode at the tracked file, which is all the project needs. A comment at the old site records why the block must not come back. Verified: after `xcodegen generate --spec ios/project.yml`, `git status` shows Info.plist unmodified, and the app builds and launches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/build-ios-xcframework.sh never set IPHONEOS_DEPLOYMENT_TARGET. rustc and the `cc` crate both fall back to the installed SDK's version when it is unset, so with Xcode 27 every C object in librustynes_ios.a was compiled for iOS 27.0: Lua 5.5 (lua-src, via mlua), rcheevos and ring. The app's deployment target is 17.0 (ios/project.yml), and the link printed one "object file ... was built for newer 'iOS-simulator' version (27.0) than being linked (17.0)" warning per object: 76 in one build. The warning is a real hazard, not noise. Such an object may reference symbols that only exist on newer iOS, and on an iOS 17-26 device that is a launch-time dyld failure. The script now exports IPHONEOS_DEPLOYMENT_TARGET=17.0, kept in step with project.yml by a comment, and a caller can still override it from the environment. A clean build picks it up. An incremental one does not fully: ring and rcheevos rebuild, but lua-src's build script does not declare the variable as a rerun trigger, so a warm target/ keeps its 27.0 Lua objects until `cargo clean -p lua-src` for each iOS target. Verified after that clean: 0 "built for newer" warnings, BUILD SUCCEEDED. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An iOS build without the iCloud container entitlement aborted at launch:
*** Terminating app due to uncaught exception 'CKException',
reason: 'containerIdentifier can not be nil'
... CloudSaveStateSync.refreshAccount() <- CloudSaveStateSync.start()
AppModel calls `cloudSaveStates.start()` at launch, and start() ran
`CKContainer.default().accountStatus()` unconditionally, even though
save-state sync is opt-in and off by default. `CKContainer.default()`
derives its id from the signed `icloud-container-identifiers`
entitlement and raises an Objective-C exception when there is none,
which Swift's `try?` cannot catch. Any unsigned simulator build hit it,
and so would a sideload whose profile lacks the iCloud capability. The
file header and RustyNES.entitlements both promised it "gracefully
no-op[s]" until provisioned; it did not.
Two changes:
* start() and refreshAccount() return early when sync is disabled, so
a default install never touches CloudKit.
* The container is created only when the binary carries the
entitlement: `container` and `database` are now optional, and every
call site treats nil like a signed-out account, so the slot reports
unavailable and the local save is never blocked. Creating the
container explicitly does not avoid the trap:
`CKContainer(identifier:)` was measured to stop in a `brk` inside its
initialiser on the iOS 27 simulator. So the entitlement is probed
first, with public API only (`CloudKitEntitlement.isPresent`):
- simulator: the main executable's __TEXT,__entitlements section,
which Xcode links in from the Simulated.xcent and which an
unsigned build lacks (getsectiondata on _dyld_get_image_header(0));
- device: the Entitlements dictionary of embedded.mobileprovision,
read from the CMS envelope by its plist delimiters. App Store and
TestFlight builds carry no embedded profile and are always signed
with the capability, so a missing profile counts as entitled.
Measured on the iPhone 18 Pro simulator (iOS 27.0). Each of four builds
was installed fresh and launched with `cloudSaveStates` forced via
`defaults write`:
unsigned, sync off -> runs, no crash report (was: CKException)
unsigned, sync on -> runs, no crash report (was: brk in init)
ad-hoc, sync off -> runs, no crash report
ad-hoc, sync on -> runs, no crash report
A temporary NSLog probe, removed before this commit, showed the
unsigned build reading isPresent=false and the ad-hoc build
isPresent=true, the latter reaching `accountStatus()` without trapping
(accountAvailable=false: the simulator has no iCloud account).
The device branch, embedded.mobileprovision, is unexercised: no
device was available, and it needs a check on hardware.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ck screen) Every game opened to a black picture under a working controller overlay: the ROM loaded, but no frame ever ran or presented. MetalGameView's Coordinator.attachAndStart() is called from makeUIView, before SwiftUI lays the MTKView out, so `view.drawableSize` is 0 x 0 there and the renderer build is deferred. The deferred build had two retry paths, and neither could run: * `step(_:)`, the CADisplayLink callback, rebuilds when it sees a real drawable size. But the display link was created only at the END of a successful attachAndStart(), so with the build deferred there was no link and `step` never ran. * `mtkView(_:drawableSizeWillChange:)` was an intentional no-op. Calling attachAndStart() from it would not help either, because `view.drawableSize` still reads 0 x 0 inside the callback. The only remaining retry was appWillEnterForeground, so a background and foreground round trip was the one way a game ever started. Measured on the iPhone 18 Pro simulator (iOS 27.0) with temporary NSLog probes (not committed). Before the fix: attachAndStart drawableSize=(0.0, 0.0) drawableSizeWillChange (1206.0, 2622.0) (no step tick, no rustynes_ios_gfx_init call, ever) After it: attachAndStart drawableSize=(0.0, 0.0) <- makeUIView, deferred step #1 attachAndStart drawableSize=(1206.0, 2622.0) gfx_init 1206x2622 -> ok step #61, step #121, ... <- 60 Hz The fix starts the display link first in attachAndStart(), before the size guard. `EmulatorCore.tick()` returns early until the renderer exists, so ticking before the build is free, and `step`'s existing resize branch completes the build on the first tick with a real size. Because the link can now exist before the renderer does, the foreground handler's not-yet-attached branch also unpauses it. Otherwise a background trip before the first layout would leave it paused, since appDidEnterBackground pauses it. This was a host-side bug. The wgpu/Metal renderer in rustynes-ios builds and presents on the simulator once it is called. Verified by opening nestest.nes, dpcmletterbox.nes and AccuracyCoin.nes through `simctl openurl`: each renders its title screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… all Two gaps, found while building the iOS app on a Mac for the first time: 1. Nothing in CI compiled the Swift app. ios.yml builds the Rust xcframework and runs `xcodegen generate`, but never runs xcodebuild on the Swift target. So v2.9.7 shipped an app that did not compile: 24 errors, from a `NesButton` collision with the UniFFI-generated type and a mis-cased `MobileError.MissingFdsBios` (fixed in 196b92c). 2. ios.yml had not run for a release since v2.3.8. `gh run list --workflow ios.yml` shows its last tag run on 2026-08-20 (v2.3.8), and nothing since but a skipped cron. Releases are now cut by release-auto.yml, which pushes the tag with GITHUB_TOKEN, and GitHub's recursion guard stops such a tag firing `on: push: tags`. release-auto already works around this for release.yml by calling it (`workflow_call`), but never did so for ios.yml. So v2.3.9 through v2.9.7, about forty releases, built no iOS host at all. Changes: * ios.yml gains a `workflow_call` trigger with a `tag` input (the same shape as release.yml) and checks out `inputs.tag || github.ref`. release-auto.yml gains an `ios` job that calls it for every release it cuts, with `secrets: inherit` so the existing TestFlight upload works once signing is provisioned. The "Detect iOS signing secrets" step still skips the upload when the secrets are absent. The job runs alongside `build`, so a failure marks the release run red without touching the attached binaries. * After the xcframework step, ios.yml now: - fails if the script modified any tracked file (`git diff --exit-code`). It rewrote ios/RustyNES/Info.plist on every run until c6ca71d, and on a throwaway checkout nothing noticed; - runs `xcodebuild build` of the RustyNES scheme for `generic/platform=iOS Simulator` with CODE_SIGNING_ALLOWED=NO. No device or certificate is involved, so it runs whether or not signing is provisioned; - uploads the xcodebuild log as an artifact on failure. * docs/ios.md: the CI paragraph said "gated to tag pushes", which has been untrue in practice since v2.3.9. It now describes the release-auto call and the Swift build. Cost, by the maintainer's direction (2026-10-02): macOS jobs run at release time only, never on PRs, pushes to main or the weekly cron. ci.yml is untouched. The trade-off is that the check cannot block a release: release-auto creates the tag and GitHub Release before calling ios.yml, so a broken app still ships, and the run turns red afterwards. For a pre-merge answer, dispatch "iOS" by hand on the release branch. Verified locally (Xcode 27.0, macOS arm64): actionlint passes on both workflow files. The new steps run as written: the script succeeds, `git diff --exit-code` over ios/ and scripts/ is clean, and the generic simulator build reports BUILD SUCCEEDED with an x86_64 + arm64 app. The workflow itself has not run on GitHub yet; its first real run is the next release, or a manual dispatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er Unreleased Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: doublegate/RustyNES/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (16)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe changes add an iOS Simulator build to release automation, update iOS build configuration and runtime behavior, and change the AccuracyCoin test harness documentation reference. ChangesiOS Build and Runtime
AccuracyCoin ROM Documentation
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Change: Bug fix Sequence Diagram(s)sequenceDiagram
participant ReleaseWorkflow as release-auto.yml
participant IOSWorkflow as ios.yml
participant Checkout as Git checkout
participant Xcode as xcodebuild
ReleaseWorkflow->>IOSWorkflow: Call with release tag
IOSWorkflow->>Checkout: Check out supplied tag
IOSWorkflow->>Xcode: Build unsigned generic iOS Simulator app
Xcode-->>IOSWorkflow: Return build status and log
Merge Risk: ⚪ Minimal · up to The iOS build, launch and CI fixes show no concrete merge-blocking issue. The real-device iCloud path and the new release workflow have not yet been run, so check them after merge. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The changes add useful release-time validation and avoid cloud access when sync is disabled or unavailable. No introduced security vulnerability was established. Remaining uncertainty concerns signed-device entitlement handling and cloud operations during account changes. The new iOS validation runs after release publication rather than blocking it. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 8 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (8 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 63.16% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 10 files. (6 skipped: 6 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Docs7 for doublegate/rustynes
Commit |
Antigravity review (Gemini via Ultra)This PR configures CI to compile the iOS host during releases and fixes several iOS runtime issues, including a CloudKit entitlement crash and an unstarted display link. Blocking issuesNone found. Suggestions
Nitpicks
Automated first-pass review by Earlier review rounds (newest first)Round reviewed at 2026-10-02 05:10 UTCAntigravity review (Gemini via Ultra)This PR restores iOS compilation by resolving UniFFI type collisions, introduces a release-time CI check for the Swift app, and prevents launch crashes on unentitled builds by making CloudKit sync safely opt-in. Blocking issues
Suggestions
Nitpicks
Automated first-pass review by Round reviewed at 2026-10-02 04:56 UTCAntigravity review (Gemini via Ultra)This PR fixes iOS compilation and launch issues, prevents a CloudKit initialization crash on unentitled builds, and adds an unsigned iOS simulator build to the release CI pipeline to catch future regressions. Blocking issues
Suggestions
Nitpicks
Automated first-pass review by |
|
@coderabbitai review |
|
PR #578 review (Antigravity, blocking): two CloudKit paths in CloudSaveStateSync dropped their errors with no record, against the project rule on swallowed errors. The same file already logs every upload failure with NSLog. * delete(sha:slot:) ran `_ = try? await database.modifyRecords(...)`. It now logs a thrown error and a per-record failure, because modifyRecords reports a per-record failure inside its result, not by throwing. The exception is `.unknownItem`, which only means the slot was never uploaded and is the expected case for a local-only slot. The call stays best-effort: the local delete never waits on it. * reconcile(sha:)'s fetch `catch { return }` now logs before returning. The behaviour is unchanged (an offline or transient failure keeps the locally derived slot states), but the failure is recorded. Both lines predate this PR, which only renamed `Self.database` to the unwrapped `database` on them. Fixed here because the review is right and the fix is local. Verified: `xcodebuild build` for generic/platform=iOS Simulator, unsigned, reports BUILD SUCCEEDED with no warnings in this file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The retained scheduled workflow contradicts the stated release-only, no-cron policy and accompanying documentation.
Review effort: Balanced
Findings: 1
Open (3)
What changed in this PR
Fixes iOS compilation, launch, rendering, build tooling, and release-time validation.
Changes:
- Resolves Swift/UniFFI naming and FDS error mismatches.
- Fixes CloudKit launch crashes and deferred Metal initialization.
- Adds iOS app compilation to release automation.
| File | Description |
|---|---|
tests/roms/accuracycoin/RUNTIME.md |
Avoids case-insensitive path collision. |
scripts/build-ios-xcframework.sh |
Sets the iOS 17 deployment target. |
ios/RustyNES/TouchControlsOverlay.swift |
Updates renamed button type documentation. |
ios/RustyNES/NesButtons.swift |
Renames the local button enum. |
ios/RustyNES/MultiTouchControlPad.swift |
Uses the renamed enum. |
ios/RustyNES/MetalGameView.swift |
Starts rendering retries before attachment. |
ios/RustyNES/GameControllerManager.swift |
Uses the renamed enum. |
ios/RustyNES/ControlPadLayout.swift |
Updates button layout types. |
ios/RustyNES/CloudSaveStateSync.swift |
Safely gates CloudKit initialization. |
ios/RustyNES/AppModel.swift |
Corrects the generated FDS error case. |
ios/project.yml |
Preserves the tracked property list. |
docs/ios.md |
Documents iOS CI behavior. |
crates/rustynes-test-harness/tests/accuracycoin.rs |
Updates the runtime documentation path. |
CHANGELOG.md |
Records the fixes and CI addition. |
.github/workflows/release-auto.yml |
Invokes the reusable iOS workflow. |
.github/workflows/ios.yml |
Builds the Swift app and checks cleanliness. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…d v2.9.8 (review) PR #578 review (Copilot, 3 threads), all correct: * "Release time only" was false. ios.yml keeps its pre-existing schedule trigger, the every-other-month TestFlight refresh (builds expire after 90 days), and that cron runs the same macOS job, including the new Swift build, whenever the IOS_SIGNING_READY repo variable is "true". It is dormant today: `gh variable list` shows no variables, and its last cron run (2026-09-01) was skipped. The cost constraint behind this PR keeps macOS jobs off pull requests, off pushes to main and off ci.yml's weekly cron. The refresh cron is TestFlight's renewal mechanism and stays. The workflow header comment and docs/ios.md now say exactly that: never on a PR; for a release, by hand, and on the dormant refresh cron. * "until the call was added at v2.9.8" and "Until v2.9.8 it rewrote ..." presented an unreleased version as released (the current release is v2.9.7, and the change sits under CHANGELOG [Unreleased]). The docs now say the call is unreleased and first runs on the next release, and the workflow comment says "Through v2.9.7". The `e.g. v2.9.8` in the workflow_call input description is an example value and stays. actionlint passes on ios.yml. markdownlint-cli 0.49.1 passes on docs/ios.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re: Antigravity review, round 1 (comment of 04:44:34Z, on 5d88fcb) Blocking: silent failure paths in
Suggestion: rename one of the Nitpick: |
|
Re: Docs7 (comment of 04:44:10Z): content review passed with no findings, so there is nothing to act on. Thanks. |
|
@coderabbitai review |
❌ Action failedReview failed.
|
|
@coderabbitai full review |
|
Re: Antigravity review, round 2 (comment updated 04:56:25Z, on 9c3fbd8) Blocking: " Suggestion: rename one of the Suggestion: Nitpick: add |
✅ Action performedFull review finished. |
…tions (review) PR #578 review (CodeRabbit, full review of 9c3fbd8: "No actionable comments", Merge Risk minimal). Two items in its summary were acted on. * Least privilege for the called workflow. Its security review noted that the release path can reach the App Store Connect and fastlane match credentials, and that their scope could not be bounded. The part this PR controls was `secrets: inherit` on release-auto's `ios` job, which handed ios.yml every repository secret. ios.yml reads exactly six: ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_CONTENT, MATCH_GIT_URL, MATCH_PASSWORD, MATCH_GIT_BASIC_AUTHORIZATION (`grep -o 'secrets\.[A-Z_]*' ios.yml | sort -u`). ios.yml now declares those six under `workflow_call.secrets`, all `required: false`, because their absence is the normal unprovisioned state: the "Detect iOS signing secrets" step then skips the upload. release-auto now passes them by name. Tag pushes, dispatch and the cron read the repo's secrets directly, so they are unaffected. * Docstring coverage (pre-merge check: 63% of the functions this diff touches, threshold 80%). Doc comments were added to the four touched functions that carried logic without one: CloudSaveStateSync.refreshAccount() (when it touches CloudKit, and what "unavailable" means to callers); CloudKitEntitlement's simulatorEntitlements(), which also records why image 0 and not `dladdr` (in Debug the app's code is in RustyNES.debug.dylib, and only the stub executable carries __TEXT,__entitlements); plist(_:); and MetalGameView.Coordinator.step(_:) (deferred build, 60 Hz lock, and the pacing fallback). The remaining undocumented touched function is a one-line local helper (`has(_:)`) inside a drawing closure. Verified: actionlint passes on both workflows, and the generic iOS-Simulator unsigned build reports BUILD SUCCEEDED. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re: CodeRabbit full review (run Pre-merge check, Docstring Coverage (63% vs 80%). Addressed in c6d2bce. Doc comments were added to the four touched functions that carried logic without one:
The remaining undocumented touched function is a one-line local helper ( Security blast radius (credentials whose scope cannot be bounded). The part this PR controls is fixed in c6d2bce. Hardening proposal (generation fencing for sync and delete across account switches). Deferred. As the review states, this is pre-existing behaviour and not a PR-introduced vulnerability: the upload and delete tasks and their SHA checks predate this PR, which adds only availability guards and failure logging. It belongs with the CloudKit work that the device run sheet exercises, where an account switch can actually be tested, not in a build-fix PR. Observed points (release gating, sync off by default, unsigned build before signing steps). These are accurate and need no action. The "runs after release publication" limit is stated in the PR description. |
|
Re: Antigravity review, round 3 (comment updated 05:10:58Z, on c6d2bce): no blocking issues. Suggestion: unify Nitpick: Nitpick: pipe Per this repo's review stopping rule ( |


Description
The first build of the iOS app on a Mac (Xcode 27.0, iOS 27.0 simulator). In v2.9.7 the app did not compile. Once it did, it crashed at launch, and once that was fixed every game opened to a black screen. This PR fixes all three, plus two build-script defects that turned up along the way. It also gives CI its first build of the Swift app, at release time only, by the maintainer's direction.
The maintainer has played it on the simulator: the picture renders, the controls work, and AccuracyCoin runs to its end.
Type of Change
Component(s) Affected
ios/), not a template checkboxChanges Made
1b6d2b02tests/roms/accuracycoin/README.md→RUNTIME.md. It andtests/roms/AccuracyCoin/README.mdare one path on a case-insensitive filesystem, so a macOS clone warned "paths have collided" and started with a dirty tree.196b92c7enum NesButtoncollided with theNesButtonUniFFI generates fromrustynes-mobile(24 errors); renamedNesButtonBit, with the bridge, Kotlin and Android untouched. The FDS BIOS prompt caughtMobileError.missingFdsBios; UniFFI keeps Rust's casing, so the case isMissingFdsBios.c6ca71dfios/project.yml'sinfo: path:block made XcodeGen regenerate the hand-writtenInfo.pliston everybuild-ios-xcframework.shrun, dropping the document types, UTIs, fonts, orientations and the 120 Hz key. Removed;INFOPLIST_FILEalready points at the tracked plist.a8bd5cf8IPHONEOS_DEPLOYMENT_TARGET, so with Xcode 27 every C object (Lua, rcheevos, ring) was built for iOS 27.0 against the app's 17.0 floor: 76 link warnings, and a dyld hazard on iOS 17–26 devices. Now exported as 17.0 (0 warnings).3510eb2bCloudSaveStateSync.start()checked the iCloud account at launch although sync is opt-in. Without the container entitlement,CKContainer.default()throws an uncatchableCKException, andCKContainer(identifier:)traps too (measured). CloudKit is now untouched while sync is off, and the container is created only when an entitlement probe (public API only) finds it.bc828a9bMetalGameViewstarted itsCADisplayLinkonly after a successful renderer build. AtmakeUIViewthe drawable is 0×0, so the build was deferred, and the only retry ran off that same link. Probes showedgfx_initwas never called. The link now starts first.37b1cc5bios.ymlrun at all. See below.5d88fcb4[Unreleased]entries.af092df89c3fbd85ios.ymlruns, and stop presenting the unreleased v2.9.8 as released.c6d2bce7ios.ymlreceives only the six signing secrets it reads, declared underworkflow_call.secretsand passed by name, notsecrets: inherit. Doc comments added to the four touched functions that lacked them (docstring-coverage check).The CI change, and its limits
ios.ymlhadn't run for a release since v2.3.8 (gh run list --workflow ios.yml: last tag run 2026-08-20).release-auto.ymlpushes release tags withGITHUB_TOKEN, which does not fireon: push: tags. It already callsrelease.ymldirectly for that reason, but never calledios.yml.ios.ymlnow has aworkflow_calltrigger with ataginput, and release-auto has aniosjob that calls it. It passes, by name, only the six App Store Connect / fastlane match secrets the TestFlight steps read, all optional (absent, the upload is skipped).ios.ymlnow builds the Swift app:xcodebuild buildforgeneric/platform=iOS Simulator, unsigned. Agit diff --exit-codestep fails the run if the script modified a tracked file (theInfo.plistclass of bug), and the xcodebuild log is uploaded on failure.mainorci.yml's weekly cron (ci.ymlis unchanged).ios.ymlruns for a release, by hand, and on its pre-existing TestFlight refresh cron, which is dormant until theIOS_SIGNING_READYrepo variable is set (corrected after Copilot's review).ios.yml, so a broken app still ships and the release run turns red afterwards. For a pre-merge answer, dispatch "iOS" by hand on the release branch.Testing Performed
NSLogprobes, not committed, confirmed the entitlement check (unsignedfalse, ad-hoctrue, reachingaccountStatus()without trapping) and the black-screen cause and fix (gfx_initnever called before,1206x2622 -> okand 60 Hz ticks after).nestest.nes,dpcmletterbox.nesandAccuracyCoin.nesrender, opened viasimctl openurl.ios.ymlsteps were run locally as written: the script succeeds, the clean-tree check passes, and the generic-simulator build succeeds with an x86_64 + arm64 app.actionlintpasses onios.ymlandrelease-auto.yml. markdownlint 0.49.1 passes ondocs/ios.mdandCHANGELOG.md.Not verified
embedded.mobileprovision) has not run. App Store and TestFlight builds carry no profile and are treated as entitled.ios.ymlhas not run there yet. Its first real run is the next release, or a manual dispatch, and itsmacos-latestimage's Xcode is unknown.Review
All bot findings are adjudicated, with replies on the PR; every inline thread is resolved.
9c3fbd85.9c3fbd85): no actionable comments, merge risk minimal. Its summary items: least-privilege secrets and docstrings fixed inc6d2bce7; a generation-fencing hardening proposal deferred, since CodeRabbit marks it pre-existing.af092df8; round 2's blocking claim ("won't compile") refuted, since it builds; round 3 has no blocking issues. Repeated suggestions declined with evidence.Documentation
docs/ios.md, CI paragraph)Review closeout
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Build & Reliability