diff --git a/scripts/check_release.py b/scripts/check_release.py index f30ed5cd9..8e16288fa 100644 --- a/scripts/check_release.py +++ b/scripts/check_release.py @@ -7,9 +7,9 @@ SHA256SUMS.txt lists for it, as must each handheld launcher (RetroBIOS.zip and the others build_port.py writes) the list names, the parts of a pack must add up to the download size the README prints, and nothing else may be attached. The record holds one pack for each platform the registry marks release_pack, and no other -pack. The installer (install.py, install.sh, install.ps1) and every file -list of install/ are listed and attached too, so a package can pin one -release. An upload that +pack. The installer and the file lists are not release files: a package pins +them from the tag, so a reader of the release page sees the packs and nothing +to puzzle over. An upload that failed in the loop, or a part re-uploaded from an older build, otherwise publishes a release that `sha256sum --check` rejects for every user. @@ -39,14 +39,6 @@ from release_record import RECORD, load_record SUMS = "SHA256SUMS.txt" SIGNATURE = SUMS + ".sig" -INSTALLER = ("install.py", "install.sh", "install.ps1") - - -def installer_files(install_dir: str | Path = "install") -> list[str]: - """The release files a package pins: the installer and each file list.""" - return [*INSTALLER, *sorted(p.name for p in Path(install_dir).glob("*.json"))] - - @dataclass(frozen=True) class Asset: size: int @@ -81,7 +73,6 @@ def compare_release( tag: str, local_files: dict[str, str] | None = None, released: set[str] | None = None, - installer: list[str] | None = None, ) -> list[Finding]: """Every way the release can differ from what was built. @@ -89,7 +80,6 @@ def compare_release( signature) to its local sha256: the copy on the release must be that one. *released* names the platforms the registry marks release_pack: each one has a recorded pack, and no recorded pack serves only other platforms. - *installer* names the installer files each release lists and carries. """ findings: list[Finding] = [] if record.get("tag") != tag: @@ -122,11 +112,9 @@ def compare_release( findings.append(Finding("extra", name, "attached but neither recorded nor listed")) for name in sorted(set(recorded) - set(sums)): findings.append(Finding("unlisted", name, f"absent from {SUMS}")) - for name in sorted(set(installer or ()) - set(sums)): - findings.append(Finding("no-installer", name, f"absent from {SUMS}")) # The handheld launchers are release files but no packs: release.json # records packs, and the checksum list alone vouches for the launchers. - for name in sorted(set(sums) - set(recorded) - set(ARCHIVES) - set(installer or ())): + for name in sorted(set(sums) - set(recorded) - set(ARCHIVES)): findings.append(Finding("unrecorded", name, f"listed in {SUMS}, absent from release.json")) wanted = {**sums, **(local_files or {})} @@ -202,10 +190,6 @@ def main() -> int: "--platforms-dir", default="platforms", help="registry whose release_pack platforms each need a pack (default: %(default)s)", ) - parser.add_argument( - "--install-dir", default="install", - help="file lists the release must carry beside the installer (default: %(default)s)", - ) parser.add_argument("--json", action="store_true", help="JSON output") args = parser.parse_args() require_directory(args.platforms_dir, "--platforms-dir") @@ -240,8 +224,7 @@ def main() -> int: return 2 released = set(list_release_pack_platforms(args.platforms_dir)) - findings = compare_release(record, sums, assets, tag, local_files, released, - installer_files(args.install_dir)) + findings = compare_release(record, sums, assets, tag, local_files, released) if args.json: print(json.dumps( {"tag": tag, "draft": draft, "assets": len(assets), diff --git a/scripts/release_notes.py b/scripts/release_notes.py index e795521ca..dff6f6f49 100644 --- a/scripts/release_notes.py +++ b/scripts/release_notes.py @@ -4,9 +4,10 @@ The notes carried the pack table, typed by hand from figures release.json already held: a copied cell or an old collection total at the top is how issue 81 happened. Install commands, the parts sentence and the table come -from the record and the registry. Each row links the platform's page under -get/ on the site, which outlives the release, and the setups the registry -does not mark release_pack are named with the command. The maintainer writes +from the record and the registry. Each row links the files of this release, +which sit right below the notes, and the setups the registry does not mark +release_pack are named with the command. The text is a beginner's page: one +idea per line, plain words (owner, 2026-11-10). The maintainer writes what changed, and that text is appended as given once it holds none of the REFUSED phrases. @@ -84,9 +85,8 @@ def no_pack_paragraph(platforms_dir: str) -> list[str]: return [] listed = ", ".join(names[:-1]) + f" and {names[-1]}" if len(names) > 1 else names[0] return [ - (f"No ZIP for {listed}. ZIPs are made for the setups most people download." - " The command above installs every file for these setups and checks each one." - " Add `--dry-run` to see what it would install."), + (f"No ZIP for {listed}. The command above installs them." + " Add `--dry-run` to see what it would do."), "", ] @@ -96,11 +96,13 @@ def _match_key(value: str) -> str: return re.sub(r"[^a-z0-9]", "", value.lower()) -def _link(name: str, pack: dict) -> str: - """The pack's permanent page, which lists the files of the current release.""" - parts = len(pack["assets"]) - label = "Download" if parts == 1 else f"Download, {parts} parts" - return f"[{label}]({release_record.get_page(_owner(name, pack), SITE)})" +def _link(pack: dict, tag: str) -> str: + """The pack's files on this release, one link per part.""" + base = f"https://github.com/{REPO}/releases/download/{tag}" + assets = pack["assets"] + if len(assets) == 1: + return f"[Download]({base}/{assets[0]})" + return ", ".join(f"[part {i}]({base}/{name})" for i, name in enumerate(assets, 1)) # Words the published notes of v2026.10.10 carried by hand: "never from a @@ -120,11 +122,11 @@ def refused_words(text: str) -> list[str]: def render(record: dict, platforms_dir: str, whats_new: str) -> str: packs = record.get("packs", {}) folders = _extract_folders(record, platforms_dir) + tag = str(record.get("tag", "")) lines = [ "### Install in one command", "", - ("The installer detects the platform and its BIOS folder, downloads only" - " what is missing and checks every file. Running it again is safe."), + "It finds your setup and its BIOS folder, then downloads only what is missing.", "", "**Linux, macOS, Steam Deck**", "```sh", @@ -136,35 +138,39 @@ def render(record: dict, platforms_dir: str, whats_new: str) -> str: install_command("powershell"), "```", "", - "### Download a pack", + (f"**On a handheld**: copy the `RetroBIOS` file for your system to the card" + f" ([which one]({SITE}wiki/installer/#on-a-handheld-the-retrobios-port))."), "", - ("One complete pack for each platform below: the BIOS list the platform" - " publishes plus the files its emulators open. Files no pack carries yet" - f" are listed in the [gap analysis]({SITE}gaps/). Each link is a page" - " that keeps its address from one release to the next. Files is what the pack" - " holds once extracted, the count a file manager shows. Extracted size" - " is the space those files take, and the download is smaller."), + "### Or download a pack", + "", + "Extract it into your BIOS folder.", "", ] if any(len(pack["assets"]) > 1 for pack in packs.values()): lines += [ - ("A pack over 2 GB comes as parts that are each an ordinary ZIP of whole" - " files (`Pack.part1of2.zip`, `Pack.part2of2.zip`). Download every part" - " and extract them all into the same folder. Nothing needs joining."), + ("A pack in parts: each part is an ordinary ZIP. Extract every part into" + " the same folder. Nothing needs joining."), "", ] lines += [ - "| Platform | Download | Extracted size | Files | Extract to |", - "|----------|----------|---------------:|------:|-----------|", + "| Platform | Download | Size | Files | Extract to |", + "|----------|----------|-----:|------:|-----------|", ] for name, pack in sorted(packs.items(), key=lambda item: -item[1]["extracted_size"]): folder = folders.get(name) lines.append( - f"| {_display(name)} | {_link(name, pack)} |" + f"| {_display(name)} | {_link(pack, tag)} |" f" {_size(pack['extracted_size'])} | {pack['files']:,} |" f" {f'`{folder}`' if folder else '-'} |" ) - lines += ["", *no_pack_paragraph(platforms_dir), whats_new.strip(), ""] + lines += [ + "", + f"Files no pack holds yet: [gap analysis]({SITE}gaps/).", + "", + *no_pack_paragraph(platforms_dir), + whats_new.strip(), + "", + ] return "\n".join(lines) diff --git a/tests/test_pack_counts.py b/tests/test_pack_counts.py index 05d04534d..bdb0ed3ed 100644 --- a/tests/test_pack_counts.py +++ b/tests/test_pack_counts.py @@ -594,10 +594,12 @@ class TheReleaseNotesReadTheRecord(unittest.TestCase): text = self.render() row = next(line for line in text.splitlines() if line.startswith("| RetroArch")) self.assertIn("8,504", row) - self.assertIn("[Download, 2 parts](https://abdess.github.io/retrobios/get/retroarch/)", row) - self.assertNotIn("/releases/download/", row) + base = "https://github.com/Abdess/retrobios/releases/download/v2026.10.10/" + self.assertIn(f"[part 1]({base}RetroArch_Lakka_v1.22.2_BIOS_Pack.part1of2.zip)", row) + self.assertIn(f"[part 2]({base}RetroArch_Lakka_v1.22.2_BIOS_Pack.part2of2.zip)", row) + self.assertNotIn("retrobios/get/", row) self.assertIn("`system/`", row) - self.assertIn("| Platform | Download | Extracted size | Files |", text) + self.assertIn("| Platform | Download | Size | Files |", text) def test_the_parts_sentence_sits_above_the_table_only_when_needed(self): text = self.render() diff --git a/tests/test_release_packs.py b/tests/test_release_packs.py index 3f23abb96..33d5e88bb 100644 --- a/tests/test_release_packs.py +++ b/tests/test_release_packs.py @@ -322,12 +322,16 @@ class TheReleaseNotesNameTheSetupsWithoutAZip(unittest.TestCase): self.assertIn("--dry-run", paragraph) self.assertLess(text.index(paragraph), text.index("### What changed")) - def test_the_rows_link_get_pages(self): + def test_the_rows_link_this_release_files(self): + """The files sit right below the notes: a row sends nobody to the + site to find them (owner, 2026-11-10).""" import release_notes text = release_notes.render(self.RECORD, PLATFORMS, "x") - self.assertIn(f"[Download]({SITE}get/romm/)", text) - self.assertNotIn("/releases/download/", text) + self.assertIn( + "[Download](https://github.com/Abdess/retrobios/releases/download/" + "v2026.10.24/RomM_5_BIOS_Pack.zip)", text) + self.assertNotIn(f"{SITE}get/", text) class EveryPlatformHasAPermanentPage(unittest.TestCase): diff --git a/tests/test_site_wiki.py b/tests/test_site_wiki.py index e584ef7b0..4bb1e8378 100644 --- a/tests/test_site_wiki.py +++ b/tests/test_site_wiki.py @@ -48,7 +48,7 @@ sys.path.insert(0, str(REPO_ROOT)) import sitediagrams # noqa: E402 from siteguides import INSTALL_SH # noqa: E402 from tests import sitepages # noqa: E402 -from check_release import INSTALLER, SUMS, Asset, compare_release, installer_files # noqa: E402 +from check_release import SUMS, Asset, compare_release # noqa: E402 WIKI = REPO_ROOT / "wiki" @@ -858,49 +858,27 @@ def _sums_with(names: list[str]) -> dict[str, str]: class PinnedInstaller(unittest.TestCase): - """J9, D5: release tags are deleted and the release carried no installer.""" + """The release page carries the packs a beginner picks from, and no + installer or file list: a package pins those from the tag (owner, + 2026-11-10, "dans release on met le minimum sans bruit").""" - def test_the_list_holds_the_installer_and_every_file_list(self): - names = installer_files(REPO_ROOT / "install") - self.assertEqual(names[:3], list(INSTALLER)) - self.assertIn("retroarch.json", names) - self.assertEqual(len(names), 3 + len(list((REPO_ROOT / "install").glob("*.json")))) - - def test_a_release_without_the_installer_is_refused(self): + def test_an_installer_attached_to_the_release_is_refused(self): record = {"tag": "v1", "packs": {"P.zip": {"download_size": 5, "assets": ["P.zip"]}}} - files = ["install.py", "install.sh", "install.ps1", "retroarch.json"] - sums = _sums_with(["P.zip", *files]) + sums = _sums_with(["P.zip", "install.py", "retroarch.json"]) assets = {name: Asset(5, digest) for name, digest in sums.items()} assets[SUMS] = Asset(1, None) - self.assertEqual(compare_release(record, sums, assets, "v1", installer=files), []) + kinds = {(f.kind, f.name) for f in compare_release(record, sums, assets, "v1")} + self.assertEqual(kinds, {("unrecorded", "install.py"), ("unrecorded", "retroarch.json")}) - del sums["install.py"] - del assets["install.py"] - kinds = {(f.kind, f.name) for f in compare_release(record, sums, assets, "v1", - installer=files)} - self.assertEqual(kinds, {("no-installer", "install.py")}) - - def test_listed_but_not_uploaded_is_missing(self): - record = {"tag": "v1", "packs": {}} - sums = _sums_with(list(INSTALLER)) - assets = {name: Asset(5, digest) for name, digest in sums.items() if name != "install.sh"} - assets[SUMS] = Asset(1, None) - kinds = {(f.kind, f.name) for f in compare_release(record, sums, assets, "v1", - installer=list(INSTALLER))} - self.assertEqual(kinds, {("missing", "install.sh")}) - - def test_the_release_steps_ship_them(self): + def test_the_release_steps_ship_none(self): text = _wiki("release-process.md") - self.assertIn("cp install.py install.sh install.ps1 install/*.json dist/", text) + self.assertNotIn("cp install.py", text) sums = re.search(r"sha256sum [^>]+> SHA256SUMS.txt", text, re.S).group(0) upload = re.search(r"for f in dist/SHA256SUMS.txt.*?; do", text, re.S).group(0) - for name in (*INSTALLER, "*.json"): + for name in ("install.py", "install.sh", "install.ps1", ".json"): with self.subTest(name=name): - self.assertIn(name, sums) - for name in INSTALLER: - with self.subTest(upload=name): - self.assertIn(f"dist/{name}", upload) - self.assertIn("install/*.json", upload) + self.assertNotIn(name, sums) + self.assertNotIn(name, upload) def test_the_installer_page_pins_one_commit(self): # v2026.10.10 carries no install.py: until a release does, the diff --git a/wiki/installer.md b/wiki/installer.md index 0b42087a7..7dcd94386 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -1506,9 +1506,8 @@ RetroBIOS also runs on the handheld itself, in the Copy the file for the system to the SD card, then start RetroBIOS from the menu. -The files below ship with the next release. Until then, build them from a -clone of the repository with `python scripts/build_port.py`, which writes all -four into `dist/`, or use the computer route at the end of this section. +Download the file for your system from the +[latest release](https://github.com/Abdess/retrobios/releases/latest). | System | File | Copy it to | Start it from | |--------|------|------------|---------------| diff --git a/wiki/release-process.md b/wiki/release-process.md index 104eab64a..d910bfe48 100644 --- a/wiki/release-process.md +++ b/wiki/release-process.md @@ -256,17 +256,17 @@ python scripts/split_pack.py dist/ # check_release.py compares each asset with its line there. python scripts/build_port.py -# 2c. The installer and the file lists step 1 wrote, so a package can pin -# one release (installer page, "For packagers"). check_release.py -# refuses a release that lacks one of them. -cp install.py install.sh install.ps1 install/*.json dist/ +# 2c. The release carries the packs, the handheld launchers and the signed +# checksum list, and nothing else: a beginner reads that page to pick one +# file. A package pins install.py and its file list from the tag +# (installer page, "For packagers"), and check_release.py refuses an +# installer or a file list attached to the release. # 3. Checksums of the files as published, then sign the list. The checksums # answer corruption, and the signature answers a rewritten release, which is # the one thing a checksum published beside its own artifacts cannot # answer. -(cd dist && sha256sum *.zip RetroBIOS.muxapp install.py install.sh install.ps1 \ - $(cd ../install && ls *.json) > SHA256SUMS.txt) +(cd dist && sha256sum *.zip RetroBIOS.muxapp > SHA256SUMS.txt) ssh-keygen -Y sign -f ~/.ssh/retrobios_signing -n file dist/SHA256SUMS.txt # 4. Record what each pack holds, read from the archives just checked: @@ -291,8 +291,7 @@ python scripts/release_notes.py --whats-new tmp/whats-new.md --output tmp/notes. # A public release with half its assets is a broken download for everyone # during the whole upload. gh release create "v${DATE}" --draft --title "BIOS Pack v${DATE}" --notes-file tmp/notes.md -for f in dist/SHA256SUMS.txt dist/SHA256SUMS.txt.sig dist/*.zip dist/RetroBIOS.muxapp \ - dist/install.py dist/install.sh dist/install.ps1 $(ls install/*.json | sed 's|^install/|dist/|'); do +for f in dist/SHA256SUMS.txt dist/SHA256SUMS.txt.sig dist/*.zip dist/RetroBIOS.muxapp; do [ -f "$f" ] && gh release upload "v${DATE}" "$f#$(basename "$f")" --clobber done # A count does not see a failed upload beside a stray file, nor a part