mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 12:19:13 -05:00
docs: point users to --check and exact pack files
Troubleshooting, the FAQ and the download page sent people who only ran the installer to verify.py, which reads the repository, not their BIOS folder. Download links now name the exact asset of the recorded release, the --dest example names the BIOS folder on the card, the slice layout left with the last release that used it, and the shown verify output is what the tool prints.
This commit is contained in:
1 parent
621f4fcdee
commit
6ee138a819
12 files changed
+486
-170
No files matched your search
+2
-3
@@ -50,13 +50,12 @@ pip install pyyaml "jsonschema[format-nongpl]" markdown
|
||||
python scripts/validate_pr.py --changed # the BIOS files the branch adds
|
||||
python scripts/validate_schemas.py --source-only # profiles and platform files
|
||||
python scripts/profile_sync.py --emulator NAME --strict # each profile the branch adds
|
||||
python scripts/refresh_data_dirs.py # data directories the pack-building tests read
|
||||
python -m unittest discover -s tests -t .
|
||||
```
|
||||
|
||||
The full pipeline is not needed for a contribution: it rebuilds every pack
|
||||
(about 35 GB in `dist/`) and needs the data directories fetched first
|
||||
(`python scripts/refresh_data_dirs.py`), which is why maintainers run it
|
||||
before a release.
|
||||
in `dist/`, which is why maintainers run it before a release.
|
||||
|
||||
## PR validation
|
||||
|
||||
|
||||
@@ -24,8 +24,9 @@ irm https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1 | iex
|
||||
# Android (Termux, after pkg install python and termux-setup-storage)
|
||||
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh
|
||||
|
||||
# Handheld (SD card mounted on PC)
|
||||
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --platform retroarch --dest /path/to/sdcard
|
||||
# SD card mounted on a PC: --dest is the BIOS folder on the card
|
||||
# (RetroArch on Switch: retroarch/cores/system, on Android: RetroArch/system)
|
||||
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --platform retroarch --dest /path/to/sdcard/retroarch/cores/system
|
||||
```
|
||||
|
||||
The script auto-detects your platform, downloads only missing files, and verifies checksums.
|
||||
@@ -35,27 +36,24 @@ The script auto-detects your platform, downloads only missing files, and verifie
|
||||
One pack per platform, and it holds everything the platform runs: its own BIOS list plus every file its emulator cores load. Pick your platform, download the ZIP, extract to the BIOS path. The installer above does the same file by file. At a terminal it asks whether to install everything or to choose systems, cores and regions, and `--system`, `--core`, `--region` and `--target` make the same choice without questions. For a bare minimum, build your own pack below.
|
||||
Files is how many the pack holds once extracted, the figure a file manager shows for the folder. It differs from one platform to the next because a pack carries only what that platform's emulators load. The size is what the files occupy once extracted, and the ZIP itself downloads smaller.
|
||||
|
||||
A pack over 2 GB comes in several parts, and every part is needed. How to open them depends on their name:
|
||||
|
||||
- `Pack.part1of2.zip`, `Pack.part2of2.zip`: each part is an ordinary ZIP. Extract them all into the same folder.
|
||||
- `Pack.zip.001`, `Pack.zip.002` (releases up to v2026.09.04): slices of one ZIP, none of which opens on its own. Put them in one folder and open the `.001` with 7-Zip or PeaZip, or join them first with `cat Pack.zip.0* > Pack.zip` on Linux and macOS, `cmd /c copy /b Pack.zip.001+Pack.zip.002 Pack.zip` on Windows.
|
||||
A pack over 2 GB comes in several parts (`Pack.part1of2.zip`, `Pack.part2of2.zip`), and every part is needed. Each part is an ordinary ZIP: extract them all into the same folder.
|
||||
|
||||
Every release ships `SHA256SUMS.txt` and a detached signature of it, checkable against `allowed_signers` in this repository: [verifying a release](https://abdess.github.io/retrobios/wiki/release-process/#verifying-a-release).
|
||||
|
||||
| Platform | Files | Extracted size | Extract to | Download |
|
||||
|----------|------:|---------------:|-----------|----------|
|
||||
| Batocera | 12,810 | 7.5 GB | `/userdata/bios/` | [Download](../../releases/latest) |
|
||||
| BizHawk | 950 | 2.4 GB | `Firmware/` | [Download](../../releases/latest) |
|
||||
| EmuDeck | 620 | 3.2 GB | `~/Emulation/bios/` | [Download](../../releases/latest) |
|
||||
| Lakka | 8,504 | 6.0 GB | `/storage/system/` | [Download](../../releases/latest) |
|
||||
| MiSTer FPGA | 83 | 29 MB | `/media/fat/games/` | [Download](../../releases/latest) |
|
||||
| ROCKNIX | 5,804 | 6.1 GB | `/storage/roms/bios/` | [Download](../../releases/latest) |
|
||||
| Recalbox | 3,222 | 3.9 GB | `/recalbox/share/bios/` | [Download](../../releases/latest) |
|
||||
| RetroArch | 8,504 | 6.0 GB | `system/` | [Download](../../releases/latest) |
|
||||
| RetroBat | 12,399 | 7.9 GB | `bios/` | [Download](../../releases/latest) |
|
||||
| RetroDECK | 7,793 | 8.0 GB | `~/retrodeck/` | [Download](../../releases/latest) |
|
||||
| RetroPie * | 12,533 | 8.1 GB | `~/RetroPie/BIOS/` | [Download](../../releases/latest) |
|
||||
| RomM | 1,087 | 1.5 GB | `library/bios/` | [Download](../../releases/latest) |
|
||||
| Batocera | 12,810 | 7.5 GB | `/userdata/bios/` | Download: [part 1 of 3](../../releases/download/v2026.10.10/Batocera_43.1_BIOS_Pack.part1of3.zip), [part 2 of 3](../../releases/download/v2026.10.10/Batocera_43.1_BIOS_Pack.part2of3.zip), [part 3 of 3](../../releases/download/v2026.10.10/Batocera_43.1_BIOS_Pack.part3of3.zip) |
|
||||
| BizHawk | 950 | 2.4 GB | `Firmware/` | [Download](../../releases/download/v2026.10.10/BizHawk_2.11.1_BIOS_Pack.zip) |
|
||||
| EmuDeck | 620 | 3.2 GB | `~/Emulation/bios/` | [Download](../../releases/download/v2026.10.10/EmuDeck_2.7EA_BIOS_Pack.zip) |
|
||||
| Lakka | 8,504 | 6.0 GB | `/storage/system/` | Download: [part 1 of 2](../../releases/download/v2026.10.10/RetroArch_Lakka_v1.22.2_BIOS_Pack.part1of2.zip), [part 2 of 2](../../releases/download/v2026.10.10/RetroArch_Lakka_v1.22.2_BIOS_Pack.part2of2.zip) |
|
||||
| MiSTer FPGA | 83 | 29 MB | `/media/fat/games/` | [Download](../../releases/download/v2026.10.10/MiSTer_FPGA_2026-09-29_BIOS_Pack.zip) |
|
||||
| ROCKNIX | 5,804 | 6.1 GB | `/storage/roms/bios/` | Download: [part 1 of 2](../../releases/download/v2026.10.10/ROCKNIX_20261001_BIOS_Pack.part1of2.zip), [part 2 of 2](../../releases/download/v2026.10.10/ROCKNIX_20261001_BIOS_Pack.part2of2.zip) |
|
||||
| Recalbox | 3,222 | 3.9 GB | `/recalbox/share/bios/` | Download: [part 1 of 2](../../releases/download/v2026.10.10/Recalbox_10.1.1_BIOS_Pack.part1of2.zip), [part 2 of 2](../../releases/download/v2026.10.10/Recalbox_10.1.1_BIOS_Pack.part2of2.zip) |
|
||||
| RetroArch | 8,504 | 6.0 GB | `system/` | Download: [part 1 of 2](../../releases/download/v2026.10.10/RetroArch_Lakka_v1.22.2_BIOS_Pack.part1of2.zip), [part 2 of 2](../../releases/download/v2026.10.10/RetroArch_Lakka_v1.22.2_BIOS_Pack.part2of2.zip) |
|
||||
| RetroBat | 12,399 | 7.9 GB | `bios/` | Download: [part 1 of 3](../../releases/download/v2026.10.10/RetroBat_8.2.1_BIOS_Pack.part1of3.zip), [part 2 of 3](../../releases/download/v2026.10.10/RetroBat_8.2.1_BIOS_Pack.part2of3.zip), [part 3 of 3](../../releases/download/v2026.10.10/RetroBat_8.2.1_BIOS_Pack.part3of3.zip) |
|
||||
| RetroDECK | 7,793 | 8.0 GB | `~/retrodeck/` | Download: [part 1 of 3](../../releases/download/v2026.10.10/RetroDECK_0.10.10b_BIOS_Pack.part1of3.zip), [part 2 of 3](../../releases/download/v2026.10.10/RetroDECK_0.10.10b_BIOS_Pack.part2of3.zip), [part 3 of 3](../../releases/download/v2026.10.10/RetroDECK_0.10.10b_BIOS_Pack.part3of3.zip) |
|
||||
| RetroPie * | 12,533 | 8.1 GB | `~/RetroPie/BIOS/` | Download: [part 1 of 3](../../releases/download/v2026.10.10/RetroPie_4.8.12_BIOS_Pack.part1of3.zip), [part 2 of 3](../../releases/download/v2026.10.10/RetroPie_4.8.12_BIOS_Pack.part2of3.zip), [part 3 of 3](../../releases/download/v2026.10.10/RetroPie_4.8.12_BIOS_Pack.part3of3.zip) |
|
||||
| RomM | 1,087 | 1.5 GB | `library/bios/` | [Download](../../releases/download/v2026.10.10/RomM_5.3.1_BIOS_Pack.zip) |
|
||||
|
||||
The RetroDECK pack already carries its own `bios/` folder, so it extracts into `~/retrodeck/` rather than into the BIOS folder.
|
||||
|
||||
@@ -124,7 +122,8 @@ python scripts/generate_pack.py --platform retroarch --region us --one-per-slot
|
||||
python scripts/generate_pack.py --list-emulators
|
||||
python scripts/generate_pack.py --list-systems
|
||||
|
||||
# Verify your BIOS collection
|
||||
# Coverage of this repository's collection per platform, emulator or system
|
||||
# (it reads database.json and bios/: for your own BIOS folder, run the installer with --check)
|
||||
python scripts/verify.py --all
|
||||
python scripts/verify.py --platform batocera
|
||||
python scripts/verify.py --emulator flycast
|
||||
@@ -180,4 +179,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 [FAQ](https://abdess.github.io/retrobios/wiki/faq/#is-this-legal).
|
||||
|
||||
*Auto-generated on 2026-10-20T20:53:07Z*
|
||||
*Auto-generated on 2026-10-21T02:26:05Z*
|
||||
+40
-18
@@ -124,6 +124,34 @@ def release_totals(platform_name: str, record: dict) -> tuple[int | None, int |
|
||||
return pack["files"], pack["extracted_size"]
|
||||
|
||||
|
||||
def download_links(
|
||||
platform_name: str, record: dict, repo: str, label: str, attrs: str = ""
|
||||
) -> str:
|
||||
"""Markdown links to the files of a platform's pack in the recorded release.
|
||||
|
||||
A pack published whole gets one link labelled *label*. A pack in parts
|
||||
gets one link per part, since every part is needed. *repo* is the
|
||||
repository URL, absolute or relative, and *attrs* a MkDocs attribute list
|
||||
put after each link. Without a pack in the record the link falls back to
|
||||
the latest release page.
|
||||
"""
|
||||
pack = release_record.pack_for(platform_name, record)
|
||||
tag = record.get("tag")
|
||||
assets = pack.get("assets") if pack else None
|
||||
if not tag or not assets:
|
||||
return f"[{label}]({repo}/releases/latest){attrs}"
|
||||
urls = [f"{repo}/releases/download/{tag}/{asset}" for asset in assets]
|
||||
if len(urls) == 1:
|
||||
return f"[{label}]({urls[0]}){attrs}"
|
||||
parts = [
|
||||
f"[{'P' if attrs else 'p'}art {number} of {len(urls)}]({url}){attrs}"
|
||||
for number, url in enumerate(urls, 1)
|
||||
]
|
||||
if attrs:
|
||||
return " ".join(parts)
|
||||
return f"{label}: " + ", ".join(parts)
|
||||
|
||||
|
||||
def extract_targets(platforms_dir: str) -> list[tuple[str, str]]:
|
||||
"""(display name, folder) per registered platform, where its pack extracts.
|
||||
|
||||
@@ -182,7 +210,7 @@ def download_table(
|
||||
f"| {display} | {f'{files:,}' if files else '-'} |"
|
||||
f" {format_size(size) if size else '-'} |"
|
||||
f" {extract_paths.get(cov['platform'], '')} |"
|
||||
f" [Download]({RELEASE_URL}) |"
|
||||
f" {download_links(name, record, REPO_PATH, 'Download')} |"
|
||||
)
|
||||
return lines
|
||||
|
||||
@@ -219,7 +247,7 @@ def format_size(size: int) -> str:
|
||||
|
||||
|
||||
SITE_URL = "https://abdess.github.io/retrobios/"
|
||||
RELEASE_URL = "../../releases/latest"
|
||||
REPO_PATH = "../.."
|
||||
REPO = "Abdess/retrobios"
|
||||
|
||||
|
||||
@@ -365,8 +393,9 @@ def generate_readme(db: dict, platforms_dir: str) -> str:
|
||||
"# Android (Termux, after pkg install python and termux-setup-storage)",
|
||||
"curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh",
|
||||
"",
|
||||
"# Handheld (SD card mounted on PC)",
|
||||
"curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --platform retroarch --dest /path/to/sdcard",
|
||||
"# SD card mounted on a PC: --dest is the BIOS folder on the card",
|
||||
"# (RetroArch on Switch: retroarch/cores/system, on Android: RetroArch/system)",
|
||||
"curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --platform retroarch --dest /path/to/sdcard/retroarch/cores/system",
|
||||
"```",
|
||||
"",
|
||||
"The script auto-detects your platform, downloads only missing files, and verifies checksums.",
|
||||
@@ -381,16 +410,9 @@ def generate_readme(db: dict, platforms_dir: str) -> str:
|
||||
" The size is what the files occupy once extracted, and the ZIP itself"
|
||||
" downloads smaller.",
|
||||
"",
|
||||
"A pack over 2 GB comes in several parts, and every part is needed."
|
||||
" How to open them depends on their name:",
|
||||
"",
|
||||
"- `Pack.part1of2.zip`, `Pack.part2of2.zip`: each part is an ordinary"
|
||||
" ZIP. Extract them all into the same folder.",
|
||||
"- `Pack.zip.001`, `Pack.zip.002` (releases up to v2026.09.04): slices"
|
||||
" of one ZIP, none of which opens on its own. Put them in one folder"
|
||||
" and open the `.001` with 7-Zip or PeaZip, or join them first with"
|
||||
" `cat Pack.zip.0* > Pack.zip` on Linux and macOS,"
|
||||
" `cmd /c copy /b Pack.zip.001+Pack.zip.002 Pack.zip` on Windows.",
|
||||
"A pack over 2 GB comes in several parts (`Pack.part1of2.zip`,"
|
||||
" `Pack.part2of2.zip`), and every part is needed. Each part is an"
|
||||
" ordinary ZIP: extract them all into the same folder.",
|
||||
"",
|
||||
"Every release ships `SHA256SUMS.txt` and a detached signature of it,"
|
||||
" checkable against `allowed_signers` in this repository:"
|
||||
@@ -575,7 +597,8 @@ def generate_readme(db: dict, platforms_dir: str) -> str:
|
||||
"python scripts/generate_pack.py --list-emulators",
|
||||
"python scripts/generate_pack.py --list-systems",
|
||||
"",
|
||||
"# Verify your BIOS collection",
|
||||
"# Coverage of this repository's collection per platform, emulator or system",
|
||||
"# (it reads database.json and bios/: for your own BIOS folder, run the installer with --check)",
|
||||
"python scripts/verify.py --all",
|
||||
"python scripts/verify.py --platform batocera",
|
||||
"python scripts/verify.py --emulator flycast",
|
||||
@@ -734,13 +757,12 @@ pip install pyyaml "jsonschema[format-nongpl]" markdown
|
||||
python scripts/validate_pr.py --changed # the BIOS files the branch adds
|
||||
python scripts/validate_schemas.py --source-only # profiles and platform files
|
||||
python scripts/profile_sync.py --emulator NAME --strict # each profile the branch adds
|
||||
python scripts/refresh_data_dirs.py # data directories the pack-building tests read
|
||||
python -m unittest discover -s tests -t .
|
||||
```
|
||||
|
||||
The full pipeline is not needed for a contribution: it rebuilds every pack
|
||||
(about 35 GB in `dist/`) and needs the data directories fetched first
|
||||
(`python scripts/refresh_data_dirs.py`), which is why maintainers run it
|
||||
before a release.
|
||||
in `dist/`, which is why maintainers run it before a release.
|
||||
|
||||
## PR validation
|
||||
|
||||
|
||||
+49
-17
@@ -37,6 +37,7 @@ from common import (
|
||||
list_registered_platforms,
|
||||
load_database,
|
||||
load_emulator_profiles,
|
||||
load_platform_config,
|
||||
load_provenance_snapshots,
|
||||
native_layer,
|
||||
require_yaml,
|
||||
@@ -59,6 +60,7 @@ yaml = require_yaml()
|
||||
from generate_readme import (
|
||||
compute_coverage,
|
||||
contributing_text,
|
||||
download_links,
|
||||
extract_notes,
|
||||
extract_targets,
|
||||
release_totals,
|
||||
@@ -335,7 +337,7 @@ def generate_home(
|
||||
lines.append(
|
||||
f"| {logo_md} | [{display}](platforms/{name}.md) | "
|
||||
f"{_released_files(name)} | {mode_label} | "
|
||||
f"[Pack]({RELEASE_URL}){{ .md-button .md-button--primary }} |"
|
||||
f"{_pack_buttons(name)} |"
|
||||
)
|
||||
|
||||
lines.extend(
|
||||
@@ -448,6 +450,17 @@ def compute_stats(db: dict, coverages: dict, profiles: dict) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _pack_buttons(platform_name: str, label: str = "Pack") -> str:
|
||||
"""Buttons to the files of the platform's pack in the recorded release."""
|
||||
return download_links(
|
||||
platform_name,
|
||||
release_record.load_record(),
|
||||
REPO_URL,
|
||||
label,
|
||||
"{ .md-button .md-button--primary }",
|
||||
)
|
||||
|
||||
|
||||
def _released_files(platform_name: str) -> str:
|
||||
"""File count of the pack the latest release serves, as a table cell."""
|
||||
files, _size = release_totals(platform_name, release_record.load_record())
|
||||
@@ -1673,7 +1686,7 @@ def generate_platform_index(coverages: dict, registry: dict | None = None) -> st
|
||||
f"| [{display}]({name}.md) | "
|
||||
f"{cov['present']:,} | {_released_files(name)} | "
|
||||
f"{mode_html} | {status_html} | "
|
||||
f"[Pack]({RELEASE_URL}){{ .md-button .md-button--primary }} |"
|
||||
f"{_pack_buttons(name)} |"
|
||||
)
|
||||
|
||||
lines.extend(
|
||||
@@ -1914,8 +1927,7 @@ def generate_platform_page(
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
f"[Download {display} Pack]({RELEASE_URL})"
|
||||
"{ .md-button .md-button--primary }",
|
||||
_pack_buttons(name, f"Download {display} Pack"),
|
||||
"",
|
||||
]
|
||||
)
|
||||
@@ -4439,7 +4451,7 @@ def _mkdocs_config(docs: Path, text: str) -> tuple[Path, str]:
|
||||
def generate_which_pack() -> str:
|
||||
"""Generate the 'Which pack?' decision page."""
|
||||
rel = RELEASE_URL
|
||||
return f"""\
|
||||
return _link_pack_cells(f"""\
|
||||
# Download
|
||||
|
||||
Some retro consoles need firmware files (commonly called BIOS) to run games.
|
||||
@@ -4473,7 +4485,12 @@ irm https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1 | iex
|
||||
That is the complete default flow. Extra copies into detected standalone
|
||||
emulator directories are deliberately opt-in with `--standalone-copies`, so
|
||||
automatic setup never writes outside the selected platform tree unexpectedly.
|
||||
Use `python install.py --check` from a checkout for a read-only verification.
|
||||
Adding `--check` reads the BIOS folder, hashes what is there and writes
|
||||
nothing:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --check
|
||||
```
|
||||
|
||||
Options, environment overrides, how each platform is detected and what the
|
||||
installer is allowed to write: [Installer](wiki/installer.md).
|
||||
@@ -4487,16 +4504,9 @@ download it, and extract the files into the BIOS folder listed below.
|
||||
The metadata and website can be newer than that release: pack publication is
|
||||
manual and only happens after the release gates pass.
|
||||
|
||||
A pack over 2 GB comes in several parts, and every part is needed. How to
|
||||
open them depends on their name:
|
||||
|
||||
- `PackName.part1of2.zip`, `PackName.part2of2.zip`: each part is an ordinary
|
||||
ZIP. Extract them all into the same folder.
|
||||
- `PackName.zip.001`, `PackName.zip.002` (releases up to v2026.09.04): slices
|
||||
of one ZIP, none of which opens on its own. Put them in one folder and open
|
||||
the `.001` with 7-Zip or PeaZip, or join them first:
|
||||
- Linux/macOS: `cat PackName.zip.0* > PackName.zip`
|
||||
- Windows: `cmd /c copy /b PackName.zip.001+PackName.zip.002 PackName.zip`
|
||||
A pack over 2 GB comes in several parts (`PackName.part1of2.zip`,
|
||||
`PackName.part2of2.zip`), and every part is needed. Each part is an ordinary
|
||||
ZIP: extract them all into the same folder.
|
||||
|
||||
### Steam Deck
|
||||
|
||||
@@ -4591,7 +4601,29 @@ but standalone emulators may still need their BIOS path configured.
|
||||
If a game still asks for a missing file, check the
|
||||
[platforms section](platforms/index.md) for the full file list, or the
|
||||
[emulators section](emulators/index.md) for what each core expects.
|
||||
"""
|
||||
""")
|
||||
|
||||
|
||||
def _link_pack_cells(page: str) -> str:
|
||||
"""Point each table cell naming a platform's pack at that pack's files.
|
||||
|
||||
The cell names the platform by its display name, resolved through the
|
||||
registry, and the links come from the release record.
|
||||
"""
|
||||
record = release_record.load_record()
|
||||
keys = {
|
||||
load_platform_config(key, "platforms").get("platform", key): key
|
||||
for key in list_registered_platforms("platforms", include_archived=True)
|
||||
}
|
||||
cell = re.compile(rf"\| \[([^\]]+)\]\({re.escape(RELEASE_URL)}\) \|")
|
||||
|
||||
def link(match: re.Match[str]) -> str:
|
||||
label = match.group(1)
|
||||
if label not in keys:
|
||||
raise KeyError(f"no registered platform is displayed as {label!r}")
|
||||
return f"| {download_links(keys[label], record, REPO_URL, label)} |"
|
||||
|
||||
return cell.sub(link, page)
|
||||
|
||||
|
||||
def generate_mkdocs_nav(
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
"""What the README, the site and the wiki tell an end user.
|
||||
|
||||
Someone with a game that does not boot runs the one-line installer, not a
|
||||
clone of this repository: `scripts/verify.py` reads the collection, never
|
||||
their BIOS folder. A download link names the file to fetch, parts included,
|
||||
from the release the table describes. Output shown on a page is output the
|
||||
tool prints, and a size is the one the release record holds.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
import release_record # noqa: E402
|
||||
from generate_readme import ( # noqa: E402
|
||||
contributing_text,
|
||||
download_links,
|
||||
download_table,
|
||||
format_size,
|
||||
)
|
||||
|
||||
RECORD = release_record.load_record(REPO_ROOT / "release.json")
|
||||
REPO = "https://github.com/Abdess/retrobios"
|
||||
|
||||
SPLIT_RECORD = {
|
||||
"tag": "v2000.01.01",
|
||||
"packs": {
|
||||
"Batocera_1_BIOS_Pack.zip": {
|
||||
"files": 3,
|
||||
"extracted_size": 3 * 1024**3,
|
||||
"download_size": 2 * 1024**3,
|
||||
"assets": [
|
||||
"Batocera_1_BIOS_Pack.part1of2.zip",
|
||||
"Batocera_1_BIOS_Pack.part2of2.zip",
|
||||
],
|
||||
},
|
||||
"RomM_1_BIOS_Pack.zip": {
|
||||
"files": 1,
|
||||
"extracted_size": 1024**2,
|
||||
"download_size": 1024**2,
|
||||
"assets": ["RomM_1_BIOS_Pack.zip"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _read(relative: str) -> str:
|
||||
return (REPO_ROOT / relative).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _section(text: str, heading: str) -> str:
|
||||
"""The text under a markdown heading, up to the next heading of its level."""
|
||||
level = len(heading) - len(heading.lstrip("#"))
|
||||
start = text.index(heading + "\n")
|
||||
rest = text[start + len(heading) + 1:]
|
||||
stop = re.search(rf"^#{{1,{level}}} ", rest, re.MULTILINE)
|
||||
return rest[: stop.start()] if stop else rest
|
||||
|
||||
|
||||
def _which_pack() -> str:
|
||||
import generate_site
|
||||
|
||||
with contextlib.chdir(REPO_ROOT):
|
||||
return generate_site.generate_which_pack()
|
||||
|
||||
|
||||
class DownloadLinksNameTheFile(unittest.TestCase):
|
||||
def test_a_whole_pack_links_its_asset(self):
|
||||
self.assertEqual(
|
||||
download_links("romm", SPLIT_RECORD, "../..", "Download"),
|
||||
"[Download](../../releases/download/v2000.01.01/RomM_1_BIOS_Pack.zip)",
|
||||
)
|
||||
|
||||
def test_a_split_pack_links_every_part(self):
|
||||
cell = download_links("batocera", SPLIT_RECORD, "../..", "Download")
|
||||
for part in ("part1of2", "part2of2"):
|
||||
self.assertIn(
|
||||
f"releases/download/v2000.01.01/Batocera_1_BIOS_Pack.{part}.zip", cell
|
||||
)
|
||||
self.assertIn("part 1 of 2", cell)
|
||||
|
||||
def test_buttons_carry_their_attributes_on_each_part(self):
|
||||
cell = download_links("batocera", SPLIT_RECORD, REPO, "Pack", " { .x }")
|
||||
self.assertEqual(cell.count("{ .x }"), 2)
|
||||
|
||||
def test_without_a_pack_the_link_is_the_release_page(self):
|
||||
self.assertEqual(
|
||||
download_links("bizhawk", SPLIT_RECORD, "../..", "Download"),
|
||||
"[Download](../../releases/latest)",
|
||||
)
|
||||
|
||||
def test_the_readme_table_links_each_recorded_pack(self):
|
||||
coverages = {
|
||||
"batocera": {"platform": "Batocera"},
|
||||
"romm": {"platform": "RomM"},
|
||||
}
|
||||
rows = download_table(coverages, set(), {}, SPLIT_RECORD)[2:]
|
||||
self.assertEqual(len(rows), 2)
|
||||
for row in rows:
|
||||
self.assertNotIn("releases/latest", row)
|
||||
self.assertIn("releases/download/v2000.01.01/", row)
|
||||
|
||||
def test_the_generated_readme_links_every_released_asset(self):
|
||||
if not RECORD.get("packs"):
|
||||
self.skipTest("release.json is not written")
|
||||
readme = _read("README.md")
|
||||
for pack in RECORD["packs"].values():
|
||||
for asset in pack["assets"]:
|
||||
with self.subTest(asset=asset):
|
||||
self.assertIn(
|
||||
f"../../releases/download/{RECORD['tag']}/{asset})", readme
|
||||
)
|
||||
|
||||
def test_the_download_page_rows_link_assets(self):
|
||||
if not RECORD.get("packs"):
|
||||
self.skipTest("release.json is not written")
|
||||
page = _which_pack()
|
||||
rows = [line for line in page.splitlines() if line.startswith("| ")]
|
||||
self.assertTrue(rows)
|
||||
for row in rows:
|
||||
self.assertNotIn("releases/latest", row)
|
||||
for pack in RECORD["packs"].values():
|
||||
for asset in pack["assets"]:
|
||||
self.assertIn(f"/releases/download/{RECORD['tag']}/{asset})", page)
|
||||
|
||||
|
||||
class SlicesLeaveWithTheirRelease(unittest.TestCase):
|
||||
"""Parts named `.zip.001` were cut by v2026.09.04, deleted since. A page
|
||||
explaining a layout no release serves buries the one the user has."""
|
||||
|
||||
PAGES = ("README.md", "wiki/getting-started.md", "wiki/troubleshooting.md")
|
||||
|
||||
def setUp(self):
|
||||
assets = [a for p in RECORD.get("packs", {}).values() for a in p["assets"]]
|
||||
if any(re.search(r"\.zip\.\d{3}$", a) for a in assets):
|
||||
self.skipTest("the recorded release is published in slices")
|
||||
|
||||
def test_user_pages_name_one_layout(self):
|
||||
for page in self.PAGES:
|
||||
with self.subTest(page=page):
|
||||
self.assertNotIn(".zip.001", _read(page))
|
||||
self.assertNotIn("copy /b", _read(page))
|
||||
|
||||
def test_the_download_page_names_one_layout(self):
|
||||
self.assertNotIn(".zip.001", _which_pack())
|
||||
|
||||
|
||||
class EndUsersCheckTheirOwnFolder(unittest.TestCase):
|
||||
"""verify.py reads database.json and bios/. Advice about a user's own
|
||||
files points at the installer's --check, which hashes their folder."""
|
||||
|
||||
USER_SECTIONS = (
|
||||
("wiki/troubleshooting.md", "## Game won't start / black screen"),
|
||||
("wiki/faq.md", "## My game shows a black screen"),
|
||||
("wiki/faq.md", "## How do I know which BIOS I need?"),
|
||||
("wiki/getting-started.md", "## Checking what you installed"),
|
||||
)
|
||||
|
||||
def test_user_sections_never_send_to_verify(self):
|
||||
for page, heading in self.USER_SECTIONS:
|
||||
with self.subTest(page=page, heading=heading):
|
||||
section = _section(_read(page), heading)
|
||||
self.assertNotIn("verify.py", section)
|
||||
self.assertNotIn("python install.py --check", section)
|
||||
|
||||
def test_the_download_page_gives_the_one_liner_check(self):
|
||||
page = _which_pack()
|
||||
self.assertNotIn("from a checkout", page)
|
||||
self.assertIn("install.sh | sh -s -- --check", page)
|
||||
|
||||
def test_the_readme_does_not_call_verify_a_check_of_your_bios(self):
|
||||
self.assertNotIn("Verify your BIOS collection", _read("README.md"))
|
||||
|
||||
|
||||
class DestIsTheBiosFolder(unittest.TestCase):
|
||||
"""--dest is used as the BIOS folder as given (install.py), so the root
|
||||
of a card puts every file beside the folder the frontend reads."""
|
||||
|
||||
PAGES = (
|
||||
"README.md",
|
||||
"wiki/getting-started.md",
|
||||
"wiki/installer.md",
|
||||
"wiki/faq.md",
|
||||
"wiki/first-install.md",
|
||||
)
|
||||
|
||||
def test_no_example_installs_to_a_card_root(self):
|
||||
for page in self.PAGES:
|
||||
for dest in re.findall(r"--dest\s+(\S+)", _read(page)):
|
||||
with self.subTest(page=page, dest=dest):
|
||||
self.assertFalse(dest.rstrip("/").endswith("sdcard"), dest)
|
||||
|
||||
|
||||
class ShownOutputIsPrintedOutput(unittest.TestCase):
|
||||
"""Every line in the troubleshooting page's verify samples has a shape
|
||||
verify.py prints, and the shapes are read from its source."""
|
||||
|
||||
SHAPES = (
|
||||
(r"^\S.*: \d+/\d+ (OK|present)\b.*\[(md5|sha1|existence)\]$", "}: {', '.join(parts)} [{mode}]"),
|
||||
(r"^ (OK|UNTESTED|MISSING) \((required|optional)[^)]*\): \S+/\S+", '" UNTESTED ({req}{hle}): {key}: {reason}"'),
|
||||
(r"^ DISCREPANCY: \S+/\S+: ", '" DISCREPANCY: {key}: {disc}"'),
|
||||
(r"^ Core files: \d+ in pack, \d+ required missing, \d+ optional missing$", '" Core files: {core_in_pack} in pack'),
|
||||
(r"^ \S", None),
|
||||
)
|
||||
|
||||
def test_the_shapes_are_verify_output(self):
|
||||
source = _read("scripts/verify.py")
|
||||
for _pattern, literal in self.SHAPES:
|
||||
if literal:
|
||||
with self.subTest(literal=literal):
|
||||
self.assertIn(literal, source)
|
||||
|
||||
def test_samples_match_a_shape(self):
|
||||
section = _section(_read("wiki/troubleshooting.md"), "### Reading the output")
|
||||
blocks = re.findall(r"```\n(.*?)```", section, re.DOTALL)
|
||||
self.assertTrue(blocks)
|
||||
for block in blocks:
|
||||
for line in block.splitlines():
|
||||
with self.subTest(line=line):
|
||||
self.assertTrue(
|
||||
any(re.match(pattern, line) for pattern, _ in self.SHAPES), line
|
||||
)
|
||||
|
||||
def test_no_severity_column(self):
|
||||
self.assertNotIn("[CRITICAL]", _read("wiki/troubleshooting.md"))
|
||||
|
||||
|
||||
class SizesComeFromTheRecord(unittest.TestCase):
|
||||
def test_the_quoted_pack_sizes_are_the_recorded_ones(self):
|
||||
text = _read("wiki/release-process.md")
|
||||
quoted = re.search(
|
||||
r"in (v[\d.]+),\s+(\w+) downloaded as ([\d.]+ GB) and extracted to"
|
||||
r"\s+([\d.]+ GB)",
|
||||
text,
|
||||
)
|
||||
self.assertIsNotNone(quoted, "the sizes are quoted with their release tag")
|
||||
tag, platform, download, extracted = quoted.groups()
|
||||
if tag != RECORD.get("tag"):
|
||||
self.skipTest(f"release.json records {RECORD.get('tag')}, not {tag}")
|
||||
pack = release_record.pack_for(platform, RECORD)
|
||||
self.assertEqual(download, format_size(pack["download_size"]))
|
||||
self.assertEqual(extracted, format_size(pack["extracted_size"]))
|
||||
|
||||
|
||||
class ContributorChecksMatchCi(unittest.TestCase):
|
||||
"""validate.yml fetches the data directories before the suite: the tests
|
||||
that build packs exit non-zero without them."""
|
||||
|
||||
def test_the_pr_checklist_fetches_data_directories_before_the_tests(self):
|
||||
for text in (
|
||||
contributing_text(lambda page: page, "# C", "here"),
|
||||
_read("CONTRIBUTING.md"),
|
||||
):
|
||||
block = _section(text, "## Before opening a PR")
|
||||
self.assertIn("python scripts/refresh_data_dirs.py", block)
|
||||
self.assertLess(
|
||||
block.index("refresh_data_dirs.py"), block.index("unittest discover")
|
||||
)
|
||||
|
||||
def test_ci_does_the_same(self):
|
||||
workflow = _read(".github/workflows/validate.yml")
|
||||
self.assertLess(
|
||||
workflow.index("python scripts/refresh_data_dirs.py"),
|
||||
workflow.index("python -m unittest discover"),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -212,13 +212,12 @@ class PartsAreExplainedWhereTheyAreDownloaded(unittest.TestCase):
|
||||
def _text(self, relative: str) -> str:
|
||||
return (REPO_ROOT / relative).read_text(encoding="utf-8")
|
||||
|
||||
def test_every_page_names_both_layouts(self):
|
||||
"""The older release stays downloadable until the next one replaces it."""
|
||||
def test_every_page_names_the_parts(self):
|
||||
"""Slices left the pages with the last release that used them
|
||||
(test_doc_surfaces.SlicesLeaveWithTheirRelease)."""
|
||||
for page in self.PAGES:
|
||||
with self.subTest(page=page):
|
||||
text = self._text(page)
|
||||
self.assertTrue(".part1of2.zip" in text)
|
||||
self.assertTrue(".zip.001" in text)
|
||||
self.assertIn(".part1of2.zip", self._text(page))
|
||||
|
||||
def test_the_windows_join_command_runs_from_powershell(self):
|
||||
"""`copy /b A+B C` is a cmd builtin. Typed into PowerShell, the shell
|
||||
|
||||
+9
-9
@@ -89,16 +89,13 @@ pack cannot do that.
|
||||
|
||||
Two approaches:
|
||||
|
||||
1. **Run verify.py** for your platform. It lists every expected file with its hash and status.
|
||||
2. **Check the project site.** Each platform page lists all required and optional BIOS files per system.
|
||||
1. **Ask the installer.** `--dry-run` lists what it would install for your platform and writes nothing, `--list-systems` gives the file count per system, and `--emulator` narrows to what one core reads:
|
||||
|
||||
For a specific emulator core:
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --emulator beetle_psx --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
python scripts/verify.py --emulator beetle_psx --verbose
|
||||
```
|
||||
|
||||
The `--verbose` flag shows source references and expected values from the emulator's source code.
|
||||
2. **Check the project site.** Each platform page lists all required and optional BIOS files per system, and each emulator page the files that core loads, with the source references and expected values.
|
||||
|
||||
## Is piping the installer into a shell safe?
|
||||
|
||||
@@ -136,9 +133,12 @@ Yes, `--dest` takes any path and the one-liner forwards arguments:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh \
|
||||
| sh -s -- --platform retroarch --dest /path/to/sdcard
|
||||
| sh -s -- --platform retroarch --dest /path/to/sdcard/retroarch/cores/system
|
||||
```
|
||||
|
||||
`--dest` is the BIOS folder itself: on a card, `retroarch/cores/system` for
|
||||
RetroArch on Switch, `RetroArch/system` on Android.
|
||||
|
||||
On Windows the arguments need a script block, since `iex` would read them as
|
||||
its own. The form is on the [Installer](installer.md#passing-options) page.
|
||||
|
||||
|
||||
+16
-31
@@ -65,9 +65,13 @@ mounted on another machine:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh \
|
||||
| sh -s -- --platform retroarch --dest /path/to/sdcard
|
||||
| sh -s -- --platform retroarch --dest /path/to/sdcard/retroarch/cores/system
|
||||
```
|
||||
|
||||
`--dest` is the BIOS folder itself, so on a card it names that folder on the
|
||||
card: `retroarch/cores/system` for RetroArch on Switch, `RetroArch/system` on
|
||||
Android.
|
||||
|
||||
PowerShell needs another form, the environment overrides and the detection
|
||||
rules are listed per platform, and the trust boundary is described in full on
|
||||
the [Installer](installer.md) page.
|
||||
@@ -91,20 +95,10 @@ against the SHA-256 the release publishes before anything is extracted.
|
||||
2. Download the ZIP pack for your platform
|
||||
3. Extract to the BIOS directory listed below
|
||||
|
||||
A pack over 2 GB comes in several parts, and every part is needed. How to
|
||||
open them depends on their name:
|
||||
|
||||
- `Pack.part1of2.zip`, `Pack.part2of2.zip`: each part is an ordinary ZIP.
|
||||
Extract them all into the same folder.
|
||||
- `Pack.zip.001`, `Pack.zip.002` (releases up to v2026.09.04): slices of one
|
||||
ZIP, none of which opens on its own. Put them in one folder and open the
|
||||
`.001` with 7-Zip or PeaZip, or join them first:
|
||||
- Linux/macOS: `cat Pack.zip.0* > Pack.zip`
|
||||
- Windows: `cmd /c copy /b Pack.zip.001+Pack.zip.002 Pack.zip`
|
||||
|
||||
A frontend's own extractor may refuse a slice: Batocera answers `Archive
|
||||
type: '001' is not yet supported`. Join the slices from a shell there. See
|
||||
[Download](../which-pack.md) for the per-setup instructions.
|
||||
A pack over 2 GB comes in several parts (`Pack.part1of2.zip`,
|
||||
`Pack.part2of2.zip`), and every part is needed. Each part is an ordinary ZIP:
|
||||
extract them all into the same folder. See [Download](../which-pack.md) for
|
||||
the per-setup instructions.
|
||||
|
||||
## BIOS directory by platform
|
||||
|
||||
@@ -249,31 +243,22 @@ Checking existing files...
|
||||
whose contents the platform will reject, which is how a wrong revision or a bad
|
||||
dump shows up.
|
||||
|
||||
`scripts/verify.py` answers a different question, how complete this
|
||||
repository's collection is. It reads `database.json`, never your BIOS folder,
|
||||
and needs a clone.
|
||||
|
||||
The installer's `--check` hashes every file against the list on every
|
||||
platform, so it catches a wrong revision even where the frontend would not:
|
||||
RetroArch, Lakka and RetroPie only check that a file exists, while Batocera,
|
||||
RetroBat, Recalbox, EmuDeck, RetroDECK, RomM, ROCKNIX and MiSTer FPGA compare
|
||||
MD5 and BizHawk compares SHA1. To see which emulator reads a file and what it
|
||||
expects, `python scripts/verify.py --platform retroarch --verbose` lists the
|
||||
ground truth per core, for the collection rather than your folder.
|
||||
MD5 and BizHawk compares SHA1.
|
||||
|
||||
For a single system:
|
||||
For a single system, or the files a single emulator core reads (`--list-systems`
|
||||
and `--list-cores` give the names):
|
||||
|
||||
```bash
|
||||
python scripts/verify.py --system sony-playstation
|
||||
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --check --system sony-playstation
|
||||
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --check --emulator beetle_psx
|
||||
```
|
||||
|
||||
For a single emulator core:
|
||||
|
||||
```bash
|
||||
python scripts/verify.py --emulator beetle_psx
|
||||
```
|
||||
|
||||
See [Tools](tools.md) for the full CLI reference.
|
||||
`--verbose` names every file still to fetch, wrong hashes included. See [Installer](installer.md)
|
||||
for every option, and [Tools](tools.md) for the repository's own scripts.
|
||||
|
||||
## Next steps
|
||||
|
||||
|
||||
+4
-1
@@ -30,9 +30,12 @@ The shell bootstrap forwards everything after `-s --`:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh \
|
||||
| sh -s -- --platform retroarch --dest /path/to/sdcard
|
||||
| sh -s -- --platform retroarch --dest /path/to/sdcard/retroarch/cores/system
|
||||
```
|
||||
|
||||
`--dest` is the BIOS folder itself: on a card, `retroarch/cores/system` for
|
||||
RetroArch on Switch, `RetroArch/system` on Android.
|
||||
|
||||
PowerShell needs another form. `iex` takes the script text as its own argument,
|
||||
so anything appended to `irm ... | iex` is read as an argument to `iex` and
|
||||
rejected with `A positional parameter cannot be found`. Building a script block
|
||||
|
||||
@@ -298,8 +298,8 @@ release assets, since a lighter pack means a core that fails with no message.
|
||||
The release notes are written by `release_notes.py`: the install commands,
|
||||
the pack table and its parts sentence come from `release.json`, then what
|
||||
changed since the previous tag and the contributors of the closed issues,
|
||||
written by hand. A pack has two sizes and they are far apart: Batocera
|
||||
downloads as 2.4 GB and extracts to 4.0 GB. Step 4 prints both, and whichever
|
||||
written by hand. A pack has two sizes and they are far apart: in v2026.10.10,
|
||||
Batocera downloaded as 5.2 GB and extracted to 7.5 GB. Step 4 prints both, and whichever
|
||||
one the table carries, the header names it. Someone sizing a USB drive is
|
||||
reading that column. The README table is the extracted size of the released
|
||||
pack, from `release.json`. `SHA256SUMS.txt` lists the checksums of the files
|
||||
@@ -318,9 +318,10 @@ Until v2026.09.04 the parts were byte ranges cut by `split`, named
|
||||
`.zip.001`, and the sentence explaining them sat under the table. A range
|
||||
opened alone is not an archive and no tool says a part is missing, so five
|
||||
reports in six months took one for a broken download (issues 45, 51, 66, 77
|
||||
and 79). The README, the download page and the troubleshooting page describe
|
||||
both layouts for as long as a release cut that way is still published. Once
|
||||
step 6 has deleted it, the `.zip.001` paragraph leaves those three pages.
|
||||
and 79). The README, the download page and the troubleshooting page described
|
||||
both layouts for as long as a release cut that way was still published.
|
||||
v2026.09.04 has since been deleted by step 6, and the `.zip.001` paragraph
|
||||
left those pages.
|
||||
|
||||
The table carries a Files column, the count step 4 prints, and the
|
||||
notes never open on the size of the collection. That total covers every
|
||||
|
||||
@@ -46,7 +46,7 @@ Every module in `tests/`, by the area it guards.
|
||||
| Installer and downloads | loopback server | `test_check_release`, `test_download`, `test_install`, `test_install_cores`, `test_install_experience`, `test_install_selection` |
|
||||
| Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_diff`, `test_truth_layout` |
|
||||
| Scrapers, upstream and provenance | inline sources, synthetic | `test_check_freshness`, `test_dedup`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_profile_sync`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_scraper_contract`, `test_upstream`, `test_upstream_index` |
|
||||
| Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_optional_dependencies`, `test_prose_style`, `test_site_availability`, `test_site_cores`, `test_site_exports`, `test_site_platform_hashes`, `test_site_system_links`, `test_site_validation`, `test_site_write`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
|
||||
| Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_doc_surfaces`, `test_optional_dependencies`, `test_prose_style`, `test_site_availability`, `test_site_cores`, `test_site_exports`, `test_site_platform_hashes`, `test_site_system_links`, `test_site_validation`, `test_site_write`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` |
|
||||
|
||||
## Test architecture
|
||||
|
||||
|
||||
+60
-60
@@ -14,28 +14,34 @@ curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh |
|
||||
|
||||
It hashes what is there and writes nothing. Anything counted as needing
|
||||
downloading is absent, and a required file that is absent means the core cannot
|
||||
start games for that system at all.
|
||||
|
||||
`scripts/verify.py` answers a different question, how complete this
|
||||
repository's collection is. It reads `database.json`, never your BIOS folder,
|
||||
and needs a clone:
|
||||
start games for that system at all. `--system` narrows the check to one
|
||||
system (`--list-systems` names them):
|
||||
|
||||
```bash
|
||||
python scripts/verify.py --platform retroarch --verbose
|
||||
python scripts/verify.py --system sony-playstation
|
||||
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh -s -- --check --system sony-playstation --verbose
|
||||
```
|
||||
|
||||
### Check if the hash matches
|
||||
|
||||
Look for `untested` entries in the verify output. This means the file exists but
|
||||
its hash does not match the expected value. Common causes:
|
||||
The count `--check` prints says how many files are present with contents the
|
||||
platform does not accept:
|
||||
|
||||
```
|
||||
Checking existing files...
|
||||
1/109 present (0 verified, 1 wrong hash)
|
||||
```
|
||||
|
||||
With `--verbose` the list under it names each one (`wrong hash: scph5501.bin`).
|
||||
The check hashes every file, on RetroArch too, where the frontend itself only
|
||||
looks for the name. Common causes:
|
||||
|
||||
- Wrong region (a PAL BIOS instead of NTSC, or vice versa)
|
||||
- Wrong hardware revision (e.g. SCPH-5501 vs SCPH-1001 for PlayStation)
|
||||
- Corrupted download
|
||||
|
||||
Each system page on the site lists the expected hashes. Compare your file's
|
||||
MD5 or SHA1 against those values.
|
||||
Running the same command without `--check` replaces each file with a wrong
|
||||
hash by a verified copy. Each system page on the site also lists the expected
|
||||
hashes, to compare against your file's MD5 or SHA1 by hand.
|
||||
|
||||
### Wrong region BIOS
|
||||
|
||||
@@ -106,23 +112,26 @@ the ones it left out.
|
||||
|
||||
## Hash mismatch / UNTESTED
|
||||
|
||||
`verify.py` reports `UNTESTED` for a file.
|
||||
This section and the two after it are about this repository's own tools, run
|
||||
from a clone. `scripts/verify.py` checks the collection in `bios/` against each
|
||||
platform, never your BIOS folder: for that, use `--check` above.
|
||||
|
||||
The file exists and was hashed, but the computed hash doesn't match any expected
|
||||
value. This means you have a different version of the file than what the platform
|
||||
or emulator expects. The reason field shows the expected vs actual hash prefix.
|
||||
`verify.py` reports `UNTESTED` for a file of the collection that was hashed
|
||||
and does not match what the platform declares. The line gives the reason:
|
||||
|
||||
To find the correct version, check the system page on the site. It lists every
|
||||
known BIOS file with its expected MD5 and SHA1.
|
||||
```
|
||||
UNTESTED (required): nintendo-ds/dsifirmware.bin: declared hash contradicted by the local file
|
||||
```
|
||||
|
||||
The system page on the site lists every known version of the file with its
|
||||
expected MD5 and SHA1.
|
||||
|
||||
### Existence-mode platforms never report UNTESTED
|
||||
|
||||
On RetroArch, Lakka and RetroPie, verification only asks whether the file is
|
||||
there, so the only outcomes are OK and MISSING. A wrong-region or corrupt file
|
||||
still shows as OK. To find out whether the content is right on those platforms,
|
||||
run `verify.py --verbose`: it adds the emulator ground truth from the profiles
|
||||
and flags a `DISCREPANCY` when the file passes the platform check but fails the
|
||||
emulator's own size or hash validation.
|
||||
On RetroArch, Lakka and RetroPie, the platform check only asks whether the file
|
||||
is there, so the only outcomes are OK and MISSING. `verify.py` adds the emulator
|
||||
ground truth from the profiles and flags a `DISCREPANCY` when a file passes the
|
||||
platform check but fails the emulator's own size or hash validation.
|
||||
|
||||
### The .variants/ directory
|
||||
|
||||
@@ -140,8 +149,8 @@ A generated pack doesn't contain all the files you expected.
|
||||
|
||||
### Severity levels
|
||||
|
||||
`verify.py` assigns a severity to each issue. Not all missing files are equally
|
||||
important:
|
||||
`verify.py` assigns a severity to each issue, counted under `severity_counts`
|
||||
in its `--json` output. Not all missing files are equally important:
|
||||
|
||||
| Severity | Meaning | Action needed |
|
||||
|----------|---------|---------------|
|
||||
@@ -183,23 +192,33 @@ How to read and interpret `verify.py` output.
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| `ok` | File present, hash matches (or existence check passed) |
|
||||
| `untested` | File present, hash not confirmed against expected value |
|
||||
| `missing` | File not found in the repository |
|
||||
|
||||
Hash and size mismatches are reported as `untested` with a reason field
|
||||
showing expected vs actual values (e.g., `expected abc123... got def456...`).
|
||||
| `OK` | File present, hash matches (or existence check passed) |
|
||||
| `UNTESTED` | File present, hash not confirmed against the expected value |
|
||||
| `MISSING` | File not found in the repository |
|
||||
| `DISCREPANCY` | The platform check passes, an emulator's own check fails |
|
||||
|
||||
### Reading the output
|
||||
|
||||
Each line shows the file path, its status, and severity. In verbose mode, ground
|
||||
truth data from emulator profiles is appended, showing which cores reference the
|
||||
file and what validations they perform.
|
||||
The first line counts the platform's files and names its check. Under it come
|
||||
the entries that need attention, one per line as `STATUS (required|optional):
|
||||
system/file`, followed by a reason where there is one, then the counts of the
|
||||
files the emulator cores add. From `python scripts/verify.py --platform retrodeck`:
|
||||
|
||||
```
|
||||
scph5501.bin ok [OK]
|
||||
dc_boot.bin missing [CRITICAL]
|
||||
gba_bios.bin untested [WARNING]
|
||||
RetroDECK: 2007/2008 OK, 1 untested [md5]
|
||||
UNTESTED (required): nintendo-ds/dsifirmware.bin: declared hash contradicted by the local file
|
||||
DISCREPANCY: nintendo-snes/sgb2.boot.rom: RetroDECK says OK but supersnes9x says size mismatch: got 524288, accepted [256]
|
||||
supersnes9x [size]
|
||||
Core files: 1495 in pack, 0 required missing, 1 optional missing
|
||||
```
|
||||
|
||||
`--verbose` also lists the passing entries that an emulator validates, with
|
||||
the check and the source lines behind it:
|
||||
|
||||
```
|
||||
Recalbox: 351/351 OK [md5]
|
||||
OK (required): macintosh/MacII.ROM
|
||||
minivmac validates size=262144 [minivmac/cfg/CNFGRAPI.h:32, minivmac/src/OSGLUERETRO.c:781]
|
||||
```
|
||||
|
||||
### Cross-reference section
|
||||
@@ -307,26 +326,7 @@ Parts named `Pack.part1of2.zip` are ordinary ZIPs: extract each one into the
|
||||
same folder, and check that all of them were downloaded, the name says how
|
||||
many there are.
|
||||
|
||||
Parts named `Pack.zip.001`, `Pack.zip.002` (releases up to v2026.09.04) are
|
||||
slices of one ZIP. None opens on its own, and the error never says a part is
|
||||
missing: 7-Zip reports `Unavailable start of archive` on a `.002`, Windows
|
||||
has no program for the extension. Put every slice in one folder; 7-Zip and
|
||||
PeaZip open the `.001` directly. A frontend's own extractor may refuse it,
|
||||
Batocera among them:
|
||||
|
||||
```
|
||||
Archive type: '001' is not yet supported
|
||||
```
|
||||
|
||||
The slices are plain byte ranges, so joining them from a shell rebuilds the
|
||||
ZIP (`cmd /c copy /b Pack.zip.001+Pack.zip.002 Pack.zip` on Windows):
|
||||
|
||||
```bash
|
||||
cat Pack.zip.0* > Pack.zip
|
||||
unzip Pack.zip -d /userdata/bios/
|
||||
rm Pack.zip
|
||||
```
|
||||
|
||||
`scripts/download.sh` and `scripts/download.py` do this on their own, checksum
|
||||
included. Both stage inside the destination directory, never in `/tmp`, which
|
||||
is a RAM disk on most of these systems.
|
||||
`scripts/download.sh` and `scripts/download.py` download every part and check
|
||||
it against the release's checksums before extracting. Both stage inside the
|
||||
destination directory, never in `/tmp`, which is a RAM disk on most of these
|
||||
systems.
|
||||
Reference in new issue
Block a user