chore: merge the launchers served by the site

This commit is contained in:
Abdessamad Derraz committed 2026-11-10 12:03:29 +01:00
commit fba9a0b511
17 files changed
+837 -80

No files matched your search

+16 -14
View File
@@ -15,6 +15,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
import build_port # noqa: E402
from check_release import SIGNATURE, SUMS, Asset, compare_release, parse_sums
A = "a" * 64
@@ -97,20 +98,21 @@ class ReleaseMatchesWhatWasBuilt(unittest.TestCase):
self.assertIn(("unrecorded", "Other_BIOS_Pack.zip"), kinds)
self.assertIn(("missing", "Other_BIOS_Pack.zip"), kinds)
def test_the_handheld_port_is_vouched_for_by_the_checksum_list(self):
sums = {**SUMS_LIST, "RetroBIOS.zip": A}
assets = _assets()
self.assertEqual(
_kinds(compare_release(RECORD, sums, assets, "v2026.10.10")),
{("missing", "RetroBIOS.zip")},
)
assets["RetroBIOS.zip"] = Asset(70, A)
self.assertEqual(compare_release(RECORD, sums, assets, "v2026.10.10"), [])
assets["RetroBIOS.zip"] = Asset(70, B)
self.assertEqual(
_kinds(compare_release(RECORD, sums, assets, "v2026.10.10")),
{("content", "RetroBIOS.zip")},
)
def test_a_handheld_launcher_is_no_release_file(self):
"""The site serves the launchers built from its own commit: one in
the checksum list or among the assets is refused."""
for name in build_port.ARCHIVES:
with self.subTest(name=name):
sums = {**SUMS_LIST, name: A}
assets = _assets()
assets[name] = Asset(70, A)
self.assertEqual(_kinds(compare_release(RECORD, sums, assets, "v2026.10.10")),
{("unrecorded", name)})
assets = _assets()
assets[name] = Asset(70, A)
self.assertEqual(
_kinds(compare_release(RECORD, SUMS_LIST, assets, "v2026.10.10")),
{("extra", name)})
def test_parts_must_add_up_to_the_printed_download_size(self):
assets = _assets()
+8 -2
View File
@@ -374,10 +374,16 @@ class TheArchives(unittest.TestCase):
with self.subTest(archive=name), self.assertRaises(ValueError):
build_port.build_archive(name, self.out / name, root)
def test_the_release_process_attaches_each(self):
def test_the_site_serves_each_and_the_release_none(self):
"""Each archive carries install.py: the site serves it from its own
commit (sitehandheld.py), and the release steps never build one."""
text = (REPO_ROOT / "wiki" / "release-process.md").read_text(encoding="utf-8")
self.assertNotIn("scripts/build_port.py", text)
installer = (REPO_ROOT / "wiki" / "installer.md").read_text(encoding="utf-8")
for name in build_port.ARCHIVES:
self.assertIn(name, text)
with self.subTest(archive=name):
self.assertNotIn(name, text)
self.assertIn(f"](../get/handheld/{name})", installer)
FAKE_INSTALL = r'''
+139 -4
View File
@@ -313,13 +313,14 @@ class TheReleaseNotesNameTheSetupsWithoutAZip(unittest.TestCase):
import release_notes
text = release_notes.render(self.RECORD, PLATFORMS, "### What changed\n\n- x\n")
paragraph = next(line for line in text.splitlines() if line.startswith("No ZIP for "))
paragraph = next(line for line in text.splitlines()
if line.startswith("Only the most used setups have a pack"))
for name in ("KNULLI", "Recalbox", "RetroPie", "Lakka"):
self.assertIn(name, paragraph)
for name in ("RetroArch", "Batocera", "RomM"):
self.assertNotIn(f" {name},", paragraph)
self.assertIn("The command above", paragraph)
self.assertIn("--dry-run", paragraph)
self.assertTrue(paragraph.endswith("use the command above."))
self.assertLess(text.index("| Platform"), text.index(paragraph))
self.assertLess(text.index(paragraph), text.index("### What changed"))
def test_the_rows_link_this_release_files(self):
@@ -331,7 +332,141 @@ class TheReleaseNotesNameTheSetupsWithoutAZip(unittest.TestCase):
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)
rows = [line for line in text.splitlines() if line.startswith("| ")]
self.assertTrue(rows)
for row in rows:
self.assertNotIn(f"{SITE}get/", row)
def test_the_command_comes_before_the_packs(self):
"""Most readers need a few files, not a pack of several GB (owner,
2026-11-10): the command and the handheld app open the notes."""
import release_notes
text = release_notes.render(self.RECORD, PLATFORMS, "### What changed\n\n- x\n")
command = text.index("```sh")
# The installer's handheld section, which opens on every download.
handheld = text.index(f"{SITE}wiki/installer/#on-a-handheld-the-retrobios-port")
packs = text.index("### Full packs")
self.assertLess(text.index("### Get your BIOS"), command)
self.assertLess(command, handheld)
self.assertLess(handheld, packs)
self.assertLess(packs, text.index("| Platform"))
self.assertLess(packs, text.index("### What changed"))
self.assertNotIn("releases/download/", text[:packs])
class TheReleaseNotesThankPeople(unittest.TestCase):
"""The notes end with everyone who helped since the repository started
and the full changelog, read from gh and git (owner, 2026-11-10).
Fixtures stand in for both."""
TAGS = {"c4": "v2026.11.10", "c3": "v2026.10.10", "c2": "v2026.10.10",
"c1": "v2026.10.10", "c0": "v2026.09.04"}
# `gh api --paginate --jq` prints one login per line, none for a
# commit GitHub links to no account.
COMMIT_AUTHORS = "Abdess\nhabib256\nAbdess76\ndependabot[bot]\nzed\nhabib256\n"
PULLS = [
{"number": 84, "author": {"login": "dajiaohuang", "is_bot": False}},
{"number": 86, "author": {"login": "habib256", "is_bot": False}},
{"number": 80, "author": {"login": "app/dependabot", "is_bot": True}},
{"number": 67, "author": {"login": "prakashmagat50-byte", "is_bot": False}},
{"number": 70, "author": {"login": "closedwithoutmerge", "is_bot": False}},
]
ISSUES = [
{"number": 87, "author": {"login": "paperweightorder", "is_bot": False}},
{"number": 85, "author": {"login": "72374", "is_bot": False}},
{"number": 83, "author": {"login": "72374", "is_bot": False}},
{"number": 32, "author": {"login": "Bappo0", "is_bot": False}},
{"number": 31, "author": {"login": "ghost", "is_bot": False}},
{"number": 30, "author": {"login": "habib256", "is_bot": False}},
{"number": 29, "author": {"login": "Abdess", "is_bot": False}},
]
ISSUE_COMMENTS = "Abdess\nhelper\n72374\ngithub-actions[bot]\nAlpha\n"
REVIEW_COMMENTS = "reviewer\nhabib256\n"
def fake(self, argv):
self.calls.append(argv)
if argv[:2] == ["git", "log"]:
return "\n".join(self.TAGS) + "\n"
if argv[:2] == ["git", "show"]:
return json.dumps({"tag": self.TAGS[argv[2].split(":")[0]]})
if argv[:2] == ["gh", "api"]:
path = argv[3]
return {"repos/Abdess/retrobios/commits": self.COMMIT_AUTHORS,
"repos/Abdess/retrobios/issues/comments": self.ISSUE_COMMENTS,
"repos/Abdess/retrobios/pulls/comments": self.REVIEW_COMMENTS}[path]
if argv[:3] == ["gh", "pr", "list"]:
return json.dumps(self.PULLS)
if argv[:3] == ["gh", "issue", "list"]:
return json.dumps(self.ISSUES)
raise AssertionError(argv)
def setUp(self):
self.calls = []
def test_the_base_is_the_previous_release_record(self):
import release_notes
self.assertEqual(release_notes.previous_record_commit("v2026.11.10", self.fake), "c1")
def test_everyone_since_the_start_is_thanked_once(self):
import release_notes
lines = release_notes.gather_thanks(self.fake)
self.assertEqual(lines, [
"- **Code and data**: @closedwithoutmerge, @dajiaohuang, @habib256 and @zed.",
"- **Reports**: @72374 and @paperweightorder.",
"- **Help in issues**: @Alpha, @helper and @reviewer.",
])
text = "\n".join(lines)
for left_out in ("Abdess", "bot", "dependabot", "ghost", "Bappo0", "prakashmagat50"):
self.assertNotIn(left_out, text)
# Every state: a pull request closed without merge still counts.
pulls = next(argv for argv in self.calls if argv[:3] == ["gh", "pr", "list"])
self.assertEqual(pulls[pulls.index("--state") + 1], "all")
issues = next(argv for argv in self.calls if argv[:3] == ["gh", "issue", "list"])
self.assertEqual(issues[issues.index("--state") + 1], "all")
def test_the_notes_end_with_thanks_then_the_changelog(self):
import release_notes
closing = release_notes.closing_lines(
["- **Code and data**: @habib256."],
release_notes.changelog_url("55eb95344" + "0" * 31, "v2026.11.10"))
text = release_notes.render(TheReleaseNotesNameTheSetupsWithoutAZip.RECORD, PLATFORMS,
"### What changed\n\n- x\n", closing)
self.assertLess(text.index("### What changed"), text.index("### Thanks"))
self.assertIn("### Thanks\n\n<details>\n"
"<summary>Everyone who helped since the start</summary>\n\n"
"- **Code and data**: @habib256.\n\n</details>\n\n**Full changelog**",
text)
self.assertEqual(release_notes.refused_words(text), [])
self.assertTrue(text.rstrip().endswith(
"**Full changelog**: https://github.com/Abdess/retrobios/compare/"
"55eb95344...v2026.11.10"))
def test_a_checked_list_replaces_the_one_gh_gives(self):
import release_notes
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
tmp = Path(tmp)
(tmp / "whats-new.md").write_text("### What changed\n\n- x\n", encoding="utf-8")
(tmp / "thanks.md").write_text("- **Code and data**: @habib256.\n",
encoding="utf-8")
(tmp / "release.json").write_text(
json.dumps(TheReleaseNotesNameTheSetupsWithoutAZip.RECORD), encoding="utf-8")
argv = ["release_notes.py", "--record", str(tmp / "release.json"),
"--whats-new", str(tmp / "whats-new.md"), "--thanks", str(tmp / "thanks.md"),
"--platforms-dir", PLATFORMS, "--output", str(tmp / "notes.md")]
with mock.patch.object(sys, "argv", argv), \
mock.patch.object(release_notes, "_run", self.fake), \
mock.patch.object(release_notes, "gather_thanks",
side_effect=AssertionError("gh read")), \
contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(release_notes.main(), 0)
text = (tmp / "notes.md").read_text(encoding="utf-8")
self.assertIn("- **Code and data**: @habib256.\n", text)
self.assertIn("/compare/c4...v2026.10.24", text)
class EveryPlatformHasAPermanentPage(unittest.TestCase):
+168
View File
@@ -0,0 +1,168 @@
"""The handheld launchers the site serves.
The four archives build_port.py writes carry install.py pinned by SHA-256,
so they left the release (owner, 2026-11-10): the site builds them from its
own commit, serves them under get/handheld/, and the get/ page of each
system names the file, its size and hash, where it goes and which menu
starts it.
"""
from __future__ import annotations
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
import contextlib
import hashlib
import re
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import build_port # noqa: E402
import common # noqa: E402
import sitehandheld # noqa: E402
PLATFORMS = str(REPO_ROOT / "platforms")
def _pages() -> dict[str, str]:
"""Every page that carries a launcher, by site path."""
import generate_site # noqa: PLC0415
with contextlib.chdir(REPO_ROOT):
pages = {f"get/{key}.md": generate_site.generate_get_page(key)
for key in sorted(set(sitehandheld.PLATFORM_OF.values()))}
pages[f"get/{sitehandheld.GROUP_PAGE}.md"] = sitehandheld.group_page(REPO_ROOT)
return pages
class TheSiteServesTheLaunchers(unittest.TestCase):
def setUp(self):
scratch = REPO_ROOT / "tmp"
scratch.mkdir(exist_ok=True)
self.tmp = Path(tempfile.mkdtemp(dir=scratch, prefix="site-handheld-"))
self.addCleanup(shutil.rmtree, self.tmp, True)
def test_the_written_archives_are_the_ones_build_port_writes(self):
written = sitehandheld.write_launchers(self.tmp / "public", REPO_ROOT)
self.assertEqual([path.name for path in written], list(build_port.ARCHIVES))
for path in written:
with self.subTest(archive=path.name):
self.assertEqual(path.parent, self.tmp / "public" / "get" / "handheld")
built = build_port.build_archive(path.name, self.tmp / "built" / path.name)
self.assertEqual(path.read_bytes(), built.read_bytes())
def test_an_unchanged_archive_is_not_rewritten(self):
first = sitehandheld.write_launchers(self.tmp / "public", REPO_ROOT)
stamps = {path: path.stat().st_mtime_ns for path in first}
sitehandheld.write_launchers(self.tmp / "public", REPO_ROOT)
self.assertEqual({path: path.stat().st_mtime_ns for path in first}, stamps)
def test_a_stale_pin_stops_the_build(self):
"""build_port refuses an install.py the bootstraps do not pin: served,
that launcher would refuse its own installer."""
with mock.patch.dict(sitehandheld._built, clear=True), \
mock.patch.object(build_port, "pinned_installer_sha256", return_value="0" * 64):
with self.assertRaisesRegex(ValueError, "re-pin the bootstraps"):
sitehandheld.write_launchers(self.tmp / "public", REPO_ROOT)
self.assertFalse(list((self.tmp / "public").rglob("RetroBIOS*")))
def test_every_archive_is_on_a_page_with_its_link_and_hash(self):
public = self.tmp / "public"
written = {path.name: path for path in sitehandheld.write_launchers(public, REPO_ROOT)}
pages = _pages()
for name in build_port.ARCHIVES:
with self.subTest(archive=name):
data = written[name].read_bytes()
digest = hashlib.sha256(data).hexdigest()
holding = [page for page, text in pages.items()
if f'href="handheld/{name}"' in text]
self.assertTrue(holding, f"no page links {name}")
for page in holding:
text = pages[page]
# The link resolves to the file written under public/.
target = (Path(page).parent / "handheld" / name).as_posix()
self.assertTrue((public / target).is_file(), target)
block = text.split(f'href="handheld/{name}"', 1)[1].split("\n\n", 2)[1]
self.assertIn(f"SHA-256: `{digest}`", block)
self.assertIn(sitehandheld._size(len(data)), block)
self.assertRegex(block, r"- (Copy it to|Unzip it into) .+\.")
self.assertRegex(block, r"- Start it from .+\.")
class WhereEachSystemFindsItsFile(unittest.TestCase):
def test_each_platform_page_is_a_registered_platform(self):
registered = set(common.list_registered_platforms(PLATFORMS))
self.assertLessEqual(set(sitehandheld.PLATFORM_OF.values()), registered)
self.assertNotIn(sitehandheld.GROUP_PAGE, registered)
def test_the_group_page_sits_with_the_handheld_guide(self):
"""It shares the guide's name, so the sidebar files it with it, and
the guide a muOS or ArkOS owner lands on links it."""
import generate_site # noqa: PLC0415
guide = generate_site.HANDHELD_GUIDE
self.assertEqual(sitehandheld.GROUP_PAGE, guide.page)
with contextlib.chdir(REPO_ROOT):
self.assertIn(f"(../get/{sitehandheld.GROUP_PAGE}.md)",
generate_site.generate_handheld_guide(guide))
self.assertIn(f"]({sitehandheld.GROUP_PAGE}.md)", generate_site.generate_get_index())
def test_every_system_has_its_launchers_on_its_page(self):
pages = _pages()
for system in sitehandheld.systems():
page = sitehandheld.page_of(system)
with self.subTest(system=system):
self.assertIn(page, pages)
for launcher in sitehandheld.launchers_for(system):
self.assertIn(f'href="handheld/{launcher.archive}"', pages[page])
self.assertIn(launcher.folder_for(system), pages[page])
self.assertIn(launcher.start, pages[page])
for system in sitehandheld.group_systems():
self.assertIn(f"\n## {system}\n", pages[f"get/{sitehandheld.GROUP_PAGE}.md"])
def test_the_installer_table_is_written_from_the_same_rows(self):
text = (REPO_ROOT / "wiki" / "installer.md").read_text(encoding="utf-8")
block = re.search(r"<!-- generated: handheld -->\n(.*?)<!-- /generated: handheld -->",
text, re.S)
self.assertIsNotNone(block)
self.assertEqual(block.group(1).strip(), "\n".join(sitehandheld.table_rows()))
self.assertNotIn("releases/latest", text.split("## On a handheld", 1)[1].split("\n## ")[0])
def test_the_installer_section_opens_on_the_downloads(self):
"""The section was long, with no link to click (owner, 2026-11-10):
it opens on one row per system, each file a link to its copy on the
site, before the detail."""
text = (REPO_ROOT / "wiki" / "installer.md").read_text(encoding="utf-8")
section = text.split("## On a handheld", 1)[1].split("\n## ")[0]
block = re.search(r"<!-- generated: handheld-downloads -->\n(.*?)"
r"<!-- /generated: handheld-downloads -->", section, re.S)
self.assertIsNotNone(block)
self.assertEqual(block.group(1).strip(), "\n".join(sitehandheld.download_rows()))
self.assertLess(block.start(), section.index("### Where each file goes"))
self.assertLess(len(section[:block.start()].split()), 40)
for system in sitehandheld.systems():
with self.subTest(system=system):
row = next(line for line in block.group(1).splitlines()
if line.startswith(f"| {system} |"))
for launcher in sitehandheld.launchers_for(system):
self.assertIn(f"](../{sitehandheld.SERVED_DIR}/{launcher.archive})", row)
self.assertIn(f"](../{sitehandheld.page_of(system)}#", row)
for name in build_port.ARCHIVES:
self.assertIn(f"](../{sitehandheld.SERVED_DIR}/{name})", block.group(1))
def test_the_generator_writes_them(self):
source = (REPO_ROOT / "scripts" / "generate_site.py").read_text(encoding="utf-8")
self.assertIn("sitehandheld.write_launchers(public, _REPO_ROOT)", source)
self.assertIn("sitehandheld.platform_block(platform_name, _REPO_ROOT)", source)
self.assertIn("sitehandheld.group_page(_REPO_ROOT)", source)
if __name__ == "__main__":
unittest.main()
+21 -1
View File
@@ -33,6 +33,7 @@ from __future__ import annotations
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
import fnmatch
import json
import re
import sys
@@ -48,6 +49,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
import build_port # noqa: E402
from check_release import SUMS, Asset, compare_release # noqa: E402
@@ -870,15 +872,33 @@ class PinnedInstaller(unittest.TestCase):
kinds = {(f.kind, f.name) for f in compare_release(record, sums, assets, "v1")}
self.assertEqual(kinds, {("unrecorded", "install.py"), ("unrecorded", "retroarch.json")})
def test_a_launcher_attached_to_the_release_is_refused(self):
"""The handheld launchers carry install.py: the site serves them
from its own commit, the release never does."""
record = {"tag": "v1", "packs": {"P.zip": {"download_size": 5, "assets": ["P.zip"]}}}
sums = _sums_with(["P.zip", *build_port.ARCHIVES])
assets = {name: Asset(5, digest) for name, digest in sums.items()}
assets[SUMS] = Asset(1, None)
kinds = {(f.kind, f.name) for f in compare_release(record, sums, assets, "v1")}
self.assertEqual(kinds, {("unrecorded", name) for name in build_port.ARCHIVES})
def test_the_release_steps_ship_none(self):
text = _wiki("release-process.md")
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 ("install.py", "install.sh", "install.ps1", ".json"):
for name in ("install.py", "install.sh", "install.ps1", ".json", "RetroBIOS",
".muxapp", "build_port", " *.zip", "/*.zip"):
with self.subTest(name=name):
self.assertNotIn(name, sums)
self.assertNotIn(name, upload)
# A launcher built into dist/ is never caught by the glob either.
for archive in build_port.ARCHIVES:
with self.subTest(archive=archive):
self.assertFalse(fnmatch.fnmatch(archive, "*_BIOS_Pack*.zip"))
self.assertIn("*_BIOS_Pack*.zip", sums)
self.assertIn("*_BIOS_Pack*.zip", upload)
self.assertNotIn("scripts/build_port.py", text)
def test_the_installer_page_pins_one_commit(self):
# v2026.10.10 carries no install.py: until a release does, the
+1 -1
View File
@@ -278,7 +278,7 @@ class ReleaseProcessPublishesArchives(unittest.TestCase):
def test_the_checksums_are_those_of_the_published_files(self):
self.assertLess(
self.text.index("scripts/split_pack.py"),
self.text.index("sha256sum *.zip"),
self.text.index("sha256sum *_BIOS_Pack*.zip"),
)