mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
docs: keep the release page to packs and launchers
The release carries the packs, the handheld launchers and the signed checksum list. The installer and the file lists are pinned from the tag (installer page, For packagers), and check_release.py now refuses them as unrecorded files. The notes link each pack's files on the release itself, in a beginner's page: one idea per line. The handheld section links the latest release.
This commit is contained in:
1 parent
96ef06b9fa
commit
12daa6698f
7 files changed
+72
-101
No files matched your search
@@ -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),
|
||||
|
||||
+33
-27
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
+13
-35
@@ -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
|
||||
|
||||
+2
-3
@@ -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 |
|
||||
|--------|------|------------|---------------|
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user