diff --git a/tests/test_doc_surfaces.py b/tests/test_doc_surfaces.py index 19cf72816..8de4cbc7d 100644 --- a/tests/test_doc_surfaces.py +++ b/tests/test_doc_surfaces.py @@ -78,6 +78,79 @@ class TestSkeletonReadsTheResultShape(unittest.TestCase): self.assertIn(key.value, result) +class InstallerOutputExample(unittest.TestCase): + """The page showed a run with no `Not installable:` line while the + RetroArch manifest left eleven entries out, and a count that the menu + example below it no longer shared.""" + + def test_run_and_menu_show_one_dated_run_of_the_manifest(self): + import install + + text = _page("installer.md") + intro, run = re.search(r"(?s)\n([^\n]*)\n\n```\n(Fetching file index.*?)```", text).groups() + self.assertRegex(intro, r"\d{4}-\d{2}-\d{2}", "an output example says when it was taken") + manifest = json.loads((ROOT / "install" / "retroarch.json").read_text(encoding="utf-8")) + omitted = re.search(r"Not installable: \d+ files? \(\d+ required, (.*)\)\.", run) + if manifest.get("omitted_files"): + self.assertIsNotNone(omitted, "the manifest omits files and the run shows no line for it") + if omitted: + for part in omitted.group(1).split(", "): + self.assertIn(part.rsplit(":", 1)[0], install.OMISSION_REASONS.values()) + count, size = re.search(r"\n (\d+) files \(([\d.]+ [KMG]B)\)", run).groups() + self.assertIn(f"0/{count} present", run) + menu = re.search(r"Retroarch: (\d+) files, ([\d.]+ [KMG]B)\.", text) + self.assertEqual(menu.groups(), (count, size)) + + +class ReleaseRefPromise(unittest.TestCase): + """RETROBIOS_REF was said to pin a reproducible file set, while every + release deletes the previous tags and large files come from one fixed + release whatever the ref.""" + + def test_the_page_says_what_a_tag_does_not_pin(self): + import install + + self.assertIn("/large-files/", install.RELEASE_URL) + self.assertIn("--cleanup-tag", _page("release-process.md")) + row = next( + line for line in _page("installer.md").splitlines() + if line.startswith("| `RETROBIOS_REF`") + ) + self.assertIn("`large-files`", row) + self.assertIn("next release", row) + for page in PUBLISHED: + for paragraph in page.read_text(encoding="utf-8").split("\n\n"): + if "RETROBIOS_REF" in paragraph: + self.assertNotIn("reproducible", paragraph, page.name) + + +class ReleaseCommitCarriesTheManifests(unittest.TestCase): + """The documented release commit left out install/*.json, which the + release pipeline regenerates: v2026.10.10 added three by hand.""" + + def test_the_commit_step_adds_what_the_pipeline_rewrites(self): + pipeline = (ROOT / "scripts" / "pipeline.py").read_text(encoding="utf-8") + self.assertIn('"--manifest"', pipeline) + block = _section(_page("release-process.md"), "Cutting a release") + add = next(line for line in block.splitlines() if line.startswith("git add ")) + self.assertIn("install/*.json", add.split()) + + +class PageOpenings(unittest.TestCase): + """Four pages opened by announcing what they cover.""" + + META = re.compile(r"(?i)\bthis (page|guide) (covers|documents|explains|describes|picks)\b|^Diagnosis guide") + + def test_no_page_announces_itself(self): + offenders = [ + f"{page.name}: {line[:80]}" + for page in PUBLISHED + for line in FENCED.sub("", page.read_text(encoding="utf-8")).splitlines() + if self.META.search(line) + ] + self.assertEqual(offenders, [], "\n".join(offenders)) + + class NoHedgedRules(unittest.TestCase): """The profiling guide and the verification discipline turned rules into wishes ("it's worth", "Ideally", "something to look into").""" @@ -97,6 +170,22 @@ class NoHedgedRules(unittest.TestCase): self.assertEqual(offenders, [], "\n".join(offenders)) +class ExitStatusTable(unittest.TestCase): + """Code 2 was documented as 'every file failed' only, while argparse + exits 2 on every refused option, and the 141 example named an option + that does not exist.""" + + def test_codes_and_options_are_the_installers(self): + source = (ROOT / "install.py").read_text(encoding="utf-8") + section = _section(_page("installer.md"), "Exit status") + documented = {int(row[0].strip("`")) for row in _table_rows(section)} + raised = {int(code) for code in re.findall(r"sys\.exit\((\d+)\)", source)} + self.assertEqual(documented, raised | {2}) + self.assertIn("install.py: error:", section) + for option in set(re.findall(r"(--[a-z][a-z-]+)", section)): + self.assertIn(f'"{option}"', source, option) + + class ProfileFieldsRequired(unittest.TestCase): """The table marked source, profiled_date, core_version and systems as required and called the schema authoritative, which requires none.""" @@ -115,5 +204,19 @@ class ProfileFieldsRequired(unittest.TestCase): self.assertEqual(marked, required) +class PythonFloorOnRetroPie(unittest.TestCase): + """The page offered `apt install python3` to a RetroPie 4.8 image, where + python3 is 3.7.3 and apt has nothing newer.""" + + def test_both_pages_name_the_case(self): + install_sh = (ROOT / "install.sh").read_text(encoding="utf-8") + self.assertIn("(3, 8)", install_sh) + first = _section(_page("first-install.md"), "Error: Python 3.8 or newer is required.") + self.assertIn("RetroPie 4.8", first) + self.assertIn("3.7.3", first) + changing = _section(_page("installer.md"), "Changing the installer") + self.assertIn("3.7.3", changing) + + if __name__ == "__main__": unittest.main() diff --git a/wiki/first-install.md b/wiki/first-install.md index f3781b7bb..71f39e5a6 100644 --- a/wiki/first-install.md +++ b/wiki/first-install.md @@ -1,8 +1,7 @@ # First install One path, from an empty BIOS folder to files the emulator can read. Every step -shows what the screen looks like when it worked, so there is never a moment of -wondering. +shows what the screen looks like when it worked. If something on this page does not match what happens, the last section covers the cases that come up. @@ -154,11 +153,17 @@ its BIOS folder, see [Getting started](getting-started.md). ### Error: Python 3.8 or newer is required. -Python is missing from the machine. The message names the command that installs -it, `sudo apt install python3` on Debian and Ubuntu. On a handheld running Batocera or Recalbox it is already there, so +No Python 3.8 or newer was found: Python is missing, or older. The message +names the command that installs it, `sudo apt install python3` on Debian and +Ubuntu. On a handheld running Batocera or Recalbox it is already there, so this message usually means the command ran on the computer rather than on the console. +A stock RetroPie 4.8 image is Debian Buster, where `python3` is 3.7.3 and apt +has nothing newer, so the same command changes nothing. The RetroPie pack from +the [releases page](https://github.com/Abdess/retrobios/releases/latest), +extracted into `~/RetroPie/BIOS/`, installs the same files. + ### Error: the downloaded installer does not match its expected fingerprint. Nothing was run and nothing was written. The download did not match the diff --git a/wiki/installer.md b/wiki/installer.md index d646ce5c3..b32726eaa 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -100,14 +100,15 @@ macOS. rest. 7. Copies into standalone emulator directories, only with `--standalone-copies`. -A run looks like this: +A RetroArch run into an empty folder printed this on 2026-10-21: ``` Fetching file index for retroarch... - 5865 files (5.7 GB) + 8069 files (6.5 GB) + Not installable: 11 files (1 required, not found: 11). Checking existing files... - 0/5865 present (0 verified, 0 wrong hash) + 0/8069 present (0 verified, 0 wrong hash) ``` When the manifest leaves entries out (a file the collection cannot supply, or @@ -122,9 +123,9 @@ By default the installer fetches everything the platform runs. At a terminal, after detection, it asks: ``` -Retroarch: 5865 files, 5.7 GB. +Retroarch: 8069 files, 6.5 GB. Enter) install everything - i) only what the installed cores read: 102 files, 44.5 MB + i) only what the installed cores read: 100 files, 44.2 MB c) choose systems, cores and regions q) quit ``` @@ -246,7 +247,7 @@ Files the list does not name are never read, moved or deleted. | Variable | Effect | |----------|--------| -| `RETROBIOS_REF` | Branch or tag the file list and the payloads are read from. Defaults to `main`, and a release tag pins a reproducible file set | +| `RETROBIOS_REF` | Branch or tag the file list and the files committed to the repository are read from. Defaults to `main`. Files the manifest marks `release_asset` always come from the `large-files` release, whatever the ref, and that release replaces an asset in place when it is rebuilt. Each release deletes the previous release tags ([release process](release-process.md#cutting-a-release)), so a tag holds a file list until the next release | | `RETROBIOS_BASE_URL` | Base URL for the manifest and the files it declares. HTTPS only, plain HTTP allowed to loopback for end-to-end tests. Whoever controls this base controls the expected hashes, hence the check | | `RETROBIOS_OS` | Names the host outright (`linux`, `wsl`, `windows`, `darwin`, `android`) instead of detecting it | | `EXTERNAL_STORAGE` | Android only, the shared storage root RetroArch builds its default directories from. Defaults to `/storage/emulated/0` | @@ -467,8 +468,12 @@ followed. ## Changing the installer -`install.py` runs on the user's machine, often an older handheld or a stock -RetroPie image, so it stays on Python 3.8 and the standard library alone. +`install.py` runs on the user's machine, often an older handheld, so it stays +on Python 3.8 and the standard library alone. The RetroPie 4.8 images +(`retropie-buster-4.8-*.img.gz`) are Debian Buster, whose `python3` is 3.7.3: +there the bootstrap stops before running anything, and the RetroPie pack from +the releases page is the way to install +([first install](first-install.md#error-python-38-or-newer-is-required)). Tests refuse any 3.9 syntax or API, and one runs the installer end to end under a real 3.8 interpreter, which CI installs beside 3.12 so the check cannot skip. The build scripts need 3.12 and are not bound by this. @@ -487,9 +492,9 @@ fields, the installer refuses to narrow rather than install everything. |------|---------| | `0` | everything asked for is installed or already up to date | | `1` | a failure: the one-liner could not download or verify `install.py`, or found no Python 3.8 (nothing was run), an unusable base URL, an unknown platform or target, `--installed-cores` with no core found, a manifest that fails validation, a fetch that never succeeded, no platform to install for in a non-interactive run, some files failed while others installed, or `--check` found files to fetch | -| `2` | nothing was installed: every file the run tried failed (a BIOS folder that cannot be written, for one) | +| `2` | nothing was installed: every file the run tried failed (a BIOS folder that cannot be written, for one). Also an option the installer refused before any download: an unknown option, `--jobs` outside 1 to 32, `--target` with `--every-board`. The message then starts with `install.py: error:` | | `130` | stopped with Ctrl+C. Run the same command again to carry on | -| `141` | the output was piped to a reader that closed early (`--list \| head`) | +| `141` | the output was piped to a reader that closed early (`--list-platforms \| head`) | `install.ps1` passes the installer's code on unchanged: run as a file it exits with it, run through `irm \| iex` it sets `$LASTEXITCODE` (an `exit` there diff --git a/wiki/release-process.md b/wiki/release-process.md index 043b9f3e7..18d17ed85 100644 --- a/wiki/release-process.md +++ b/wiki/release-process.md @@ -1,11 +1,11 @@ # Release process -This page documents the CI/CD pipeline: what each workflow does, how releases -are built, and how to run the process manually. +Two GitHub workflows check pull requests and publish the site. Releases are +built, checked and uploaded from the maintainer's machine. ## CI workflows overview -The project uses 2 GitHub Actions workflows. All use only official GitHub +Both workflows use only official GitHub actions (`actions/checkout`, `actions/setup-python`, `actions/upload-pages-artifact`, `actions/deploy-pages`). No third-party actions. @@ -237,8 +237,12 @@ python scripts/check_release.py --tag "v${DATE}" --sums dist/SHA256SUMS.txt \ && gh release edit "v${DATE}" --draft=false --latest # The record and the pages it feeds go to main with the release, so the -# table never describes a pack other than the one the link serves. -git add release.json README.md mkdocs.yml +# table never describes a pack other than the one the link serves. The +# install manifests go too: step 1 regenerated them, and their pack_files +# and pack_size describe the packs just built. git status names anything +# else the run rewrote. +git status --short +git add release.json README.md mkdocs.yml install/*.json install/targets/*.json git commit -m "chore: record release v${DATE}" && git push # 6. Keep only the new release plus large-files: an older pack carries hashes diff --git a/wiki/troubleshooting.md b/wiki/troubleshooting.md index 5cba726fc..e7face00b 100644 --- a/wiki/troubleshooting.md +++ b/wiki/troubleshooting.md @@ -1,7 +1,5 @@ # Troubleshooting -Diagnosis guide organized by symptom. Each section describes what to check and how to fix it. - ## Game won't start / black screen Most launch failures are caused by a missing or incorrect BIOS file. @@ -280,9 +278,9 @@ The one-line bootstrap verifies the downloaded installer before running it, and the installer reads its manifests from the same revision. A platform name is checked before anything is downloaded, so a "No file list" message comes from the revision: `RETROBIOS_REF` naming a tag that does not exist, or one older -than the platform. Unset it, or pick a tag from the releases page. -`RETROBIOS_REF` pins an installation to a published tag when a reproducible -file set matters more than the newest one. +than the platform. Unset it, or pick a tag from the releases page. What a tag +holds and for how long is under +[environment overrides](installer.md#environment-overrides). ### Manual pack not found in release