diff --git a/.github/workflows/renovate-hash-sync.yml b/.github/workflows/renovate-hash-sync.yml index da96fe0..64552f3 100644 --- a/.github/workflows/renovate-hash-sync.yml +++ b/.github/workflows/renovate-hash-sync.yml @@ -161,6 +161,11 @@ on: - "package/bcm20702-firmware/bcm20702-firmware.mk" - "package/libchdr/libchdr.mk" - "package/lzma-sdk/lzma-sdk.mk" + # rcheevos (RetroAchievements client library). Ordinary github + # tag-archive pin, covered by the generic loop in step 1. Same + # both-lists-or-silent-failure note as rtl8852cu-morrownr above: it must + # appear HERE and in HASH_SYNC_PACKAGES below. + - "package/rcheevos/rcheevos.mk" # azcopy (case 7). ADDED 2026-08-28, when that case was added -- before # that an azcopy bump PR deliberately stayed red until a human ran # package/azcopy/azcopy.hash's regeneration recipe by hand. Same @@ -280,7 +285,7 @@ jobs: # tarballs from the loop's .../archive/.tar.gz form and Buildroot's # .../archive//.tar.gz form -- see package/dualsensectl/ # dualsensectl.hash. - HASH_SYNC_PACKAGES: "rtl8852cu-morrownr aic8800 xone midilink munt bcm20702-firmware libchdr dualsensectl ltunify" + HASH_SYNC_PACKAGES: "rtl8852cu-morrownr aic8800 xone midilink munt bcm20702-firmware libchdr rcheevos dualsensectl ltunify" steps: # A manual run pushes commits to whatever branch it is pointed at, so diff --git a/Config.in b/Config.in index 311a193..6409980 100644 --- a/Config.in +++ b/Config.in @@ -132,6 +132,16 @@ menu "Main_MiSTer shared libraries" # independently selectable. source "$BR2_EXTERNAL_MISTER_PATH/package/lzma-sdk/Config.in" source "$BR2_EXTERNAL_MISTER_PATH/package/libchdr/Config.in" + # rcheevos (RetroAchievements client library) is the ODD ONE OUT in this + # menu and is sourced last to keep that visible. lzma-sdk and libchdr are + # here to REPLACE code Main_MiSTer already vendors -- that is what "the + # Main_MiSTer shared-lib refactor" means. rcheevos replaces nothing: + # neither Main nor this tree references it yet. It is in this menu anyway + # because the menu's actual contract is "shared libraries this tree + # authors for Main_MiSTer to link, which upstream Buildroot does not + # carry", and a second menu holding one library would say less. It has no + # dependency on either sibling and toggles independently. + source "$BR2_EXTERNAL_MISTER_PATH/package/rcheevos/Config.in" endmenu menu "Archival / on-device update tooling" diff --git a/README.md b/README.md index d339547..787e8b3 100644 --- a/README.md +++ b/README.md @@ -678,7 +678,7 @@ board/mister/de10nano/ rootfs-overlay/ init scripts, sshd wiring, MiSTer-specific files post-build.sh /MiSTer.version stamping, parity fixups post-image.sh linux.img assembly + contract checks -package/ 16 packages: Realtek Wi-Fi, xone, libchdr, lzma-sdk, midilink, munt… +package/ 23 packages: Wi-Fi/controller drivers, libchdr, lzma-sdk, rcheevos, midilink, munt… scripts/ the verification suite, hash-sync, SD-card builder, kernel export docs/ ADRs, parity audits, the kernel reconciliation, user docs .github/ 9 workflows + 4 composite actions diff --git a/TASKS.md b/TASKS.md index 644c180..5d4c331 100644 --- a/TASKS.md +++ b/TASKS.md @@ -1054,10 +1054,30 @@ is), never whether it has been proven yet. because that check runs at the end of an ~80-minute image build. Fixed by teaching the step to refresh `*_LICENSE_FILES` hashes and diff any that changed; see `scripts/hash-sync-ip7z-src.sh`'s header. - Still unproven: the generic github-package loop (all 12 github-sourced pins, including - libchdr) and the sdcard-payload step have never run against a real PR — treat those - regexes/URLs as reviewed-by-hand, not proven. See `docs/renovate.md`'s "Unverified / - what to check on first run". + Still unproven **against a real PR**: the generic github-package loop (every + github-sourced pin in `HASH_SYNC_PACKAGES`, libchdr and rcheevos included) and the + sdcard-payload step. Partial credit for the loop as of 2026-09-16, when + `package/rcheevos` was added — it was exercised standalone, over the network, on BOTH + of its branches: + - *already-current*: against the real pin, it derived the archive URL and the + `-.tar.gz` filename and agreed with the hand-computed sha256. + - *refreshed*: against a fixture with the pin moved back to `v12.4.0` and a + deliberately wrong tarball hash, it followed the new ref and rewrote the line to + `7fb1a43b…`, independently confirmed by fetching Buildroot's own + `.../archive//.tar.gz` URL form, and it left the three licence-file + lines alone as documented. **This is the branch that matters**: an + `already-current` run never executes the rewrite at all, so it could not have + caught a stale-write bug of the kind that once left `linux.hash` untouched while + the job reported success three times. + + So the loop's own URL/filename/hash/rewrite logic is no longer "reviewed-by-hand, not + proven". The `paths:` trigger is proven too, incidentally and on a real PR: #182 (the + PR that added `package/rcheevos`) changed exactly ONE file matching this workflow's + `paths:` filter — the new `package/rcheevos/rcheevos.mk` — and the workflow fired, + then correctly skipped itself as a non-Renovate branch. What remains unproven is the + rest of the workflow AROUND the loop — the commit-back and the outcomes gate — + which neither a skipped run nor a standalone run reaches, so the item stays open. See + `docs/renovate.md`'s "Unverified / what to check on first run". **Done when:** a real Renovate PR has exercised each of the two still-unproven paths at least once with a passing (or fixed-then-passing) run, and `docs/renovate.md` drops the "unverified" caveat for each path once proven. diff --git a/docs/buildroot-config.md b/docs/buildroot-config.md index b527d86..cd842dc 100644 --- a/docs/buildroot-config.md +++ b/docs/buildroot-config.md @@ -762,8 +762,9 @@ glibc; it does not generate any locale *data*. That is a separate knob — §5.4 The BR2_EXTERNAL half of the Main_MiSTer shared-lib refactor (no task ID — referenced by name): Main stops vendoring `lib/{lzma,zstd,miniz,libchdr}` and links Buildroot-provided shared libraries; the upstream half (zstd, minizip-ng) -is §5.4. Both packages are authored under `package/`; see -`docs/main-shared-libs.md`. +is §5.4. All three packages are authored under `package/`; see +`docs/main-shared-libs.md`. The third, rcheevos, shares the menu but not the +refactor — it replaces no vendored code. - `BR2_PACKAGE_LZMA_SDK=y` — 7-Zip LZMA SDK 26.03 as `liblzma-sdk.so.`; the full-version SONAME is the deliberate loud-ABI-event policy: the Main @@ -773,6 +774,20 @@ is §5.4. Both packages are authored under `package/`; see Findzstd pkg-config fallback (the tag cannot configure against Buildroot's zstd); system zlib/zstd/lzma-sdk via our 3 patches; exports `chd_*` ONLY (version script), so no symbol collisions with minizip-ng et al. +- `BR2_PACKAGE_RCHEEVOS=y` — the RetroAchievements client library, + `librcheevos.so.12.5.0` (273 KiB stripped in the rootfs — 279,580 bytes). **NOTHING LINKS IT YET** — unlike the + two above it replaces no vendored Main code; it is shipped so a consumer + can link it, and its only CI cover is the presence assertion in + `scripts/ci-tests.sh` (a library with no consumer has no other way to fail + visibly). Full-version SONAME for the same loud-ABI-event reason as + lzma-sdk, but on harder evidence: upstream has changed caller-allocated + public struct layouts in *patch* releases (v10.7.1, v10.3.3, v6.0.1). Built + `-DRC_SHARED -fvisibility=hidden` so only the 264 `RC_EXPORT` entry points + escape rather than 477 — the suppressed ones include `md5_*` and `AES_*`, + which would otherwise interpose against Main's own `lib/md5`. A consumer + must keep the `-DRC_CLIENT_SUPPORTS_HASH` that `rcheevos.pc` supplies: it + gates public declarations in `rc_client.h`, not just implementation + (`package/rcheevos/rcheevos.mk`). ### 5.6 graphics / fonts diff --git a/docs/ci.md b/docs/ci.md index af1d104..25b331a 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -2421,13 +2421,20 @@ idiom** and should get the same treatment when next touched. ### Safety model, the refresh cases: where each refreshed value legitimately comes from -1. **The 14 github-sourced packages** (`package/*/*.mk` + their `.hash`): the - 12 driver/firmware pins plus `libchdr` (a userspace shared library — the - Main_MiSTer shared-lib refactor) and `dualsensectl` (a userspace CLI — - the DualSense operator tool). Neither of the last two is a driver, but - both have the exact same `$(call github,...)` archive shape; - `dualsensectl` differs only in pinning a `v`-prefixed tag rather than a - commit SHA, which the loop handles without special-casing. Their own `.hash` file +1. **The github-sourced packages named in `HASH_SYNC_PACKAGES`** + (`package/*/*.mk` + their `.hash`): the driver/firmware pins plus + `libchdr` and `rcheevos` (userspace shared libraries — the first from the + Main_MiSTer shared-lib refactor, the second the RetroAchievements client + library) and `dualsensectl` + `ltunify` (userspace CLIs — the DualSense + operator tool and the Logitech pairing tool). None of the last four is a + driver, but all have the exact same `$(call github,...)` archive shape; + `dualsensectl` and `rcheevos` differ only in pinning a `v`-prefixed tag + rather than a commit SHA, which the loop handles without special-casing — + demonstrated on 2026-09-16, when the loop was run standalone against the + real `rcheevos` pin and derived the same URL, filename and sha256 the pin + was written with. Count the roster in `HASH_SYNC_PACKAGES` itself rather + than trusting a number here; that env var is the single source of truth + and a hardcoded count here has gone stale before. Their own `.hash` file headers already say the hash is "locally computed" — GitHub publishes no signed manifest for a commit/tag archive tarball, so `sha256sum` of a freshly-fetched tarball from the ACTUAL pinned owner/repo/ref IS the diff --git a/docs/main-shared-libs.md b/docs/main-shared-libs.md index 4c81ea9..85c5c75 100644 --- a/docs/main-shared-libs.md +++ b/docs/main-shared-libs.md @@ -15,6 +15,13 @@ Three of the five packages are upstream Buildroot, enabled straight from authored in this tree under `package/` and sourced via the "Main_MiSTer shared libraries" menu in the top-level `Config.in`. +A **sixth** library, `rcheevos`, is built and shipped from that same menu but +is **not part of this refactor** and is tracked separately below. The +distinction is worth keeping: the five replace code Main already vendors, so +each one's success condition is "Main stops carrying a copy". rcheevos +replaces nothing — it adds a capability the image did not have, and nothing +links it yet. + ## The five packages | Package | Provider | SONAME | Headers | pkg-config | Notes | @@ -25,6 +32,89 @@ authored in this tree under `package/` and sourced via the | lzma-sdk (`BR2_PACKAGE_LZMA_SDK`) | BR2_EXTERNAL (`package/lzma-sdk`) | `liblzma-sdk.so.26.03` | `include/lzma-sdk/` (13 headers, Main's vendored `lib/lzma` set 1:1) | `lzma-sdk.pc` | 7-Zip LZMA SDK 26.03, built `-DZ7_ST` (single-threaded — same as Main's vendored build). **The SONAME is the full SDK version, deliberately**: upstream gives no ABI guarantees between releases and the API embeds caller-allocated structs (`CLzmaDec` by value), so a silent struct-layout change is memory corruption, not an error. A full-version SONAME turns every SDK bump into a *loud* ABI event — an old binary refuses to load with a clean linker error. That matters here because the `MiSTer` binary lives on `/media/fat` and **survives rootfs reflashes**; stale-binary-meets-new-rootfs is the expected failure mode. NOT xz-utils' `liblzma.so.5` — entirely different API. | | libchdr (`BR2_PACKAGE_LIBCHDR`) | BR2_EXTERNAL (`package/libchdr`) | `libchdr.so.0` (real file `libchdr.so.0.3`) | `include/libchdr/` | `libchdr.pc` | Commit-pinned past `v0.3.0` (the tag can't configure against Buildroot's zstd — no `Findzstd` pkg-config fallback yet). Built against **system zlib/zstd/lzma-sdk** via our patches 0001–0003; the header-only **dr_flac stays bundled** (header-only by design, and `libchdr_flac.c` pokes drflac internals — no `.so` exists to unbundle to). Exports **`chd_*` only** (upstream's version script `src/link.T`), so no `mz_*` or other symbol collision with minizip-ng et al. **"System zlib" here is zlib-ng in `ZLIB_COMPAT` mode** (`BR2_PACKAGE_ZLIB_NG`, defconfig compression block): same `libz.so.1`, same `zlib.h`, so nothing changes at link or `pkg-config` time — and it is the whole point of unbundling, since a *shared* libchdr is what actually reaches system zlib (stock's static libchdr decodes through its own vendored miniz instead). Measured on the rig: CHD audio-hunk decode p90 −10 to −11 %, LZMA-dominated data hunks unchanged (`harness/rig/chd-decode-optimization.md` in Main_MiSTer). | +## The sixth library: rcheevos (not part of the refactor) + +| Package | Provider | SONAME | Headers | pkg-config | Notes | +|---|---|---|---|---|---| +| rcheevos (`BR2_PACKAGE_RCHEEVOS`) | BR2_EXTERNAL (`package/rcheevos`) | `librcheevos.so.12.5.0` | `include/rcheevos/` (15 from upstream's `include/`, plus `rc_version.h` from `src/`) | `rcheevos.pc` | RetroAchievements' own client library — achievement/leaderboard evaluation, the RetroAchievements web API marshalling (`rapi`), game-identification hashing (`rhash`), and `rc_client`. Tag-pinned `v12.5.0`. **No consumer links it yet**; it is shipped so one can. 273 KiB as shipped (279,580 bytes, stripped; the staging copy is 330 KiB unstripped). Upstream ships no Unix build system, so the package compiles it directly like `lzma-sdk`. Three sources are excluded (`rc_libretro.c` needs ``; `rc_client_external.c` and `rc_client_raintegration.c` are empty translation units without their Windows/external-ABI defines). | + +Two properties of this one are worth knowing before linking it: + +- **The SONAME is the full version, and here that is not a precaution — + upstream has changed caller-allocated public struct layouts in *patch* + releases.** `v10.7.1` added `char owns_self;` to the public `rc_runtime_t`; + `v10.3.3` inserted `const char* display_name;` into the *middle* of + `rc_api_login_response_t`; `v6.0.1` redefined `RC_ALIGNMENT` from `8` to + `sizeof(void*)`, which changes padding library-wide on a 32-bit target like + this one. Minor releases do it too (`v12.4.0` grew `rc_client_user_t`). + Derived by diffing `include/` across all 55 upstream tags. Same + `/media/fat` stale-binary hazard as `lzma-sdk`, with harder evidence: every + bump must be a clean refuse-to-load, so every bump is a rebuild-your- + consumer event. +- **It is built `-DRC_SHARED -fvisibility=hidden`, so only the 264 + `RC_EXPORT`-annotated entry points are exported** (477 without). This is + not tidiness: the suppressed symbols include `md5_init`/`md5_append`/ + `md5_finish` and six `AES_*` entry points, and ELF interposition is + first-definition-wins, so exporting those into a process that has its own + md5 — which Main does, `lib/md5` — is a silent wrong-function bind rather + than a link error. It is the same guarantee libchdr gets from its version + script, obtained a different way. Patch `0001` is load-bearing for it: + `rc_util.h` is the one public header upstream left un-annotated, so without + it nine declared entry points would be hidden and the installed headers + would promise an API the `.so` does not export. + +`rcheevos.pc` carries **`-DRC_CLIENT_SUPPORTS_HASH` and `-DRC_IMPORT` in its +`Cflags`**, and a consumer must not drop them: the first gates *public header +declarations* (`rc_client.h` lines 265 and 333, the +`rc_client_begin_identify_and_load_game` family), so without it a consumer's +`rc_client.h` hides functions the `.so` genuinely exports. + +### Its two vendored files, and why neither is unbundled + +rcheevos has no `deps/` or `third_party/` directory, so it *looks* like it is +all upstream's own code. It is not, quite: two vendored third-party +implementations are compiled into the shipped `.so`, and both carry their own +grant, which is why `RCHEEVOS_LICENSE` is `MIT, Zlib (md5), Unlicense +(tiny-AES-c)` and not plain MIT. + +| File | Origin | Used by | Unbundle to `/usr/lib`? | +|---|---|---|---| +| `src/rhash/md5.{c,h}` | L. Peter Deutsch / Aladdin, 1999–2002; the notice is the **zlib licence verbatim** (upstream Buildroot spells this same file `Zlib (md5)` in `package/rtty`) | 12 files across `rhash`, `rapi` and the runtime | **No** — see below | +| `src/rhash/aes.{c,h}` | `kokke/tiny-AES-c` @ `f06ac37`, "with unused code excised", Unlicense | `hash_encrypted.c` only | **No** — the question is whether to compile it at all | + +**md5 → OpenSSL `libcrypto.so.3`** (which this image already ships) was +considered and **rejected**. OpenSSL 3.x deprecates the low-level +`MD5_Init`/`MD5_Update`/`MD5_Final` API in favour of `EVP`, which is a +different shape from the incremental `md5_init`/`md5_append`/`md5_finish` that +rcheevos threads through 12 files — so this is an invasive, permanently-carried +patch re-validated on every monthly bump. The maintenance argument that +normally justifies unbundling does not apply either: `md5.c` has changed +**twice in rcheevos's entire history** (last 2023-02-23) and `md5.h` **once** +(2020-01-04), and MD5 here is content identification, not security, so there is +no CVE stream to track. The one argument that survives is **performance** — +OpenSSL has hand-written ARM assembly MD5 where this is plain C, which could +matter when hashing a full disc image. That is **unmeasured and deliberately +left so**: it cannot be timed honestly under `qemu-arm` (which JITs asm and C +alike), it needs the rig, and nothing links rcheevos yet. Revisit it with a +real measurement when a consumer exists, not before. + +**aes** is not an unbundling question at all. `hash_encrypted.c` is **entirely +Nintendo 3DS** — every function in it is `rc_hash_nintendo_3ds*` (CIA/NCCH/3DSX +container decryption) — so on a board with no 3DS core the honest option is not +"link a different AES" but `-DRC_HASH_NO_ENCRYPTED`, which upstream supports as +a first-class switch. **Measured: that saves 8,088 bytes** (279,580 → 271,492 +stripped). It is left **on**, for two reasons: 8 KB is noise against the size +budget, and narrowing a library that nothing links yet is the wrong default, +since a consumer cannot turn it back on without a rebuild. Note it would not +reduce "AES in the image" anyway — `libcrypto.so.3` ships a much larger one. +If it is ever wanted off, the define is **ABI-visible** (it gates declarations +in `include/rc_hash.h` lines 100–126 and 141), so it must go in `rcheevos.pc`'s +`Cflags` as well as the build, exactly like `RC_CLIENT_SUPPORTS_HASH`. + +Nothing else is vendored: `hash_zip.c` walks the zip central directory and +never inflates, so there is no bundled zlib/miniz to unbundle — unlike +`libchdr`, whose whole patch series exists for that. + ## How Main links these Against the Buildroot staging sysroot, by pkg-config name — no hardcoded @@ -73,6 +163,13 @@ Mapping from Main's vendored `lib/` dirs to their replacements - **The stage-1 initramfs stacks / installer defconfig are deliberately unchanged** — static busybox, no ABI surface, nothing there links any of these. -- Renovate manages both new pins (`lib-pin` label); hashes auto-refresh via - `renovate-hash-sync.yml` (generic loop for libchdr, a bespoke - release-asset step for lzma-sdk). See `docs/renovate.md`. +- Renovate manages all three BR2_EXTERNAL pins (`lib-pin` label); hashes + auto-refresh via `renovate-hash-sync.yml` (generic loop for libchdr and + rcheevos, a bespoke release-asset step for lzma-sdk). See + `docs/renovate.md`. +- **rcheevos has no Main-side mapping row above and should not get one** + until something links it. `scripts/check-abi.sh` stays untouched for it for + the same reason it does for the other five, only more so — no shipped + binary DT_NEEDs `librcheevos.so.12.5.0`. Its only CI cover is the + presence-in-rootfs assertion in `scripts/ci-tests.sh`, which is deliberate: + a library with no consumer has no other way to fail visibly. diff --git a/docs/renovate.md b/docs/renovate.md index 6085307..4a2ee4e 100644 --- a/docs/renovate.md +++ b/docs/renovate.md @@ -54,6 +54,7 @@ for the specific pieces most likely to need a fix on the first live run. | munt tag pin | `package/munt/munt.mk` | `github-tags` datasource, custom `regex:` versioning for the `munt_MAJOR_MINOR_PATCH` tag scheme | `package/munt/munt.hash` — auto-refreshed | | bcm20702-firmware **commit** pin | `package/bcm20702-firmware/bcm20702-firmware.mk` | `git-refs` datasource tracking `master` HEAD via `currentDigest`. **Was** a `github-tags`/`loose` tag pin until 2026-07-19 — see "Why this one is a commit pin" below | `package/bcm20702-firmware/bcm20702-firmware.hash` — auto-refreshed | | libchdr commit-SHA pin (Main_MiSTer shared-lib refactor; labeled `lib-pin`) | `package/libchdr/libchdr.mk` | `customManagers` regex, `git-refs` datasource tracking `rtissera/libchdr`'s `master` HEAD via `currentDigest` (a commit pin, not the stale `v0.3.0` tag — see the .mk's header) | `package/libchdr/libchdr.hash` — auto-refreshed by `renovate-hash-sync.yml`'s generic loop (standard `$(call github,...)` archive tarball) | +| rcheevos tag pin (RetroAchievements client library; labeled `lib-pin`) | `package/rcheevos/rcheevos.mk` | `customManagers` regex, `github-tags` datasource over `RetroAchievements/rcheevos` with `loose` versioning. A **tag**, unlike the libchdr row above — upstream tags every release and its README says to integrate against `master`, which *is* the newest tag. The captured `currentValue` **includes the leading `v`**, same trick and same reason as the dualsensectl row below | `package/rcheevos/rcheevos.hash` — auto-refreshed by `renovate-hash-sync.yml`'s generic loop (standard `$(call github,...)` archive tarball; **verified against the real package on 2026-09-16**, not just assumed to fit — the loop derived the same URL and hash and reported `already-current`). **Read every bump diff for public struct changes**: upstream has changed caller-allocated layouts in *patch* releases, which is survivable only because the SONAME is the full version — see `docs/main-shared-libs.md` | | 2 ip7z/7zip tag pins (`lzma-sdk` for the Main_MiSTer shared-lib refactor, `7zip` for the `7zz` archiver + the `/media/fat/linux/7za` updater binary — ADR 0023; both labeled `lib-pin`) | `package/lzma-sdk/lzma-sdk.mk`, `package/7zip/7zip.mk` | one `customManagers` regex **per file**, both `github-tags` over `ip7z/7zip` with `loose` versioning. **Same `depName` for both, so Renovate emits one PR touching both** — the two packages compile different halves of the identical release asset and must not drift apart. Only the `*_VERSION` line is managed in each — `*_SOURCE` derives from it via `$(subst)` in the .mk | `package/lzma-sdk/lzma-sdk.hash` and `package/7zip/7zip.hash` — both auto-refreshed by `renovate-hash-sync.yml`'s **bespoke ip7z/7zip step** (`scripts/hash-sync-ip7z-src.sh`, table-driven over both; release-*asset* URL, dots-stripped filename `7z2603-src.tar.xz`; does not fit the generic loop; also refreshes each package's `*_LICENSE_FILES` hashes and diffs any that changed — see case 3 below) | | dualsensectl tag pin (DualSense operator CLI; labeled `tool-pin`) | `package/dualsensectl/dualsensectl.mk` | `customManagers` regex, `github-tags` datasource over `nowrep/dualsensectl` with plain `loose` versioning — upstream tags ordinary `vMAJOR.MINOR` releases, so no bespoke scheme is needed (unlike munt). The captured `currentValue` **includes the leading `v`** on purpose: `scripts/hash-sync-github-packages.sh` reuses the literal `*_VERSION` string as both the archive ref and the `-.tar.gz` filename, so splitting the prefix off would desync the two | `package/dualsensectl/dualsensectl.hash` — auto-refreshed by `renovate-hash-sync.yml`'s generic loop (standard `$(call github,...)` archive tarball) | | azcopy release pin (Azure Storage CLI, off-device backup; labeled `tool-pin`) | `package/azcopy/azcopy.mk` | `customManagers` regex, `github-releases` datasource over `Azure/azure-storage-azcopy` with `semver` versioning and `extractVersionTemplate: ^v(?.*)$`. Upstream tags are `v`-prefixed but `AZCOPY_VERSION` holds the **bare** version — the opposite of the dualsensectl row above, and deliberate: that row's `v`-inside-the-version trick exists only to keep `scripts/hash-sync-github-packages.sh`'s generic loop working, and azcopy is **not in that loop** (next column), so the constraint does not apply and the bare number that `azcopy --version` prints is the more useful thing to have in the variable | `package/azcopy/azcopy.hash` — **auto-refreshed since 2026-08-28, by a case of its own (7), never by the generic loop.** azcopy is a `golang-package`: Buildroot sets `DOWNLOAD_POST_PROCESS = go`, runs `go mod vendor`, and hashes the repacked `-go2` tarball, so the generic loop's `curl \| sha256sum` would write the hash of the *pre-vendoring* archive — a wrong value that looks right. azcopy is therefore permanently absent from `HASH_SYNC_PACKAGES`, but it **is** in the workflow's `paths:` filter (it has to be, or nothing fires on a bump): `scripts/hash-sync-azcopy.sh` **rebuilds** the `-go2` tarball with Buildroot's own `support/download/go-post-process` and Buildroot's own pinned Go, then hashes it — and re-derives the `LICENSE`/`NOTICE.txt` lines too. Before that date this row read "**NOT auto-refreshed, and cannot be**", and older commits still show it; the curl-and-hash *method* was what could not work. If case 7 skips, the stale pin is still caught: the image build cannot help (azcopy is off by default, so `build.yml` never compiles it), but the **`azcopy version/hash pin consistency` step in `lint.yml`** rejects any change where `AZCOPY_VERSION` and the filename on the `sha256` line disagree, and the manual recipe in `azcopy.hash` remains the fallback. See `docs/azcopy.md` §5 and `docs/ci.md#renovate-hash-sync-safety-model` | @@ -61,12 +62,19 @@ for the specific pieces most likely to need a fix on the first live run. | CI container digests | **none today** — no workflow uses a `container:` block (see `docs/ci.md#no-container-disk-reclaim`; every build job runs bare on `ubuntu-26.04` so disk reclaim can reach the runner host). The `docker`/`pinDigests` rule is retained as a no-op in case one is ever reintroduced | Renovate's built-in `github-actions` manager, `docker` datasource, `pinDigests: true` | n/a — digest updates carry their own content-hash | | GitHub Actions | every SHA-pinned `uses:` line (with a `# vX.Y.Z` comment) across `build.yml`, `release.yml`, `reproducibility.yml`, `publish-db.yml` | Renovate's built-in `github-actions` manager (no custom config needed — it already understands "SHA-pinned + trailing semver comment" and updates both together) | n/a | -That is **23 customManagers entries** (Buildroot, the 6.18 kernel, the RT/beta -kernel, 10 driver commits, munt, bcm20702-firmware — 11 commit pins in total -once bcm20702 is counted — libchdr, lzma-sdk, 7zip, dualsensectl, azcopy, and -the 3 sdcard payload +That is **19 customManagers entries** (Buildroot, the 6.18 kernel, the RT/beta +kernel, 4 driver commits, munt, bcm20702-firmware — 5 commit pins in total +once bcm20702 is counted — libchdr, rcheevos, lzma-sdk, 7zip, dualsensectl, +ltunify, azcopy, and the 3 sdcard payload pins; note lzma-sdk and 7zip are 2 entries but only **1** upstream dependency, -so they raise a single PR) +so they raise a single PR). +This figure was **counted from `renovate.json`, not incremented**, on +2026-09-16 when the rcheevos row above was added. It had read "23 ... 10 +driver commits" since before 2026-09-10, when the seven deselected Realtek +fork packages were deleted and aic8800 added — a net −6 nobody applied here. +So the number went *down* while a manager was being added; do not read the +drop as a removal. Re-count rather than adjust: +`python3 -c "import json;print(len(json.load(open('renovate.json'))['customManagers']))"` plus the two built-in managers (`docker` digests, `github-actions`), covering every version/commit/digest pin this repository maintains by hand except the four listed below. @@ -317,11 +325,15 @@ docs/ci.md#renovate-hash-sync-outcomes-gate. **What it fixes automatically, and why each case is safe:** -1. **The 14 github-archive `.hash` files** (the 10 driver commit pins + munt + - bcm20702-firmware + libchdr + dualsensectl — the last two userspace, from - the Main_MiSTer shared-lib refactor and the DualSense operator tooling - respectively, but both the exact same - `$(call github,...)` tarball shape as the driver pins). Every one of +1. **The github-archive `.hash` files** named in `HASH_SYNC_PACKAGES` (the + driver commit pins + munt + bcm20702-firmware + libchdr + rcheevos + + dualsensectl + ltunify — the last four userspace, from the Main_MiSTer + shared-lib refactor, the RetroAchievements library, the DualSense operator + tooling and the Logitech pairing CLI respectively, but all the exact same + `$(call github,...)` tarball shape as the driver pins). `HASH_SYNC_PACKAGES` + in the workflow is the single source of truth for that list — count it + there rather than trusting a number here, which is why this line no longer + carries one. Every one of these `.hash` files' own header comment already documents that GitHub publishes no signed manifest for a commit/tag archive tarball, and that a **locally-computed** `sha256sum` diff --git a/package/mister-userspace/Config.in b/package/mister-userspace/Config.in index 2d989cf..e30f8fb 100644 --- a/package/mister-userspace/Config.in +++ b/package/mister-userspace/Config.in @@ -37,6 +37,7 @@ config BR2_PACKAGE_MISTER_USERSPACE # --- Main_MiSTer shared libs (§5.5, docs/main-shared-libs.md) --- select BR2_PACKAGE_LZMA_SDK select BR2_PACKAGE_LIBCHDR + select BR2_PACKAGE_RCHEEVOS # --- graphics / fonts (§5.6) --- select BR2_PACKAGE_FREETYPE select BR2_PACKAGE_LIBPNG diff --git a/package/rcheevos/0001-rc_util-annotate-the-public-helpers-with-RC_EXPORT.patch b/package/rcheevos/0001-rc_util-annotate-the-public-helpers-with-RC_EXPORT.patch new file mode 100644 index 0000000..856b1da --- /dev/null +++ b/package/rcheevos/0001-rc_util-annotate-the-public-helpers-with-RC_EXPORT.patch @@ -0,0 +1,92 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Michael C. Ferguson +Date: Tue, 16 Sep 2026 00:00:00 +0000 +Subject: [PATCH] rc_util: annotate the public helpers with RC_EXPORT/RC_CCONV + +rc_util.h is the ONLY public header in include/ whose function +declarations carry neither RC_EXPORT nor RC_CCONV -- every other one +(rc_client.h, rc_runtime.h, rc_api_*.h, rc_hash.h, rc_version.h, ...) +annotates all nine of its own. Verified mechanically across all 16 +installed headers at v12.5.0, not sampled. + +That is harmless for the RC_STATIC default, but it makes an RC_SHARED +build self-inconsistent: rc_export.h defines RC_EXPORT as +__attribute__((visibility("default"))) precisely so a shared build can +be compiled -fvisibility=hidden and export its documented API and +nothing else. Built that way, the nine rc_buffer_*/rc_djb2/ +rc_format_md5 entry points declared in this public header become +local symbols and cannot be linked against, so the header promises an +API the .so does not provide. + +Annotating the declaration alone is sufficient and is upstream's own +pattern: rc_version.h declares "RC_EXPORT uint32_t rc_version(void)" +while src/rc_version.c defines a bare "uint32_t rc_version(void)". +GCC propagates the visibility attribute from the declaration, so no +.c file needs touching and no calling convention mismatch is +introduced on any target. + +Provenance +---------- +Origin: Not upstream. Written for this repo against + RetroAchievements/rcheevos v12.5.0. +Author: Michael C. Ferguson +Upstream: NO -- not submitted yet. This is written to be sent as-is: + it is a nine-line consistency fix in upstream's own idiom, + with no behaviour change for the RC_STATIC default that + every current integrator uses, so it should be an easy take. + It has not been sent because this repo does not open + upstream pull requests without the owner asking first. + Drop this patch when upstream carries the annotations. +Disposition: LOAD-BEARING for this package, not cosmetic. The package + builds -DRC_SHARED -fvisibility=hidden (see rcheevos.mk for + why that pairing is the point). Without this patch the nine + entry points rc_util.h declares are compiled as local + symbols, so the installed header declares an API that + cannot be linked -- an undefined-reference at the + consumer's link step, not a build failure here. +Behaviour: No behaviour change on any target, and none for a static + consumer. RC_EXPORT under RC_STATIC and RC_IMPORT is + already visibility("default") -- the same value these + declarations get implicitly today -- so only an RC_SHARED + build compiled -fvisibility=hidden sees any difference. + RC_CCONV is empty on every target except Windows and i386. +Verified: Built for arm-buildroot-linux-gnueabihf and checked with + nm -D: all nine symbols move from local to exported (264 + exported vs 255 without the patch), and a consumer calling + rc_buffer_init/rc_djb2/rc_format_md5 links and runs. + +Signed-off-by: Michael C. Ferguson +--- +diff --git a/include/rc_util.h b/include/rc_util.h +index 18d27e0..6d28f08 100644 +--- a/include/rc_util.h ++++ b/include/rc_util.h +@@ -34,17 +34,17 @@ typedef struct rc_buffer_t { + } + rc_buffer_t; + +-void rc_buffer_init(rc_buffer_t* buffer); +-void rc_buffer_destroy(rc_buffer_t* buffer); +-uint8_t* rc_buffer_reserve(rc_buffer_t* buffer, size_t amount); +-void rc_buffer_consume(rc_buffer_t* buffer, const uint8_t* start, uint8_t* end); +-void* rc_buffer_alloc(rc_buffer_t* buffer, size_t amount); +-char* rc_buffer_strcpy(rc_buffer_t* buffer, const char* src); +-char* rc_buffer_strncpy(rc_buffer_t* buffer, const char* src, size_t len); ++RC_EXPORT void RC_CCONV rc_buffer_init(rc_buffer_t* buffer); ++RC_EXPORT void RC_CCONV rc_buffer_destroy(rc_buffer_t* buffer); ++RC_EXPORT uint8_t* RC_CCONV rc_buffer_reserve(rc_buffer_t* buffer, size_t amount); ++RC_EXPORT void RC_CCONV rc_buffer_consume(rc_buffer_t* buffer, const uint8_t* start, uint8_t* end); ++RC_EXPORT void* RC_CCONV rc_buffer_alloc(rc_buffer_t* buffer, size_t amount); ++RC_EXPORT char* RC_CCONV rc_buffer_strcpy(rc_buffer_t* buffer, const char* src); ++RC_EXPORT char* RC_CCONV rc_buffer_strncpy(rc_buffer_t* buffer, const char* src, size_t len); + +-uint32_t rc_djb2(const char* input); ++RC_EXPORT uint32_t RC_CCONV rc_djb2(const char* input); + +-void rc_format_md5(char checksum[33], const uint8_t digest[16]); ++RC_EXPORT void RC_CCONV rc_format_md5(char checksum[33], const uint8_t digest[16]); + + RC_END_C_DECLS + +-- +2.51.0 diff --git a/package/rcheevos/Config.in b/package/rcheevos/Config.in new file mode 100644 index 0000000..ab4c7ad --- /dev/null +++ b/package/rcheevos/Config.in @@ -0,0 +1,38 @@ +config BR2_PACKAGE_RCHEEVOS + bool "rcheevos" + depends on BR2_TOOLCHAIN_HAS_THREADS + help + rcheevos: RetroAchievements' own client library. Four pieces + in one .so -- rcheevos (evaluating achievement, leaderboard + and rich-presence logic against emulated memory), rapi (the + RetroAchievements web API request/response marshalling), + rhash (game identification hashing, including disc images), + and rc_client, the high-level state machine upstream + recommends integrating against. + + It does NOT perform network I/O: the consumer is handed a URL + to fetch and passes the response body back in. Nothing on this + image links it yet -- it is shipped so a consumer can. + + Built as a shared library (librcheevos.so.) + with upstream's own -DRC_SHARED plus -fvisibility=hidden, so + ONLY the RC_EXPORT-annotated public API is exported -- 264 + symbols rather than 477. That matters here: the hidden 213 + include md5_init/md5_append/md5_finish and six AES_* entry + points, which would otherwise interpose against a consumer's + own md5 and AES code. Patch 0001 annotates rc_util.h, the one + public header upstream left un-annotated. + + The SONAME is the FULL version, deliberately -- upstream gives + no ABI guarantee and its caller-allocated public structs have + changed layout in PATCH releases (v10.7.1 grew rc_runtime_t, + v10.3.3 grew rc_api_login_response_t mid-struct, v6.0.1 + changed RC_ALIGNMENT). Since the Main_MiSTer binary lives on + /media/fat and survives rootfs reflashes, a bump has to be a + loud refuse-to-load rather than silent corruption. See + package/rcheevos/rcheevos.mk. + + https://github.com/RetroAchievements/rcheevos + +comment "rcheevos needs a toolchain w/ threads" + depends on !BR2_TOOLCHAIN_HAS_THREADS diff --git a/package/rcheevos/rcheevos.hash b/package/rcheevos/rcheevos.hash new file mode 100644 index 0000000..a7c513e --- /dev/null +++ b/package/rcheevos/rcheevos.hash @@ -0,0 +1,29 @@ +# Locally computed (GitHub does not publish signed hashes for tag-archive +# tarballs -- standard practice for git/github-sourced Buildroot packages, +# per pkg-download.mk / the Buildroot manual §18.5.2). Verified at pin time +# against: +# https://github.com/RetroAchievements/rcheevos/archive/$(RCHEEVOS_VERSION)/rcheevos-$(RCHEEVOS_VERSION).tar.gz +# +# sha256sum rcheevos-$(RCHEEVOS_VERSION).tar.gz +# +# Written with the .mk variable rather than a literal tag on purpose: +# renovate-hash-sync.yml rewrites the sha256 DATA line below and can never +# touch a comment, so a literal here goes stale on the first automatic bump. +# See package/libchdr/libchdr.hash, whose comment had gone two pins stale +# exactly this way before it was rewritten in the same style. +sha256 e6df83de4e18f0a19206e711e1e589624dcfcf6bf529c14f6dd2bb2d1ced983f rcheevos-v12.5.0.tar.gz +# THE THREE LICENCE FILES. Only the FIRST sha256 line above (the tarball) is +# ever auto-refreshed -- scripts/hash-sync-github-packages.sh rewrites that +# line and leaves every later one alone, by design -- so if any of the three +# below fails the build, upstream changed a licence text and that is a human +# decision, not a hash to regenerate. +# +# LICENSE is the project's MIT text. The other two are the grants for the +# vendored third-party code this package compiles into the shipped .so, and +# they are hash-checked for that reason; see the RCHEEVOS_LICENSE comment in +# the .mk for why MIT alone would have been wrong. +sha256 bf820c034966a3b6be050bb85f94ec841a0b972bd5d477e362337d0574b5c219 LICENSE +# src/rhash/md5.h -- L. Peter Deutsch's md5, under the zlib licence verbatim +sha256 8341e2a307d7cfaeb816b261d92ce87a07be62e3eb58c98d2c769e75a6e2b959 src/rhash/md5.h +# src/rhash/aes.h -- kokke/tiny-AES-c @ f06ac37, under the Unlicense +sha256 cf769a50604be6d7a8d940675131c44ce834356ddf090419ae68abb2996b1f68 src/rhash/aes.h diff --git a/package/rcheevos/rcheevos.mk b/package/rcheevos/rcheevos.mk new file mode 100644 index 0000000..d2d5bcd --- /dev/null +++ b/package/rcheevos/rcheevos.mk @@ -0,0 +1,337 @@ +################################################################################ +# +# rcheevos +# +################################################################################ + +# RetroAchievements' own client library: achievement/leaderboard evaluation +# against emulated memory (rcheevos), the RetroAchievements web API request +# and response marshalling (rapi), game identification hashing (rhash), and +# rc_client, the high-level state machine upstream recommends integrating +# against. Built here as a target SHARED library, librcheevos. +# +# Upstream Buildroot has NO rcheevos package -- checked against the ACTUAL +# pinned tree (2026.08; Makefile's BUILDROOT_VERSION and work/buildroot's own +# BR2_VERSION), whose package/ has no rcheevos entry and no BR2_PACKAGE_ +# RCHEEVOS symbol, so this authors one. No RCHEEVOS_ Make namespace collision +# either. +# +# NO CONSUMER SHIPS TODAY, and that is not an oversight worth hiding: neither +# Main_MiSTer nor this tree references rcheevos yet (git grep -i rcheevos over +# both is empty). So unlike package/libchdr and package/lzma-sdk -- the other +# two members of the "Main_MiSTer shared libraries" menu, which exist to +# REPLACE code Main already vendors -- this package adds a NEW capability +# rather than unvendoring an existing one. It is enabled in the defconfig +# anyway (273 KiB as shipped -- 279,580 bytes stripped in rootfs.tar; the +# 330 KiB staging copy is the unstripped one) so the +# .so and its headers are simply present for a consumer to link, the same way +# a distro ships a library ahead of its dependents. + +# TAG PIN, and the leading "v" is kept IN the version string rather than +# reconstructed as "v$(RCHEEVOS_VERSION)" in the SITE line. Same reason as +# package/dualsensectl and package/munt: scripts/hash-sync-github-packages.sh's +# generic loop takes the literal RHS of the first *_VERSION line and uses it +# BOTH as the archive ref and as the "-.tar.gz" filename it +# rewrites in the .hash. Splitting the "v" off would make it fetch +# .../archive/12.5.0.tar.gz -- a 404, there is no bare "12.5.0" ref -- and +# write a filename Buildroot never asks for. +# +# A TAG, not a branch-head commit (the shape package/libchdr needs): upstream +# tags every release, and at v12.5.0 (2026-09-13) the tag, origin/master and +# origin/develop are all the same commit, 1433173220a7eaede6a9ed7a18e94117be +# 1821e0. Upstream's own README says to integrate against master, "which +# corresponds to the last official release", not develop. +RCHEEVOS_VERSION = v12.5.0 +RCHEEVOS_SITE = $(call github,RetroAchievements,rcheevos,$(RCHEEVOS_VERSION)) + +# The bare version, for the SONAME, the .pc Version field and the installed +# filename -- none of which want a "v". Derived from RCHEEVOS_VERSION rather +# than written out a second time, so a Renovate bump that rewrites the pin +# above cannot leave a stale literal behind. +RCHEEVOS_SOVERSION = $(patsubst v%,%,$(RCHEEVOS_VERSION)) + +# LICENSE is the standard MIT text ("Copyright (c) 2018 RetroAchievements.org", +# the permission grant, the "above copyright notice" condition, the all-caps +# disclaimer) -- checked by reading the file, not inferred from the README. +# +# BUT MIT ALONE IS WRONG, and an earlier revision of this file said it. There +# is no deps/ or third_party/ directory, so the tree LOOKS like it is all +# upstream's own code, and it very nearly is -- but two of the files this +# package compiles into the shipped .so are vendored third-party +# implementations carrying their own grants, both found by reading their +# headers rather than by trusting the layout: +# +# src/rhash/md5.c + md5.h L. Peter Deutsch / Aladdin Enterprises, 1999-2002. +# The notice is the ZLIB LICENSE word for word (the +# three numbered restrictions: origin must not be +# misrepresented, altered versions plainly marked, +# notice may not be removed). Upstream Buildroot +# spells this exact file's licence "Zlib (md5)" in +# package/rtty -- same lpd md5, same call. +# src/rhash/aes.c + aes.h kokke/tiny-AES-c at f06ac37, "with unused code +# excised", released under the Unlicense. +# +# Both are genuinely SHIPPED, not merely present: md5 is called from 12 files +# across rhash, rapi and the runtime, and aes from hash_encrypted.c. So they +# are named here rather than silently subsumed under the project's MIT -- +# exactly the call package/libchdr makes for its bundled dr_flac. +# +# LICENSE_FILES LISTS THE TWO HEADERS, NOT THE TWO .c FILES, on purpose. The +# full grant appears in both members of each pair, so either would serve as +# the legal text; the headers are simply the more stable half, and these are +# hash-checked at every build. Measured over upstream's whole history: +# md5.h has ONE commit (2020-01-04) and aes.h ONE (2024-01-16), against two +# and one for the .c files. This is the package/lzma-sdk reasoning applied to +# a lower-churn file: if the operative grant is not in LICENSE_FILES, then +# `make legal-info` emits a LICENSE claiming MIT while the image carries +# zlib- and Unlicense-covered object code whose grants travel nowhere. That +# these hashes will also trip on an unrelated upstream edit is the cost of +# the tripwire working at all -- and at one commit each in eight years, it is +# a cost this package is unlikely to ever pay. +RCHEEVOS_LICENSE = MIT, Zlib (md5), Unlicense (tiny-AES-c) +RCHEEVOS_LICENSE_FILES = LICENSE src/rhash/md5.h src/rhash/aes.h + +# Staging install: a consumer compiles against et al. and links +# -lrcheevos from the sysroot, so headers + .so must land in staging, not just +# the target. +RCHEEVOS_INSTALL_STAGING = YES + +# NO DEPENDENCIES, deliberately stated rather than left blank. Verified by +# collecting every system #include across src/ and include/: the only ones +# that survive the platform #ifdefs on this target are libc headers plus +# and . In particular there is NO zlib (rhash's +# hash_zip.c walks the zip central directory, it never inflates), NO openssl +# or nettle (md5 and aes are compiled in from src/rhash/), and NO Lua -- +# upstream removed the Lua evaluator entirely; the "unused_L" parameters +# still in rc_evaluate_trigger() and friends are vestigial ABI, not a +# dependency. The remaining non-libc includes (, , +# <3ds/synchronization.h>, ) belong to platforms this build is +# not, or to the one source file excluded below. +# +# pthread is NOT optional on Linux: rc_compat.h's platform ladder falls +# through to the plain-POSIX #else branch (#include , typedef +# pthread_mutex_t rc_mutex_t), so rc_mutex_* is always the pthread +# implementation here. Hence the Config.in "depends on +# BR2_TOOLCHAIN_HAS_THREADS" and the -pthread below. + +# The SDK-shaped problem again (see package/lzma-sdk): upstream ships NO +# build system this package can drive. The tree has no CMakeLists.txt and no +# top-level Makefile -- only test/Makefile, which builds the MinGW/x86 unit +# test binary and hard-errors on any ARCH it cannot detect, and Package.swift, +# which is for SwiftPM consumers. So this is a generic-package with a single +# direct $(TARGET_CC) invocation, exactly like package/lzma-sdk (further +# precedents: package/mongoose compiles with direct $(TARGET_CC); +# package/sunxi-cedarx links -shared -Wl,-soname the same way). +# +# THE SOURCE LIST IS A WILDCARD MINUS THREE FILES, not a hand-written +# enumeration, so an upstream release that adds a .c to one of the four +# directories below is picked up by the next Renovate bump instead of being +# silently dropped. If such a file ever needs a define or a header this build +# does not pass, -Wl,--no-undefined below turns that into a LOUD build failure +# on the bump PR -- which is the intended outcome, not a regression. +# +# THE LIMIT OF THAT, stated rather than left to be discovered: the four +# patterns are not recursive, so a brand-new SUBDIRECTORY (an upstream +# "src/rnet/", say) would be skipped, and silently unless something already +# compiled references it. $(wildcard) was still preferred over a recursive +# $(shell find ...) because find's output order is filesystem-dependent while +# $(wildcard) globs sorted -- and link order feeds straight into +# reproducibility.yml. Upstream's four-directory layout has been stable for +# the whole 10.x-12.x range this pin has lived in; if that changes, add the +# pattern here. +# +# The three exclusions, each checked in the v12.5.0 tree rather than copied +# from Package.swift's exclude list: +# +# rc_libretro.c #include , which only the libretro +# frontends provide (upstream's own test/ dir +# ships a private copy to compile it). It is +# frontend glue for RetroArch/RALibretro -- core +# option parsing, memory-map validation -- with +# nothing a MiSTer consumer would call. +# rc_client_external.c Entire body is inside #ifdef RC_CLIENT_SUPPORTS_ +# EXTERNAL; without that define the file compiles +# to an empty translation unit. That define opts +# into the versioned rc_client_external ABI used +# to talk to a SEPARATE rcheevos implementation +# (see src/rc_client_external_versions.h), which +# is the opposite of what shipping this .so does. +# rc_client_raintegration.c Entire body is inside #ifdef RC_CLIENT_SUPPORTS_ +# RAINTEGRATION (the #endif is the last line of +# the file); RAIntegration is the Windows-only +# achievement-development DLL, and its public +# header #undefs the feature outright on +# !defined(_WIN32). +# +# The matching header, rc_client_raintegration.h, IS still installed below: +# it self-disables on non-Windows at line 4, leaving only type definitions and +# no function declarations, so it cannot promise an entry point this .so does +# not carry. +RCHEEVOS_SKIP_SRCS = \ + rc_libretro.c \ + rc_client_external.c \ + rc_client_raintegration.c + +# -DRC_SHARED + -fvisibility=hidden IS THE POINT OF THIS PACKAGE, and the two +# must be passed together. include/rc_export.h defines RC_EXPORT as +# __attribute__((visibility("default"))) under RC_SHARED precisely so a shared +# build can hide everything else; passing RC_SHARED without -fvisibility=hidden +# marks the public API default in a build where everything is already default, +# i.e. does nothing. Measured on this toolchain at v12.5.0, with patch 0001 +# applied: 264 exported symbols with the pair, 477 without. (255 with the +# pair but without patch 0001 -- the nine it restores are exactly the +# difference.) +# +# The 213 symbols that difference suppresses are not cosmetic. Among them are +# md5_init, md5_append and md5_finish -- the RSA-derived md5 API whose names +# Main_MiSTer's own vendored lib/md5 also defines -- plus AES_init_ctx, +# AES_CTR_xcrypt_buffer and four more AES_* entry points, and a global +# literally named "g_host". ELF symbol interposition is first-definition-wins +# across the whole link, so exporting those from a library loaded into the +# same process as its own md5 implementation is a silent wrong-function bind, +# not a link error. This is the same property package/libchdr gets from +# upstream's version script (src/link.T, "global: chd_*; local: *") and which +# its Config.in calls out; rcheevos has no version script, so visibility is +# how it is obtained here. +# +# Patch 0001 is what makes the pair safe to use: rc_util.h is the only public +# header upstream left un-annotated, so without it the nine rc_buffer_*/ +# rc_djb2/rc_format_md5 entry points it declares would be hidden and the +# installed headers would promise an API the .so does not export. See that +# patch's own header; it is written to be upstreamable and should be dropped +# if upstream takes it. +# +# -DRC_CLIENT_SUPPORTS_HASH enables rhash inside rc_client -- the +# rc_client_begin_identify_and_load_game() path, i.e. "given a file, work out +# which game this is". It GATES PUBLIC HEADER DECLARATIONS, not just +# implementation (include/rc_client.h lines 265 and 333), so it must also be +# in the .pc's Cflags or a consumer's rc_client.h would hide functions the .so +# exports. It is in both; rcheevos.pc carries it deliberately. +# +# _LARGEFILE64_SOURCE IS REQUIRED HERE BUT IS DELIBERATELY NOT PASSED. It +# selects the fseeko64/ftello64 branch of rhash's filereader +# (src/rhash/hash.c:187,198); the #else branch is plain fseek/ftell, whose +# long offset is 32 bits on this ARM target and would cap disc images at +# 2 GiB. Buildroot already supplies it -- package/Makefile.in line 202 adds +# "-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64" to +# TARGET_CPPFLAGS unconditionally (no BR2_ guard), and TARGET_CFLAGS begins +# with TARGET_CPPFLAGS -- so repeating it would be exactly the +# infra-provided-option duplication package/libchdr's .mk refuses for +# -DBUILD_SHARED_LIBS. Verified effective rather than assumed, twice: the +# defines appear on the compile line above, and the built .so imports +# fseeko64@GLIBC_2.4 / ftello64@GLIBC_2.4. It is ABI-neutral for consumers +# (no off_t appears in any installed header), so unlike +# RC_CLIENT_SUPPORTS_HASH it also has no business in the .pc. +# +# -pthread is belt-and-braces. On this glibc it is a no-op for linking -- +# 2.34 merged libpthread into libc, and the .so's pthread_mutex* imports +# resolve from libc alone -- but it is the portable spelling and costs +# nothing. -lm is NOT optional: rc_typed_value_modulus() calls fmod(), and +# -Wl,--no-undefined caught its absence as a hard link error while this +# package was being written, which is exactly what that flag is for. +# +# SONAME POLICY -- THE FULL VERSION (librcheevos.so.12.5.0), matching +# package/lzma-sdk's deliberate loud-ABI-event policy, and here it is backed +# by upstream's own history rather than by analogy. rcheevos publishes no ABI +# guarantee, and its public headers are full of caller-allocated structs +# (rc_runtime_t is declared complete in rc_runtime.h and initialised in place +# by rc_runtime_init(); rc_api_*_response_t likewise). Three separate PATCH +# releases changed those layouts: +# +# v10.7.1 added "char owns_self;" to the public rc_runtime_t -- a field +# still present at v12.5.0 as uint8_t owns_self. +# v10.3.3 inserted "const char* display_name;" into the MIDDLE of +# rc_api_login_response_t, shifting every field after it. +# v6.0.1 redefined RC_ALIGNMENT from 8 to sizeof(void*), which on a +# 32-bit target like this one changes padding library-wide. +# +# (Minor releases do it too -- v12.4.0 added avatar_last_updated to +# rc_client_user_t -- so a major.minor SONAME would not have caught the three +# above either. Derived by diffing include/ across all 55 tags, not sampled.) +# +# A stale consumer meeting a changed layout is memory corruption, not an +# error, and on this image that is the EXPECTED failure mode rather than a +# theoretical one: the Main_MiSTer binary lives on the persistent /media/fat +# partition and SURVIVES rootfs reflashes, so linux.img and the binary that +# links this library are routinely out of sync. Making the full version the +# SONAME turns every bump into a clean refuse-to-load. The cost is honest and +# accepted: a pure-bugfix patch release also forces a consumer rebuild. +define RCHEEVOS_BUILD_CMDS + $(TARGET_CC) $(TARGET_CFLAGS) -fPIC -fvisibility=hidden \ + -DRC_SHARED -DRC_CLIENT_SUPPORTS_HASH \ + -I$(@D)/include -I$(@D)/src \ + $(filter-out $(addprefix $(@D)/src/,$(RCHEEVOS_SKIP_SRCS)), \ + $(wildcard $(@D)/src/*.c $(@D)/src/rcheevos/*.c \ + $(@D)/src/rapi/*.c $(@D)/src/rhash/*.c)) \ + -shared -Wl,-soname,librcheevos.so.$(RCHEEVOS_SOVERSION) \ + -Wl,--no-undefined $(TARGET_LDFLAGS) -pthread -lm \ + -o $(@D)/librcheevos.so.$(RCHEEVOS_SOVERSION) +endef + +# Headers go in a NAMESPACED /usr/include/rcheevos/ dir. rc_*.h is a +# distinctive enough prefix that flat /usr/include would probably survive, but +# rc_hash.h, rc_error.h and rc_util.h are exactly the names another project +# would also pick, and both existing members of this menu already namespace +# (include/libchdr/, include/lzma-sdk/). Safe because every intra-header +# include is same-directory quoted ("rc_export.h", "rc_runtime.h", ... -- +# checked across all 15 installed headers), so they resolve inside the +# namespaced dir with no path edits. rcheevos.pc's Cflags points -I at the +# subdirectory itself, so a consumer writes #include exactly as +# upstream's wiki does; libchdr.pc does the same. +# +# INSTALLED WITH A WILDCARD, for the same reason the source list is one: a +# release that adds a public header should not need this line edited. +# module.modulemap is excluded by the *.h glob rather than by name -- it is +# SwiftPM metadata pointing at include/rcheevos.h and means nothing to a C +# consumer. +# +# src/rc_version.h IS INSTALLED TOO, named explicitly because it is the one +# public header upstream did not put in include/. It declares rc_version() +# and rc_version_string() -- both RC_EXPORT-annotated, both compiled in from +# src/rc_version.c, and so both genuinely exported by this .so. Leaving the +# header behind would export two entry points no consumer could declare; +# caught exactly that way, by a consumer smoke test failing with "implicit +# declaration of function 'rc_version_string'". It is safe to install: it is +# self-contained (#include "rc_export.h" + , both resolving inside +# the namespaced dir) and its RCHEEVOS_VERSION_* macros are what a consumer +# needs for a compile-time version check -- which matters more here than +# usual, given the full-version SONAME policy above. +# +# It is the ONLY such header: src/ carries exactly two RC_EXPORT-annotated +# headers (checked mechanically, not sampled), and the other is +# src/rc_libretro.h, whose implementation this package deliberately does not +# compile -- so installing that one WOULD promise entry points the .so lacks. +# Not installed, correctly. +# +# The unversioned librcheevos.so dev symlink is STAGING-ONLY -- the target +# gets just the versioned file, whose filename IS the SONAME the runtime +# linker looks up. Same deliberate choice, and same note, as package/lzma-sdk: +# this is stricter than Buildroot's infra-installed packages (cmake/autotools +# targets keep their unversioned .so on the target, since target-finalize +# prunes headers/.pc/.a but not symlinks), and nothing links at runtime +# through the unversioned name, so omitting it there loses nothing. +# +# rcheevos.pc is shipped in this package dir and installed with the Version +# line substituted from RCHEEVOS_SOVERSION, so the .mk stays the single source +# of truth for the version (pc-in-package-dir precedents: package/lzma-sdk +# ships lzma-sdk.pc, upstream package/libmad ships mad.pc). +define RCHEEVOS_INSTALL_STAGING_CMDS + $(INSTALL) -D -m 0755 $(@D)/librcheevos.so.$(RCHEEVOS_SOVERSION) \ + $(STAGING_DIR)/usr/lib/librcheevos.so.$(RCHEEVOS_SOVERSION) + ln -sf librcheevos.so.$(RCHEEVOS_SOVERSION) \ + $(STAGING_DIR)/usr/lib/librcheevos.so + mkdir -p $(STAGING_DIR)/usr/include/rcheevos + $(INSTALL) -m 0644 $(wildcard $(@D)/include/*.h) $(@D)/src/rc_version.h \ + $(STAGING_DIR)/usr/include/rcheevos + $(INSTALL) -D -m 0644 $(RCHEEVOS_PKGDIR)/rcheevos.pc \ + $(STAGING_DIR)/usr/lib/pkgconfig/rcheevos.pc + $(SED) 's/@RCHEEVOS_VERSION@/$(RCHEEVOS_SOVERSION)/' \ + $(STAGING_DIR)/usr/lib/pkgconfig/rcheevos.pc +endef + +define RCHEEVOS_INSTALL_TARGET_CMDS + $(INSTALL) -D -m 0755 $(@D)/librcheevos.so.$(RCHEEVOS_SOVERSION) \ + $(TARGET_DIR)/usr/lib/librcheevos.so.$(RCHEEVOS_SOVERSION) +endef + +$(eval $(generic-package)) diff --git a/package/rcheevos/rcheevos.pc b/package/rcheevos/rcheevos.pc new file mode 100644 index 0000000..c361f59 --- /dev/null +++ b/package/rcheevos/rcheevos.pc @@ -0,0 +1,12 @@ +prefix=/usr +exec_prefix=${prefix} +libdir=${exec_prefix}/lib +includedir=${prefix}/include + +Name: rcheevos +Description: RetroAchievements client library (achievements, leaderboards, rich presence, game identification) +URL: https://github.com/RetroAchievements/rcheevos +Version: @RCHEEVOS_VERSION@ +Libs: -L${libdir} -lrcheevos +Libs.private: -lm -pthread +Cflags: -I${includedir}/rcheevos -DRC_IMPORT -DRC_CLIENT_SUPPORTS_HASH diff --git a/renovate.json b/renovate.json index cc53e4b..737acb3 100644 --- a/renovate.json +++ b/renovate.json @@ -88,6 +88,27 @@ "false positives -- the literal string appears inside the descriptions that", "warn about it, this one included. Read the hits, do not just count them.", "", + "RE-MEASURED 2026-09-16, when the rcheevos manager was added (19 custom", + "managers). A real `--platform=local --dry-run=extract` under renovate", + "44.x reports regex: {fileCount: 20, depCount: 20}. THIS VERSION DOES NOT", + "DOUBLE -- the 2026-09-10 note above recorded 36 = 2 x 18 because that", + "renovate extracted twice in local dry-run mode and accumulated; 20 here is", + "a single pass, so 20 and 36 are NOT comparable and the drop is not a", + "regression. Verified by counting the packageFile lines directly rather", + "than trusting the summary, which is the method the note above prescribes:", + "13 package/*.mk + Makefile + mister_de10nano_defconfig TWICE (the 6.18 and", + "RT managers share it) + mister_de25nano_defconfig + fetch-sdcard-payload.sh", + "THREE TIMES = 20 pairs, one dep each. Note this contradicts the", + "'the 6.18 manager matches ONE file' line further up: a kernel manager also", + "matches the DE25 defconfig. That predates this change and is left as found,", + "flagged rather than silently corrected.", + "", + "ONE MORE TRAP, met while adding rcheevos and worth writing down: four lines", + "in the debug log match 'Dependency extraction complete', and only one is a", + "real extraction. The other three are this very description block being", + "echoed back -- it quotes the phrase. Grep for the stats object, not the", + "sentence.", + "", "automerge is OFF everywhere, deliberately: every pin here is a supply-chain", "trust decision (kernel, Buildroot, out-of-tree driver commits, firmware), and", "several need a companion hash refreshed by .github/workflows/renovate-hash-sync.yml", @@ -291,6 +312,19 @@ "packageNameTemplate": "https://github.com/rtissera/libchdr", "datasourceTemplate": "git-refs" }, + { + "customType": "regex", + "description": "rcheevos tag pin (RetroAchievements/rcheevos) -- the RetroAchievements client library, shipped as librcheevos.so for Main_MiSTer to link. A TAG, not a branch-head commit like libchdr above: upstream tags every release and its README says to integrate against master, which IS the newest tag. The leading \"v\" is deliberately INSIDE the captured currentValue rather than reconstructed in the .mk's SITE line -- same reason as dualsensectl below: scripts/hash-sync-github-packages.sh takes the literal RHS of the first *_VERSION line and uses it both as the archive ref and as the -.tar.gz filename it writes into the .hash, so the two must agree character for character, and Renovate rewrites the captured group in place and therefore preserves the prefix. `loose` rather than `semver` for the same reason dualsensectl uses it: the captured value carries the \"v\", and this tree already standardises on loose for every v-in-place pin. READ THE BUMP DIFF FOR PUBLIC STRUCT CHANGES: rcheevos publishes no ABI guarantee and has changed caller-allocated public struct layouts in PATCH releases (v10.7.1, v10.3.3, v6.0.1 -- see package/rcheevos/rcheevos.mk). That is survivable only because the SONAME is the full version, so every bump renames the .so and a stale consumer refuses to load instead of corrupting memory; it also means every bump is an intentional rebuild-your-consumer event. Hash: package/rcheevos/rcheevos.hash, refreshed by renovate-hash-sync.yml's generic loop (standard $(call github,...) archive tarball).", + "managerFilePatterns": [ + "/^package/rcheevos/rcheevos\\.mk$/" + ], + "matchStrings": [ + "RCHEEVOS_VERSION\\s*=\\s*(?v[\\d.]+)" + ], + "depNameTemplate": "RetroAchievements/rcheevos", + "datasourceTemplate": "github-tags", + "versioningTemplate": "loose" + }, { "customType": "regex", "description": "7-Zip LZMA SDK tag pin (ip7z/7zip) for the Main_MiSTer shared-lib refactor. Only the LZMA_SDK_VERSION line needs managing -- LZMA_SDK_SOURCE derives from it via $(subst) in the .mk. Hash: package/lzma-sdk/lzma-sdk.hash, refreshed by renovate-hash-sync.yml's BESPOKE ip7z/7zip step (scripts/hash-sync-ip7z-src.sh -- release-asset URL with a dots-stripped filename, does not fit the generic $(call github,...) loop). That step is table-driven over BOTH packages that pin this asset: this one and package/7zip (ADR 0023).", @@ -433,13 +467,14 @@ ] }, { - "description": "Main_MiSTer shared-library pins (libchdr commit, LZMA SDK tag) -- distinct label per the established per-pin-class convention, so a reviewer knows these change a userspace library ABI Main_MiSTer links against (liblzma-sdk's SONAME is the full version, so an SDK bump IS an ABI event by design), not kernel-module source or a build tool. NOTE (ADR 0023): matchDepNames cannot separate the two ip7z/7zip consumers, because package/lzma-sdk and package/7zip deliberately share that depName so one PR bumps both. So an ip7z/7zip PR carrying this label changes BOTH the liblzma-sdk ABI AND the 7zz archiver / the /media/fat/linux/7za updater binary -- review it as both.", + "description": "Main_MiSTer shared-library pins (libchdr commit, LZMA SDK tag, rcheevos tag) -- distinct label per the established per-pin-class convention, so a reviewer knows these change a userspace library ABI Main_MiSTer links against (liblzma-sdk's SONAME is the full version, so an SDK bump IS an ABI event by design), not kernel-module source or a build tool. NOTE (ADR 0023): matchDepNames cannot separate the two ip7z/7zip consumers, because package/lzma-sdk and package/7zip deliberately share that depName so one PR bumps both. So an ip7z/7zip PR carrying this label changes BOTH the liblzma-sdk ABI AND the 7zz archiver / the /media/fat/linux/7za updater binary -- review it as both. rcheevos joined this label on 2026-09-16: its SONAME is also the full version, for a harder reason than lzma-sdk's (upstream has changed public struct layouts in PATCH releases), so the same \"a bump IS an ABI event\" reviewer contract applies -- with the caveat that nothing links it yet, so today the blast radius of a bad bump is a build failure rather than a runtime one.", "matchManagers": [ "custom.regex" ], "matchDepNames": [ "rtissera/libchdr", - "ip7z/7zip" + "ip7z/7zip", + "RetroAchievements/rcheevos" ], "addLabels": [ "lib-pin" diff --git a/scripts/ci-tests.sh b/scripts/ci-tests.sh index 6c0ed07..d2df288 100755 --- a/scripts/ci-tests.sh +++ b/scripts/ci-tests.sh @@ -1896,6 +1896,12 @@ section "Main_MiSTer shared libraries" # upstream Buildroot (defconfig), lzma-sdk + libchdr from this tree's # package/. See docs/main-shared-libs.md. # +# librcheevos is the SIXTH entry and is NOT one of those five: it replaces no +# vendored code and nothing links it yet (see package/rcheevos/rcheevos.mk). +# It is asserted here anyway, and for a sharper reason than the others -- a +# library with no consumer has NO other way to fail visibly. If it silently +# stopped being installed, every other check in this suite would still pass. +# # minizip and minizip-ng are ALTERNATIVES, not a pair: Main links the classic # libminizip.so.1 (zip.h/unzip.h API) today, while minizip-ng is staged for a # future native mz_zip.h port. Both are asserted because both are shipped -- @@ -1914,7 +1920,8 @@ for spec in \ "libminizip\.so\.1:libminizip.so.1* (minizip, classic)" \ "libminizip-ng\.so\.4:libminizip-ng.so.4* (minizip-ng)" \ "liblzma-sdk\.so\.:liblzma-sdk.so.* (lzma-sdk)" \ - "libchdr\.so\.0:libchdr.so.0* (libchdr)"; do + "libchdr\.so\.0:libchdr.so.0* (libchdr)" \ + "librcheevos\.so\.:librcheevos.so.* (rcheevos)"; do lib_re="^\\./usr/lib/${spec%%:*}" lib_name="${spec#*:}" if grep -qE "$lib_re" "$TAR_LIST"; then