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
93 changes: 93 additions & 0 deletions .github/release-notes/v2.2.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# RustyNES v2.2.2 — "Conduit" (libretro buildbot 10/10 + CI supply-chain hardening + single-source toolchain)

A **build, distribution, and CI-integrity patch**. It carries RustyNES onto RetroArch's own buildbot — the recipe now builds **all ten platform jobs**, after three rounds of diagnosis against a third-party pipeline we can neither push to nor re-run — hardens the GitHub Actions supply chain, and collapses the toolchain to a single pinned source of truth with **no `nightly` on any build path**.

**Zero emulation-core changes.** No file under `crates/rustynes-{cpu,ppu,apu,mappers,core}` is touched, so the deterministic `#![no_std]` chip stack, save-state / TAS / netplay-replay formats, and every golden vector are untouched *by construction*: **AccuracyCoin holds 141/141 (100.00%)**, nestest stays 0-diff, and `blargg_apu_2005` / `pal_apu_tests` (10/10) / `visual_regression` / the 60-ROM commercial oracle are all unchanged from v2.2.1.

One behavioural improvement does reach a shipped artifact: the libretro **tvOS** core is now built with `panic = "abort"` like every other platform, instead of the `panic = "unwind"` its previous `-Zbuild-std` path forced.

## Added

### Libretro buildbot recipe — the last step into RetroArch's core downloader

`.gitlab-ci.yml` covering Windows x64, Linux x64, macOS x64/arm64, Android (4 ABIs), iOS arm64, and tvOS arm64 via the shared `libretro-infrastructure/ci-templates`. Two structural fixes were needed to make the centrally-maintained templates work against this workspace, neither of which the templates can be asked to accommodate per-core:

- **Artifact-name collision.** The templates always run a fixed `mv lib${CORENAME}.<ext> ${CORENAME}_libretro.<ext>`, which would have doubled the `_libretro` suffix because the crate's compiled library name already contained it. `crates/rustynes-libretro/Cargo.toml` now sets `[lib] name = "rustynes"` (its `[package] name` stays `rustynes-libretro` so `-p rustynes-libretro` scoping is unaffected everywhere else), making `CORENAME=rustynes` resolve both halves correctly to the already-published `rustynes_libretro.info` name.
- **Build scoping.** The templates invoke a bare `cargo build --release --target <triple>` from the repo root with no `-p` flag. `[workspace] default-members = ["crates/rustynes-libretro"]` scopes that to the one crate instead of all 18 workspace members — in particular keeping the frontend's wgpu / winit / cpal / Wayland stack out of a cross-compile that has no business linking it.

### Libretro core feature completion

Native `RETRO_ENVIRONMENT_SET_MEMORY_MAPS` registration — the memory-descriptor path `rcheevos` prefers, alongside the existing legacy pointer API, with VRAM deliberately registered under a named `"PPU"` address space rather than the default CPU space (on the CPU bus `$2000-$2007` are PPU MMIO registers, not video RAM). An **FDS load-path fix**: `.fds` content was previously routed through the cartridge-only path and failed silently despite `valid_extensions` advertising it; it now detects the extension, routes to `Nes::from_disk`, and locates `disksys.rom` via `GET_SYSTEM_DIRECTORY` — plus a full disk-control interface for multi-side swapping from RetroArch's Quick Menu. Native Game Genie cheats (`on_cheat_set` / `on_cheat_reset`, deliberately excluded from serialized state). A `get_fastforwarding`-gated audio-push skip during RetroArch's fast-forward / rollback catch-up.

TAS and netplay needed **no** new libretro-side wiring: RetroArch's own rollback and movie/rewind machinery already ride the existing `on_serialize` / `on_unserialize` hooks.

### `libretro-cross` CI guard job

`.github/workflows/ci.yml` gains a job cross-checking `rustynes-libretro` against the buildbot ABI families a Linux runner can model faithfully — MinGW-Windows and Android/NDK. There was previously **zero** libretro coverage in GitHub Actions, which is precisely why every defect below reached a third-party buildbot first. The Apple families are excluded deliberately: bindgen needs a real per-target sysroot, there is no Apple SDK on a Linux runner, and feeding it host glibc headers would generate Apple bindings from Linux headers — a lookalike, not a rehearsal.

