mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
docs: archive recipes page and README badges
Every archive has a recipe in recipes/archives.json, and no page a user reads said so. A short reference page says what a recipe is, how to rebuild one archive and what exact and canonical mean. Its counts and the README line are counted from the recipe file at each build. The README gains a row of four badges: PyPI version, Python versions, the Validation workflow on main and the license. Tests hold the badges, the counts and the page's command.
This commit is contained in:
1 parent
d8b55fc2de
commit
92546794e2
9 files changed
+188
-1
No files matched your search
@@ -11,6 +11,7 @@ on:
|
||||
- "exports/**"
|
||||
# Every script the build runs or imports, and no other
|
||||
# (tests/test_workflow_paths.py).
|
||||
- "scripts/archive_build.py"
|
||||
- "scripts/artifacts.py"
|
||||
- "scripts/auto_fetch.py"
|
||||
- "scripts/common.py"
|
||||
@@ -60,6 +61,7 @@ on:
|
||||
- "scripts/siteexports.py"
|
||||
- "scripts/siterender.py"
|
||||
- "scripts/retired_files.py"
|
||||
- "scripts/romset_recipes.py"
|
||||
- "scripts/sitesources.py"
|
||||
- "scripts/sitesteps.py"
|
||||
- "scripts/sitestarlight.py"
|
||||
@@ -68,6 +70,7 @@ on:
|
||||
- "scripts/slots.py"
|
||||
- "scripts/sourceref.py"
|
||||
- "scripts/split_pack.py"
|
||||
- "scripts/torrentzip.py"
|
||||
- "scripts/truth.py"
|
||||
- "scripts/upstream.py"
|
||||
- "scripts/validate_pr.py"
|
||||
@@ -88,6 +91,8 @@ on:
|
||||
- "release.json"
|
||||
- "derivable.json"
|
||||
- "retired.json"
|
||||
# The README and the archive recipes page count the recipes.
|
||||
- "recipes/archives.json"
|
||||
- "measurements/**"
|
||||
# The Proof lines of the emulator pages read what an issue's runs showed.
|
||||
- "tests/campaign/observations.yml"
|
||||
|
||||
@@ -32,6 +32,7 @@ on:
|
||||
- "release.json"
|
||||
- "derivable.json"
|
||||
- "retired.json"
|
||||
- "recipes/**"
|
||||
- "measurements/**"
|
||||
- "website/**"
|
||||
# The type check's configuration and baseline.
|
||||
@@ -62,6 +63,7 @@ on:
|
||||
- "release.json"
|
||||
- "derivable.json"
|
||||
- "retired.json"
|
||||
- "recipes/**"
|
||||
- "measurements/**"
|
||||
- "website/**"
|
||||
- "pyproject.toml"
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
<p align="center"><a href="https://abdess.github.io/retrobios/">Website</a> · <a href="https://abdess.github.io/retrobios/which-pack/">Get the files</a> · <a href="https://abdess.github.io/retrobios/wiki/troubleshooting/">A game does not start</a> · <a href="https://abdess.github.io/retrobios/wiki/faq/">Questions</a></p>
|
||||
|
||||
<p align="center"><a href="https://pypi.org/project/retrobios/"><img src="https://img.shields.io/pypi/v/retrobios" alt="PyPI version"></a> <a href="https://pypi.org/project/retrobios/"><img src="https://img.shields.io/pypi/pyversions/retrobios" alt="Python versions"></a> <a href="https://github.com/Abdess/retrobios/actions/workflows/validate.yml"><img src="https://github.com/Abdess/retrobios/actions/workflows/validate.yml/badge.svg?branch=main" alt="Validation"></a> <a href="LICENSE"><img src="https://img.shields.io/github/license/Abdess/retrobios" alt="License"></a></p>
|
||||
|
||||
Some consoles need a small program, the **BIOS**, to start their games: PlayStation 1 and 2, Saturn, Neo Geo, Dreamcast and more. RetroBIOS installs the BIOS and firmware files for AmberELEC, Batocera, BizHawk, EmuDeck, EmuELEC, KNULLI, Lakka, MiSTer FPGA, MinUI, MyMinUI, NextUI, ROCKNIX, Recalbox, RetroArch, RetroBat, RetroDECK, RetroPie, and RomM, checked the way each one checks them.
|
||||
|
||||
## Install in one line
|
||||
@@ -64,6 +66,7 @@ Unzip into the BIOS folder, files straight in it. A big pack comes in parts (`Pa
|
||||
- **623 emulators profiled**, 589 from their source code and 34 closed-source builds from their binaries: 93% of the files they load cite the file and line that reads them
|
||||
- **18 setups**, each checked its own way: MD5 for Batocera and Recalbox, the file name for RetroArch, as their own code does
|
||||
- **532 systems**, 19,947 files in the collection, [what is still missing](https://abdess.github.io/retrobios/gaps/) named file by file
|
||||
- **2,222 archives**, each with a recipe that rebuilds it from the ROMs the collection holds: 1,790 byte for byte, 432 with the same files inside ([rebuilding an archive](https://abdess.github.io/retrobios/wiki/archive-recipes/))
|
||||
- Public data: [JSON, CSV and SQLite](https://abdess.github.io/retrobios/data/), the metadata under MIT
|
||||
|
||||
<details>
|
||||
@@ -107,4 +110,4 @@ The scripts and tooling are released under the [MIT License](LICENSE).
|
||||
The BIOS and firmware files are not covered by that license: they are third-party system software, preserved and provided for personal backup, archival, and interoperability with emulation software. [NOTICE](NOTICE) sets out their status and how to ask for a file to be removed.
|
||||
The reasoning, and where it is weakest, is in [the project's legal position](https://abdess.github.io/retrobios/wiki/legal/).
|
||||
|
||||
*Auto-generated on 2026-11-09T07:40:17Z*
|
||||
*Auto-generated on 2026-11-09T13:12:14Z*
|
||||
@@ -83,6 +83,17 @@ def load_ledger(path: Path = ARCHIVES_FILE) -> dict[str, dict]:
|
||||
return {record["path"]: record for record in data.get("archives", [])}
|
||||
|
||||
|
||||
def rebuild_counts(path: Path = ARCHIVES_FILE) -> dict[str, int]:
|
||||
"""How many recipes rebuild ``exact`` and ``canonical``, and ``total``:
|
||||
the figures the README and the site print."""
|
||||
records = load_ledger(path).values()
|
||||
return {
|
||||
"total": len(records),
|
||||
**{kind: sum(1 for r in records if r.get("rebuild") == kind)
|
||||
for kind in ("exact", "canonical")},
|
||||
}
|
||||
|
||||
|
||||
def ledger_text(records: dict[str, dict]) -> str:
|
||||
"""One record per line, so a diff names the archive that changed."""
|
||||
lines = [
|
||||
|
||||
@@ -38,6 +38,7 @@ from common import (
|
||||
write_if_changed,
|
||||
)
|
||||
from nativemode import check_label
|
||||
import archive_build
|
||||
import release_record
|
||||
from siteguides import PACKAGE_COMMAND, install_command
|
||||
from validate_pr import LARGE_FILE_ISSUE_URL
|
||||
@@ -563,6 +564,32 @@ def _catalog_matched_line(db: dict) -> list[str]:
|
||||
]
|
||||
|
||||
|
||||
# The badge row under the links: (alt text, image, link).
|
||||
BADGES = (
|
||||
("PyPI version", "https://img.shields.io/pypi/v/retrobios", "https://pypi.org/project/retrobios/"),
|
||||
("Python versions", "https://img.shields.io/pypi/pyversions/retrobios",
|
||||
"https://pypi.org/project/retrobios/"),
|
||||
("Validation",
|
||||
"https://github.com/Abdess/retrobios/actions/workflows/validate.yml/badge.svg?branch=main",
|
||||
"https://github.com/Abdess/retrobios/actions/workflows/validate.yml"),
|
||||
("License", "https://img.shields.io/github/license/Abdess/retrobios", "LICENSE"),
|
||||
)
|
||||
|
||||
|
||||
def badge_row() -> str:
|
||||
badges = " ".join(f'<a href="{link}"><img src="{image}" alt="{alt}"></a>'
|
||||
for alt, image, link in BADGES)
|
||||
return f'<p align="center">{badges}</p>'
|
||||
|
||||
|
||||
def archive_line(counts: dict[str, int]) -> str:
|
||||
"""The README's line on archive recipes, counted from recipes/archives.json."""
|
||||
return (f"- **{counts['total']:,} archives**, each with a recipe that rebuilds it from the"
|
||||
f" ROMs the collection holds: {counts['exact']:,} byte for byte,"
|
||||
f" {counts['canonical']:,} with the same files inside"
|
||||
f" ([rebuilding an archive]({SITE_URL}wiki/archive-recipes/))")
|
||||
|
||||
|
||||
def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json") -> str:
|
||||
total_files = db.get("total_files", 0)
|
||||
total_size = db.get("total_size", 0)
|
||||
@@ -629,6 +656,8 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
|
||||
f'<a href="{SITE_URL}wiki/faq/">Questions</a>'
|
||||
"</p>",
|
||||
"",
|
||||
badge_row(),
|
||||
"",
|
||||
"Some consoles need a small program, the **BIOS**, to start their games: PlayStation 1"
|
||||
" and 2, Saturn, Neo Geo, Dreamcast and more. RetroBIOS installs the BIOS and firmware"
|
||||
f" files for {_names_list(coverages)}, checked the way each one checks them.",
|
||||
@@ -715,6 +744,7 @@ def generate_readme(db: dict, platforms_dir: str, db_path: str = "database.json"
|
||||
" the file name for RetroArch, as their own code does",
|
||||
f"- **{len(system_ids)} systems**, {total_files:,} files in the collection,"
|
||||
f" [what is still missing]({SITE_URL}gaps/) named file by file",
|
||||
archive_line(archive_build.rebuild_counts()),
|
||||
f"- Public data: [JSON, CSV and SQLite]({SITE_URL}data/), the metadata under MIT",
|
||||
"",
|
||||
"<details>",
|
||||
|
||||
@@ -124,6 +124,7 @@ from siteguides import (
|
||||
terminal_tabs,
|
||||
trust_box,
|
||||
)
|
||||
import archive_build
|
||||
import release_record
|
||||
import sitebootstrap
|
||||
import sitemeasurements
|
||||
@@ -8784,6 +8785,30 @@ def with_romset_table(page: str, profiles: dict) -> str:
|
||||
return _ROMSET_BLOCK.sub(lambda m: f"{m.group(1)}{table}{m.group(3)}", page, count=1)
|
||||
|
||||
|
||||
ARCHIVE_RECIPES_PAGE = Path(WIKI_SRC_DIR) / "archive-recipes.md"
|
||||
_ARCHIVE_BLOCK = re.compile(
|
||||
r"(<!-- generated: archive-recipes -->\n)(.*?)(<!-- /generated: archive-recipes -->)", re.S)
|
||||
|
||||
|
||||
def archive_recipe_lines(counts: dict[str, int]) -> list[str]:
|
||||
"""What an exact and a canonical rebuild are, each with its count."""
|
||||
total = f"{counts['total']:,}"
|
||||
return [
|
||||
f"- **Exact**: the same bytes as the archive in the collection."
|
||||
f" {counts['exact']:,} of {total} archives.",
|
||||
f"- **Canonical**: the same files inside, packed another way."
|
||||
f" {counts['canonical']:,} of {total} archives.",
|
||||
]
|
||||
|
||||
|
||||
def with_archive_counts(page: str, counts: dict[str, int]) -> str:
|
||||
"""The archive recipes page with its counts read from recipes/archives.json."""
|
||||
block = "\n".join(archive_recipe_lines(counts)) + "\n"
|
||||
if not _ARCHIVE_BLOCK.search(page):
|
||||
raise ValueError(f"{ARCHIVE_RECIPES_PAGE}: no archive-recipes block to write the counts in")
|
||||
return _ARCHIVE_BLOCK.sub(lambda m: f"{m.group(1)}{block}{m.group(3)}", page, count=1)
|
||||
|
||||
|
||||
def _render_metadata_rows(profile: dict) -> list[str]:
|
||||
"""Scalar profile fields, one table row each.
|
||||
|
||||
@@ -12673,6 +12698,10 @@ def _generate(args: argparse.Namespace) -> None:
|
||||
faq = _REPO_ROOT / ROMSET_FAQ_PAGE
|
||||
if faq.is_file():
|
||||
write_if_changed(str(faq), with_romset_table(faq.read_text(encoding="utf-8"), profiles))
|
||||
recipes_page = _REPO_ROOT / ARCHIVE_RECIPES_PAGE
|
||||
if recipes_page.is_file():
|
||||
write_if_changed(str(recipes_page), with_archive_counts(
|
||||
recipes_page.read_text(encoding="utf-8"), archive_build.rebuild_counts()))
|
||||
if wiki_src.is_dir():
|
||||
for src_file in wiki_src.glob("*.md"):
|
||||
shutil.copy2(src_file, wiki_dest / src_file.name)
|
||||
|
||||
@@ -636,5 +636,74 @@ class ContributorChecksFetchThePlatformsFiles(unittest.TestCase):
|
||||
self.assertIn("export_native.py --all --fetch", _read("wiki/testing-guide.md"))
|
||||
|
||||
|
||||
class ArchiveRecipesAreCountedFromTheRecipes(unittest.TestCase):
|
||||
"""Every archive has a recipe in recipes/archives.json: the README and
|
||||
the site's page say how many rebuild exact and canonical, counted from
|
||||
that file, never typed."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
import archive_build # noqa: PLC0415
|
||||
|
||||
records = archive_build.load_ledger().values()
|
||||
self.counts = {
|
||||
"total": len(records),
|
||||
"exact": sum(r["rebuild"] == "exact" for r in records),
|
||||
"canonical": sum(r["rebuild"] == "canonical" for r in records),
|
||||
}
|
||||
self.assertEqual(archive_build.rebuild_counts(), self.counts)
|
||||
self.assertEqual(self.counts["exact"] + self.counts["canonical"], self.counts["total"])
|
||||
|
||||
def test_the_readme_line(self) -> None:
|
||||
from generate_readme import archive_line # noqa: PLC0415
|
||||
|
||||
line = archive_line(self.counts)
|
||||
self.assertIn(line, _read("README.md").splitlines())
|
||||
self.assertIn(f"**{self.counts['total']:,} archives**", line)
|
||||
self.assertIn(f"{self.counts['exact']:,} byte for byte", line)
|
||||
self.assertIn(f"{self.counts['canonical']:,} with the same files inside", line)
|
||||
self.assertIn(f"({SITE_URL}wiki/archive-recipes/)", line)
|
||||
|
||||
def test_the_page_counts(self) -> None:
|
||||
import generate_site # noqa: PLC0415
|
||||
|
||||
page = _read("wiki/archive-recipes.md")
|
||||
self.assertEqual(generate_site.with_archive_counts(page, self.counts), page,
|
||||
"wiki/archive-recipes.md counts are stale: run scripts/generate_site.py")
|
||||
for line in generate_site.archive_recipe_lines(self.counts):
|
||||
self.assertIn(line, page)
|
||||
|
||||
def test_the_page_command_is_the_tools(self) -> None:
|
||||
command = re.search(r"python scripts/romset_recipes.py (.+)", _read("wiki/archive-recipes.md"))
|
||||
source = _read("scripts/romset_recipes.py")
|
||||
for flag in re.findall(r"--[a-z-]+", command.group(1)):
|
||||
with self.subTest(flag=flag):
|
||||
self.assertIn(f'"{flag}"', source)
|
||||
|
||||
|
||||
class TheReadmeBadges(unittest.TestCase):
|
||||
"""One row of four badges under the links, each linking its source."""
|
||||
|
||||
EXPECTED = (
|
||||
'<p align="center">'
|
||||
'<a href="https://pypi.org/project/retrobios/"><img src="https://img.shields.io/pypi/v/retrobios" alt="PyPI version"></a> '
|
||||
'<a href="https://pypi.org/project/retrobios/"><img src="https://img.shields.io/pypi/pyversions/retrobios" alt="Python versions"></a> '
|
||||
f'<a href="{REPO}/actions/workflows/validate.yml"><img src="{REPO}/actions/workflows/validate.yml/badge.svg?branch=main" alt="Validation"></a> '
|
||||
'<a href="LICENSE"><img src="https://img.shields.io/github/license/Abdess/retrobios" alt="License"></a>'
|
||||
"</p>"
|
||||
)
|
||||
|
||||
def test_exactly_these_four(self) -> None:
|
||||
readme = _read("README.md")
|
||||
self.assertEqual(len(re.findall(r"<img src=\"https://(?:img\.shields\.io|github\.com)/", readme)), 4)
|
||||
lines = readme.splitlines()
|
||||
self.assertIn(self.EXPECTED, lines)
|
||||
links = next(i for i, line in enumerate(lines) if ">Get the files</a>" in line)
|
||||
self.assertEqual(lines[links + 2], self.EXPECTED)
|
||||
|
||||
def test_the_workflow_and_the_license_exist(self) -> None:
|
||||
self.assertTrue((REPO_ROOT / ".github" / "workflows" / "validate.yml").is_file())
|
||||
self.assertTrue((REPO_ROOT / "LICENSE").is_file())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -41,6 +41,7 @@ START_PAGES = {
|
||||
# Reference pages written for anyone, held to the Start here budgets.
|
||||
START_LEVEL_PAGES = {
|
||||
"wiki/measurements.md": 300,
|
||||
"wiki/archive-recipes.md": 160,
|
||||
}
|
||||
# Level 1: the guide part of each setup page, above its Reference heading.
|
||||
SETUP_CEILING = 220
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
level: reference
|
||||
order: 45
|
||||
group: Checking files
|
||||
terms: recipe, rebuild, archive, zip, romset, exact, canonical
|
||||
---
|
||||
|
||||
# Rebuilding an archive
|
||||
|
||||
Arcade emulators read their BIOS files from a ZIP archive.
|
||||
|
||||
The collection keeps a **recipe** for each archive. It lists the files
|
||||
inside, in order, with their fingerprints. The recipe and those files make
|
||||
the archive again. No copy from elsewhere is needed.
|
||||
|
||||
## Rebuild one archive
|
||||
|
||||
In a copy of the project:
|
||||
|
||||
```bash
|
||||
python scripts/romset_recipes.py --build neogeo.zip --output-dir rebuilt
|
||||
```
|
||||
|
||||
Each archive of that name is made under `rebuilt/`, in the folders it has in
|
||||
the collection. It is checked against its recipe before it is kept.
|
||||
|
||||
## Exact or canonical
|
||||
|
||||
<!-- generated: archive-recipes -->
|
||||
- **Exact**: the same bytes as the archive in the collection. 1,790 of 2,222 archives.
|
||||
- **Canonical**: the same files inside, packed another way. 432 of 2,222 archives.
|
||||
<!-- /generated: archive-recipes -->
|
||||
|
||||
An emulator reads the files inside, so both work the same. A setup that
|
||||
checks the whole archive by its fingerprint needs the exact one.
|
||||
|
||||
Every option of the command: [tools](tools.md#archive-recipes).
|
||||
Reference in new issue
Block a user