Repository navigation
release: cut v2.0.6 "Harbor" ("Parity" — iOS feature parity + opt-in crash reporting) - #245
Conversation
…crash reporting) The second iOS finalization release (the v2.0.5→v2.0.8 window) on the byte-identical v2.0.0 "Timebase" core: AccuracyCoin 141/141, nestest 0-diff, the #![no_std] chip stack untouched. Host / iOS-only — no accuracy / save-state / determinism number moves. New: opt-in crash reporting (CrashReporter.swift) — the iOS analogue of the Android v1.8.8 CrashReporter, closing the v1.9.9 readiness gap. - Privacy-first, OFF by default. AppModel.crashReportingEnabled (a @published, UserDefaults-backed, non-cloud-synced flag) gates everything; with it off the app installs no handler and writes nothing, so the "Data Not Collected" label and PrivacyInfo.xcprivacy are unchanged. Exposed as a Settings > Diagnostics toggle. - Local-only. On opt-in, CrashReporter.install(enabled:) chains an NSSetUncaughtExceptionHandler that writes a timestamped report (version, device, OS, exception name/reason, callStackSymbols) to Application Support/RustyNES/crash-logs/, pruned to the newest 10. Nothing is uploaded; a CrashLogsView (list -> detail -> Copy / Clear) lets the user read and share manually. New UI strings localized EN + ES. - Correct C-callback discipline: the handler closure captures NOTHING (only static refs — previousHandler, writeLog, the live opt-in flag), so it is convertible to the @convention(c) NSUncaughtExceptionHandler; the previous handler is chained via a static var, not a captured local. It re-checks the live opt-in at crash time so opting out stops new logs immediately, and installs once (idempotent) from AppModel.init at launch + lazily on the Settings toggle. - Honest scope: NSSetUncaughtExceptionHandler catches the ObjC/NSException class only; pure-Swift signal-based traps are left to a maintainer's third-party reporter (gated on the same flag), the same posture Android takes toward Crashlytics. Documented inline. Feature-parity re-verification: the v1.9.x host features (Game Center, CloudKit save sync, MFi controllers, ReplayKit capture/PiP, accessibility) were re-checked against the unchanged v2.0.0 bridge surface (the timebase rewrite touched only the scheduler internals) and all still route through the same typed bridge + the re-ported save-state epoch. Audit recorded in docs/ios-v2.0.6-readiness.md. An independent Swift review pass caught and fixed a genuine bug before commit: the uncaught-exception closure originally captured the previous handler as a local, which cannot convert to a @convention(c) function pointer; it is now a static. The review also flagged a PRE-EXISTING discrepancy (project.yml declares deploymentTarget.iOS 15.0, but the existing SettingsView already uses NavigationStack + .topBarTrailing, iOS 16/17); the new code follows the file's established pattern and does not change the target — reconciling it is flagged as a v2.0.7 item. Version bump: workspace 2.0.5 -> 2.0.6; iOS MARKETING_VERSION -> 2.0.6. Verification (host gates all green): cargo check --workspace (version-only lock cascade); fmt; clippy --workspace --all-targets -D warnings (no issues); RUSTDOCFLAGS=-D warnings cargo doc; the thumbv7em no_std cross-compile; both wasm32 clippy gates; markdownlint (pinned v0.39.0); Localizable.xcstrings JSON validity. Core untouched -> the determinism / AccuracyCoin 141/141 contract is unaffected. The Swift compile + on-device crash-capture verification remain the documented macOS / TestFlight carryover (flagged for v2.0.9). Docs: CHANGELOG [2.0.6]; docs/STATUS.md current-release block (chain demoted); to-dos/ROADMAP.md; .github/release-notes/v2.0.6.md; docs/ios.md (the Parity section); new docs/ios-v2.0.6-readiness.md. TestFlight-only; App Store + AltStore PAL stay deferred to the v2.1.0 joint launch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request updates RustyNES to version 2.0.6 ("Parity"), introducing an opt-in, local-only crash reporting feature for iOS along with updated documentation and localization. Feedback on the implementation focuses on critical thread-safety and robustness improvements in CrashReporter.swift to comply with Swift 6 strict concurrency, such as caching device metadata and the log directory at launch to avoid unsafe operations and @MainActor isolation violations inside the uncaught exception handler. Additionally, a performance issue was identified in SettingsView.swift where synchronous disk I/O in a computed property within the SwiftUI body could block the main thread, with a recommendation to load the log contents using @State and .onAppear instead.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Pull request overview
This PR cuts the v2.0.6 “Harbor” release for the iOS finalization train by adding an opt-in, local-only crash-reporting surface (NSException-only) and updating the iOS UI + docs/release metadata to match.
Changes:
- Adds iOS crash reporting via a process-wide uncaught-
NSExceptionhandler that writes local crash logs, plus a Settings UI to view/copy/clear logs. - Adds EN/ES localization entries for the new Diagnostics/crash-log UI.
- Updates release/version artifacts and documentation for v2.0.6 (workspace + iOS MARKETING_VERSION, changelog, status/readiness docs, release notes, roadmap).
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| to-dos/ROADMAP.md | Updates “Current release” narrative to v2.0.6 and shifts v2.0.5 to “Preceding release”. |
| ios/RustyNES/SettingsView.swift | Adds Diagnostics section + crash log viewer/detail UI and clipboard copy action. |
| ios/RustyNES/Localizable.xcstrings | Adds new localized strings for crash-reporting UI. |
| ios/RustyNES/CrashReporter.swift | Introduces opt-in NSException handler and local log persistence/pruning logic. |
| ios/RustyNES/AppModel.swift | Persists crash-reporting opt-in, installs handler at launch and on enable. |
| ios/project.yml | Bumps iOS MARKETING_VERSION to 2.0.6. |
| docs/STATUS.md | Updates current release banner/details to v2.0.6. |
| docs/ios.md | Documents v2.0.6 parity work and crash-reporting surface. |
| docs/ios-v2.0.6-readiness.md | Adds v2.0.6 readiness record and verification notes. |
| CHANGELOG.md | Adds [2.0.6] entry describing iOS-only changes and verification posture. |
| Cargo.toml | Bumps workspace version to 2.0.6. |
| Cargo.lock | Updates crate versions to 2.0.6 (version-only cascade). |
| .github/release-notes/v2.0.6.md | Adds release notes for v2.0.6. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… off-main log read) Adopt all five Gemini review comments on PR #245 — genuine crash-handler robustness + a real actor-isolation fix: - @MainActor-isolation violation (the important one): `UIDevice.current` is main-actor-isolated, so reading `.model` / `.systemVersion` inside `writeLog`, which runs from the C uncaught-exception handler on an arbitrary thread, was an isolation violation (and unsafe in an unstable post-crash process). Fixed by caching device / OS / app-version metadata AND the resolved log-directory URL as plain-value statics, populated in `install(enabled:)` — now `@MainActor` — before the handler is installed. `install` is only ever called from the `@MainActor` `AppModel` (init + the toggle didSet), so the synchronous @mainactor call is valid. - Unsafe filesystem work in the handler: log pruning (directory enumeration + deletion) moved out of `writeLog` to `install()` (launch, main thread). The handler now does the bare minimum — format one string, write one file — touching only cached statics + the pre-resolved directory (no `UIDevice.current`, no `Bundle.main.infoDictionary`, no directory creation, no enumeration). - Synchronous disk I/O in a SwiftUI computed property: `CrashLogDetailView.text` was read from disk on every `body` evaluation. Now loaded once off the main thread via `.task` + `Task.detached` into `@State` (the `Sendable` `URL` is bound locally so `self` is never captured), then published back on the main actor. An independent Swift re-review confirms the rewrite is clean: the @mainactor split removes the would-be `UIDevice.current` isolation violation from the crash handler, the `@convention(c)` closure still captures nothing (only statics), `writeLog` stays nonisolated so it remains callable from the C callback, and the `.task`/`Task.detached` load is race-free. (Standing caveat, not an error today: the nonisolated static-var access would need `nonisolated(unsafe)` only if the module ever moves to the Swift 6 language mode; the codebase is Swift 5.) Readiness doc updated with the crash-handler-discipline rationale. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fallback (Copilot review) Adopt Copilot's two review comments on PR #245: - Privacy: the crash-log directory under Application Support was not excluded from iCloud / device backups, so iOS could include the local logs in a backup — undermining the "nothing leaves the device" posture. `resolveLogDir()` now sets `URLResourceValues.isExcludedFromBackup = true` on the directory after creating it; the logs are ephemeral local diagnostics, so excluding them keeps them on-device even through a full backup / restore. - i18n: the crash-log detail view's unreadable-file fallback was a hard-coded English "(unreadable)". It is now `String(localized: "(unreadable)")` with an ES translation ("(ilegible)") added to Localizable.xcstrings, consistent with the EN + ES coverage the release advertises. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
The second release of the iOS finalization window (v2.0.5 → v2.0.8), on the byte-identical v2.0.0 "Timebase" core (AccuracyCoin 141/141, nestest 0-diff,
#![no_std]chip stack untouched). Host / iOS-only.What landed
CrashReporter.swift) — iOS analogue of the Android v1.8.8CrashReporter, closing the v1.9.9 readiness gap. Off by default (Settings → Diagnostics); when enabled, an uncaught-NSExceptionhandler writes local crash logs (viewable + copyable in-app via aCrashLogsView; nothing uploaded → "Data Not Collected" label unchanged). Handler re-checks the live opt-in at crash time. EN + ES. Scope is honest: ObjC/NSExceptiononly; Swift signal-traps left to a maintainer's third-party reporter (same posture as Android's Crashlytics note).docs/ios-v2.0.6-readiness.md.2.0.5 → 2.0.6; iOSMARKETING_VERSION → 2.0.6.Quality
An independent Swift review pass caught + fixed a real bug pre-commit: the exception-handler closure originally captured the previous handler as a local, which can't convert to a
@convention(c)function pointer — now a static. The review also flagged a pre-existingdeploymentTarget.iOS 15.0vs actual (NavigationStack/.topBarTrailing, iOS 16/17) discrepancy; the new code follows the file's established pattern and doesn't change the target — reconciling it is flagged for v2.0.7.Verification (host gates — all green)
cargo check --workspace(version-only lock cascade) ·fmt·clippy --workspace --all-targets -D warnings(no issues) ·RUSTDOCFLAGS=-D warnings cargo doc·no_stdcross-compile · both wasm32 clippy gates · markdownlint ·Localizable.xcstringsJSON validity. Core untouched → determinism / AccuracyCoin 141/141 unaffected. Swift compile + on-device crash-capture remain the documented macOS/TestFlight carryover (flagged for v2.0.9).Docs
CHANGELOG [2.0.6]·docs/STATUS.md(chain demoted) ·to-dos/ROADMAP.md·.github/release-notes/v2.0.6.md·docs/ios.md(Parity section) · newdocs/ios-v2.0.6-readiness.md.TestFlight-only; App Store + AltStore PAL deferred to v2.1.0.
🤖 Generated with Claude Code