## Fixed

### Buildbot pipeline: 1 of 10 jobs green → all 10 building

The first run ([pipeline #91899](https://git.libretro.com/libretro/RustyNES/-/pipelines/91899)) passed only `libretro-build-linux-x64`. Four independent defects surfaced across three rounds — all on our side, none in libretro's build images:

- **Missing cross-compile targets (8 jobs).** `rust-toolchain.toml` pins the channel, so rustup installs a *fresh* toolchain inside the build image carrying only the host std plus the two targets that file declares — bypassing the image's own default toolchain, on which every libretro cross target is pre-provisioned. Every non-host job died with `E0463: can't find crate for core`; Linux x64 survived only because its target *is* the host triple. Each job now runs `rustup target add ${RUST_TARGET}` into the pinned toolchain. Deliberately **not** solved by extending `rust-toolchain.toml`'s `targets = [...]`, which would make every contributor and every GitHub Actions job download ~8 extra `rust-std` components on each toolchain install — a large permanent cost on everyone to serve one downstream CI.
- **Upstream `rust-libretro` MinGW ABI bug (Windows).** Masked behind the target failure and never previously reached. `rust-libretro 0.3.2` casts a keycode to `i32` under `cfg(target_family = "windows")`, but C enum signedness follows the **ABI**, not the OS family: only the MSVC ABI gives plain enums `int`. Under MinGW — what the buildbot's `x86_64-pc-windows-gnu` job uses — bindgen emits `retro_key(c_uint)` and the crate fails with `E0308`. Upstream has had no commit since 2023-02 and 0.3.2 is its newest release, so `.cargo/config.toml` points bindgen's clang at the matching MSVC triple for **both** MinGW targets (`x86_64-pc-windows-gnu` for the buildbot, `i686-pc-windows-gnu` for the crate `Makefile`'s legacy libretro-super path, verified to fail identically). Surgical: the generated bindings differ by 28 lines, all enum signedness, with no struct layout, signature, or type size affected.
- **tvOS MSRV and `panic_abort`.** Its template overrides `script` with `cargo +nightly build -Zbuild-std`, which bypasses the channel pin onto the image's stale nightly — below the workspace MSRV, failing cargo's gate before compiling anything.
- **tvOS `-C ar`, removed in Rust 1.97.** Refreshing that nightly then hit ``error: `-C ar`: this option has been removed``. The build image injects `-Car=<path>,Clink-arg=...` into *every* Apple job, and that long-deprecated no-op became a hard error in 1.97 (bisected: 1.96.1 warns, 1.97.1 errors).

The last two are now moot rather than worked around — see below.

### tvOS builds on the pinned stable toolchain

`aarch64-apple-tvos` was a **tier-3** target when the upstream template was written, with no distributed `rust-std`, making `-Zbuild-std` — and therefore nightly — the only way to build it. **The target has since been promoted and rustup now ships a complete prebuilt std for it, `panic_abort` included.** Verified against the pinned toolchain: `rustup target add aarch64-apple-tvos` installs 26 rlibs, and `cargo check --release -p rustynes-libretro --target aarch64-apple-tvos` compiles the entire dependency graph, bindgen included.

The job now restores the shared Apple build script via `!reference [.libretro-rust-apple-base, script]` rather than duplicating it, so upstream fixes keep reaching it. That single change **dissolved three stacked workarounds instead of adding a fourth**: the nightly-channel reinstall, the `CARGO_PROFILE_RELEASE_PANIC=unwind` override, and the `-C ar` clearing. tvOS is now identical in shape to the osx / ios jobs and honours `panic = "abort"` like every other platform.

`-C ar` remains a latent hazard for **all four** Apple jobs — they are green only because the pinned channel treats it as a warning — so the hazard and its remedy are recorded in `rust-toolchain.toml`, at the line a channel bump would edit.

## Security

### `persist-credentials: false` on all 19 CI checkouts

`actions/checkout` defaults to writing the workflow `GITHUB_TOKEN` into `.git/config`, where any code the job subsequently executes from the checkout — Cargo build scripts, proc macros, test binaries, Gradle build scripts, shell scripts, the MkDocs build — can read it. On a pull request that tree is by definition unreviewed code, and nearly every job compiles or runs it.

Audited rather than applied blanket: `.github/actions/rust-setup` performs no checkout of its own, so call-site hardening is complete coverage; and **no job was found to need the credential** — nothing pushes commits, tags, or branches, there are no submodules, GitHub Pages uses the OIDC flow rather than a `gh-pages` push, and `gh release create` / `softprops/action-gh-release` / `fastlane match` authenticate by other means. The highest-exposure job is `web.yml`'s `build`, which declares `pages: write` + `id-token: write` at *workflow* level.

### Fail-closed release-tag check

`release-auto.yml`'s tag-existence check previously ran `git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1`, collapsing three distinct outcomes — tag present, tag absent, and *lookup failed* — into two, with any non-zero exit read as "absent". A transient network or auth blip would therefore have pushed an already-released version down the release path.

It now queries `gh api .../git/matching-refs/tags/<tag>`, chosen over `git/ref/tags/<tag>` because it answers "absent" with HTTP 200 and an empty array instead of a 404, so a genuine miss can never be mistaken for an error. `matching-refs` matches by **prefix**, so the exact ref is compared in `jq` — load-bearing, not defensive: `v2.2` prefix-matches two real tags while exact-matching none. Under `set -euo pipefail` a `gh` failure and a non-array body both abort the job. The non-array case needs an explicit `type != "array"` guard rather than relying on `jq` to error on its own: a body of `{}` makes `.[]` iterate zero object values, so the filter yields an empty list and `length` is `0` — indistinguishable from a genuine "tag absent", which takes the *release* path. Every other malformed shape (a bare string, `null`, an object with entries) does abort unaided, which is precisely what made `{}` easy to miss.

### `dtolnay/rust-toolchain` SHA-pinned

Moved off `@master` — a branch that advances on every upstream commit, so each run silently resolved to whatever HEAD happened to be. That composite feeds 12 of the repo's 19 checkouts, including `release.yml` (`contents: write`, builds the shipped binaries) and `web.yml` (`pages: write` + `id-token: write`), and it is the action that *installs the compiler* — the hardest place to notice a compromise from build output. The trailing `# v1` comment keeps Dependabot's `github-actions` ecosystem able to advance the pin.

### Fuzz and parser hardening carried forward

The three whitespace-fixing pre-commit hooks gained a shared `exclude` anchor so `pre-commit run --all-files` can no longer rewrite vendored and upstream trees (it had silently reformatted 41 files across the vendored TriCNES C#, vendored rcheevos C, `ref-docs/`, an upstream font licence, and upstream test-ROM READMEs — destroying exactly the byte-identical-to-upstream property those trees exist for). Scope is deliberately narrower than `.markdownlintignore`: only content we did not author.

## Changed

### One toolchain, resolved from one file

`.github/actions/rust-setup` now parses `[toolchain].channel` out of `rust-toolchain.toml` and installs that, so **no toolchain version literal remains anywhere under `.github/`** and a toolchain bump is a one-line edit.

Previously the composite defaulted to `stable` and 5 of its 12 call sites overrode that explicitly. This was **misleading rather than broken**: `rust-toolchain.toml` is a directory override that outranks the `rustup default` the action performs, so every job was *already* compiling on the pinned channel (rustup logs `overridden by .../rust-toolchain.toml`). The `stable` default merely installed a second toolchain nothing used — and made the workflows *read* as though the matrix tested against latest stable, a guarantee the repo appeared to offer and never provided.

Extraction is scoped to the `[toolchain]` table rather than "the first `channel` key in the file": a `channel` key in any earlier table would otherwise be installed instead, which for a `channel = "nightly"` decoy would have silently reintroduced the toolchain this release removes while still reporting success. Anything unparseable — missing table, single-quoted value, empty file — aborts the job with a `::error::` annotation instead of guessing.

**Nightly now survives in exactly two places, neither of them a build, test, lint, docs, release, or packaging gate:** `cargo fuzz`, which requires it for the sanitizer flags it threads through `rustc`, and the dormant `rustynes-monetization` crate's standalone `uniffi-bindgen`. Neither can move to stable without breaking the tool, so both are documented rather than quietly worked around.

### Dependency consolidation (closes Dependabot #313–#315)

All three open Dependabot PRs rolled into one reviewed change plus a full `cargo update --workspace` sweep, with **no source changes** and the `#![no_std]` core untouched. This surfaced a real latent break: **lz4_flex 0.14+** requires the crate's own `alloc` feature explicitly for `compress_prepend_size` / `decompress_size_prepended` (used by `rewind.rs` / `zwinder.rs`), having split no_std support into an `alloc`-vs-`std` distinction that did not exist in 0.13. A `cargo build --workspace` cannot catch this — `rustynes-core`'s default-on `std` feature implies it through cargo's feature unification — only the standalone `no_std` cross-compile does.

## Notes

- Version: workspace `2.2.1 → 2.2.2`. No mobile app version change (`versionCode` / `versionName` on Android and `MARKETING_VERSION` on iOS stay frozen at their v2.0.x host-only release points; the joint mobile store launch remains **v2.3.0**).
- `.gitignore` gains `*.dSYM/` and `*.dSYM.zip` — the Apple libretro jobs and the iOS xcframework build emit debug-symbol bundles beside the library, which `*.dylib` did not cover.
- **No accuracy or oracle number moves.** AccuracyCoin 141/141, nestest 0-diff, `blargg_apu_2005` / `pal_apu_tests` (10/10) / `visual_regression` / the 60-ROM commercial oracle all unchanged. Save-state / TAS / netplay-replay formats untouched.
- Full host gate green: fmt · clippy (all feature combos + wasm32) · rustdoc (`-D warnings`) · `no_std` cross-compile (`thumbv7em`) · markdownlint · `cargo deny check` · both `libretro-cross` guard legs.
- **Not verifiable here:** the tvOS *link* step. There is no tvOS SDK or linker on the development host and we cannot run a pipeline on `git.libretro.com`, so the compile path is verified and the link step is not. Issue #311 stays open until libretro confirms a clean pipeline.
16 changes: 15 additions & 1 deletion .github/workflows/release-auto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,22 @@ jobs:
# credentials, which is what let the checkout above join the rest of
# the #318 sweep.
refs_json="$(gh api "repos/${GITHUB_REPOSITORY}/git/matching-refs/tags/${tag}")"
# The `type != "array"` guard is load-bearing, not belt-and-braces.
# Without it, a body of `{}` makes `.[]` iterate zero object VALUES,
# so the filter yields an empty list and `length` is 0 — identical to
# a genuine "tag absent", which takes the RELEASE path. That is a
# fail-OPEN on the one decision this step exists to get right.
# (Other malformed shapes — a bare string, null, an object with
# entries — do abort on their own, because `.[]` or `.ref` errors;
# `{}` is the shape that slips through, which is exactly why an
# explicit type check is needed rather than relying on jq erroring.)
count="$(printf '%s\n' "$refs_json" \
| jq --arg r "refs/tags/${tag}" '[.[] | select(.ref == $r)] | length')"
| jq --arg r "refs/tags/${tag}" '
if type != "array" then
error("expected a JSON array from git/matching-refs")
else
[.[] | select(.ref == $r)] | length
end')"
case "$count" in
0)
echo "Version ${version} has no ${tag} tag yet - will release."
Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ Cargo.lock
*.a
*.lib
*.pdb
# Apple debug symbols. The libretro Apple jobs (osx / ios / tvos) and the iOS
# xcframework build emit `<name>.dylib.dSYM` bundles next to the library, and
# the buildbot additionally zips them for upload — `*.dylib` above covers the
# library but neither the bundle directory nor the archive. Building any Apple
# libretro target locally (`crates/rustynes-libretro/Makefile`) leaves both in
# the working tree.
*.dSYM/
*.dSYM.zip

# --- WebAssembly / trunk / wasm-pack ---
/pkg/
Expand Down
Loading