From b23b8f0a62a58bb58d49323364d6feb8239618b5 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:12:38 +0100 Subject: [PATCH 1/6] fix: find RetroArch-Win64 from WSL too --- install.ps1 | 2 +- install.py | 8 ++++---- install.sh | 2 +- tests/test_install.py | 15 +++++++++++++++ 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/install.ps1 b/install.ps1 index db72b7c21..729b6d2e4 100644 --- a/install.ps1 +++ b/install.ps1 @@ -16,7 +16,7 @@ # and Invoke-WebRequest throws on a cut transfer before the hash is read: # the site's build rewrites this line there (scripts/sitebootstrap.py). $mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that." - $defaultInstallSha256 = "403b6e8a7efcd24858b0f46ffa19c2b9012cdeef149768729d8e48d0b68a296a" + $defaultInstallSha256 = "87d93c52753fedb9739816c628ba9109f92f6c5cc0aa6e1c5c3c3d2252244666" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index c9894999b..eb8adfc7a 100644 --- a/install.py +++ b/install.py @@ -55,9 +55,9 @@ from collections.abc import Callable, Collection, Iterable, Iterator, Mapping from pathlib import Path, PurePosixPath from typing import Any, BinaryIO, ContextManager, NamedTuple, TextIO -# Manifests are read from the same ref the bootstrap verified this installer -# against, so a file list and the code reading it always come from one commit. -# RETROBIOS_REF pins an installation to a tag when reproducibility matters. +# File lists are read from main, the newest ones, whichever commit the +# installer came from. RETROBIOS_REF pins them to a tag or a commit when +# reproducibility matters. DEFAULT_RELEASE_REF = "main" RELEASE_REF = os.environ.get("RETROBIOS_REF", DEFAULT_RELEASE_REF) DEFAULT_BASE_URL = ( @@ -2647,7 +2647,7 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]: # default is \system (configuration.c, platform_win32.c). appdata = os.environ.get("APPDATA", "") appdata_cfg = Path(appdata) / "RetroArch" / "retroarch.cfg" if appdata else None - exe_dirs = _windows_retroarch_dirs() if os_type == "windows" else [] + exe_dirs = _windows_retroarch_dirs() for exe_dir in exe_dirs: ra_dir = _parse_retroarch_system_dir(exe_dir / "retroarch.cfg", exe_dir) if ra_dir is None and appdata_cfg is not None: diff --git a/install.sh b/install.sh index cacf12717..3e264dab6 100755 --- a/install.sh +++ b/install.sh @@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins # curl or wget stop on a cut transfer before the hash is read: the site's # build rewrites this line there (scripts/sitebootstrap.py). MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that." -DEFAULT_INSTALL_SHA256="403b6e8a7efcd24858b0f46ffa19c2b9012cdeef149768729d8e48d0b68a296a" +DEFAULT_INSTALL_SHA256="87d93c52753fedb9739816c628ba9109f92f6c5cc0aa6e1c5c3c3d2252244666" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/tests/test_install.py b/tests/test_install.py index 60d2fe8fe..6c5ffc1e7 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -3658,6 +3658,21 @@ class WindowsSeenFromWsl(unittest.TestCase): install._detect_by_path("wsl", found) self.assertIn(("retrobat", self.root / "c" / "RetroBat" / "bios"), found) + def test_retroarch_beside_its_exe_on_the_windows_side_is_detected(self): + """The installer page said both sides are searched under WSL, and + RetroArch's install folders were looked in on Windows only: a + C:\\RetroArch-Win64 was never found from WSL.""" + exe_dir = self.root / "c" / "RetroArch-Win64" + exe_dir.mkdir(parents=True) + (exe_dir / "retroarch.exe").write_bytes(b"MZ") + home = Path(_tempdir(self)) + cleared = {name: "" for name in (*install._WINDOWS_VARIABLES, "SCOOP", "SCOOP_GLOBAL", + "ProgramData")} + with unittest.mock.patch.dict(os.environ, {**cleared, "HOME": str(home)}), \ + unittest.mock.patch.object(Path, "home", return_value=home): + found = install.detect_platforms("wsl") + self.assertIn(("retroarch", exe_dir / "system"), found) + def test_the_windows_variables_are_read_through_cmd(self): answers = {"USERPROFILE": "C:\\Users\\me", "APPDATA": "C:\\Users\\me\\AppData\\Roaming"} From 4f7000f66d028c9bc25e19c18dc88fd10fce21e8 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:17:47 +0100 Subject: [PATCH 2/6] feat: hold installer pages to the code's words --- tests/test_doc_pages.py | 248 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index badc4eba8..93ee3a5ef 100644 --- a/tests/test_doc_pages.py +++ b/tests/test_doc_pages.py @@ -860,5 +860,253 @@ class RetroArchWindowsSystemFolder(unittest.TestCase): self.assertEqual(folders["macOS"], "`~/Documents/RetroArch/system/`") +# The pages that describe the installer, and the ones that cite its options. +INSTALLER_PAGES = ("installer.md", "first-install.md", "troubleshooting.md", "faq.md") +OPTION_PAGES = INSTALLER_PAGES + ("advanced-usage.md", "technical-faq.md") +INSTALLER_SOURCES = ("install.py", "install.sh", "install.ps1") +OPTION = re.compile(r"(? str: + """A page without its fenced blocks and comments.""" + return re.sub(r"", "", FENCED.sub("", text), flags=re.S) + + +def _parser_options() -> set[str]: + import install + + return {name for action in install.build_parser()._actions + for name in action.option_strings if name.startswith("--")} + + +def _script_options(name: str) -> set[str]: + """The options a build script spells, in its file or a module beside it + that it imports.""" + path = next((p for p in (ROOT / "scripts" / name, ROOT / name) if p.is_file()), None) + if path is None: + return set() + text = path.read_text(encoding="utf-8") + for module in re.findall(r"(?m)^(?:from|import) (\w+)", text): + beside = path.parent / f"{module}.py" + if beside.is_file() and beside != path: + text += beside.read_text(encoding="utf-8") + if path.suffix == ".sh": + return set(OPTION.findall(text)) + return set(re.findall(r"""["'](--[a-z][a-z0-9-]+)["'=]""", text)) | {"--help"} + + +def _owner(command: str) -> str | None: + """The program a command line runs: install.py for the one-liners.""" + if re.search(r"install\.py|\| *sh\b|scriptblock|install\.(?:sh|ps1)\b", command): + return "install.py" + named = SCRIPT.search(command) + return named.group(1) if named else None + + +def stale_options(text: str, default: str | None) -> list[str]: + """Options a page cites that the program it names does not have. + + A command line answers for the program it runs. An option in prose + answers for the scripts its paragraph names, for default (the + installer, on its own pages) and for the scripts the page runs.""" + known = {"install.py": _parser_options()} + + def has(program: str, option: str) -> bool: + if program not in known: + known[program] = _script_options(program) + return option in known[program] + + commands = [] + for block in FENCED.findall(text): + commands += [line for line in block.splitlines()[1:-1] if not line.lstrip().startswith("#")] + page_scripts = {_owner(command) for command in commands} - {None} + stale = [] + for line in commands: + for part in re.split(r"\s+&&\s+|;\s+|\|\s+(?!sh\b)", line): + if not part or part.lstrip().startswith(FOREIGN_COMMANDS): + continue + program = _owner(part) or default + for option in OPTION.findall(part): + if program is None or not has(program, option): + stale.append(f"{option} ({program}): {part.strip()}") + for paragraph in re.split(r"\n\s*\n", _prose(text)): + named = {m.group(1) for m in SCRIPT.finditer(paragraph)} - {"install.sh", "install.ps1"} + owners = named | page_scripts | ({default} if default else set()) + for span in re.findall(r"`([^`]+)`", paragraph.replace("\n", " ")): + if span.startswith(FOREIGN_COMMANDS): + continue + own = _owner(span) + candidates = {own} if own else owners + for option in OPTION.findall(span): + if not any(has(program, option) for program in candidates): + stale.append(f"{option} ({', '.join(sorted(candidates))}): {span}") + return stale + + +def stale_variables(text: str) -> list[str]: + """Environment variables a page names that neither the installer nor + its bootstraps and launchers read.""" + sources = "".join((ROOT / name).read_text(encoding="utf-8") for name in INSTALLER_SOURCES) + sources += "".join(path.read_text(encoding="utf-8", errors="replace") + for path in sorted((ROOT / "ports").rglob("*")) if path.is_file()) + found: set[str] = set() + for span in re.findall(r"`([^`]+)`", _prose(text)): + found |= set(re.findall(r"(? str: + """Every string the installer and its bootstraps print, a placeholder + (\\x01) where an f-string or the shell inserts a value, joined by \\x00.""" + texts = [] + for node in ast.walk(ast.parse((ROOT / "install.py").read_text(encoding="utf-8"))): + if isinstance(node, ast.JoinedStr): + texts.append("".join(value.value if isinstance(value, ast.Constant) else "\x01" + for value in node.values)) + elif isinstance(node, ast.Constant) and isinstance(node.value, str): + texts.append(node.value) + for name in ("install.sh", "install.ps1"): + for match in re.finditer(r'"((?:[^"\\]|\\.)*)"|\'([^\']*)\'', + (ROOT / name).read_text(encoding="utf-8")): + quoted = match.group(1) if match.group(1) is not None else match.group(2) + texts.append(re.sub(r"\$\{[^}]*\}|\$\([^)]*\)|\$\w+", "\x01", quoted)) + return "\x00".join(re.sub(r"\s+", " ", text) for text in texts) + + +# A word a message fills in: a number, a path, a name in capitals, an option. +VALUE = re.compile(r"^(?:\S*[\d/\\~<>{}=]\S*|--?[\w-]+\S*|\S*[A-Z]{2,}\S*|\.\.\.|" + r"\S*\.\w{2,4}[,.:;)]?|[A-Z][a-z]+[A-Z]\S*|\(\S*)$") +# A code span that is a command, a path or a shape, not a message. +NOT_A_MESSAGE = re.compile(r"^(?:\{|-|\.|/|~|<|\$|%|python|py |curl|irm|sh |pkg |sudo|ls |" + r"xcode|winget|termux|install\.py|scripts/|generate_|&\()") + + +def unprinted_messages(text: str, corpus: str) -> list[str]: + """Code spans that read as installer messages and that no string of + the installer prints. + + The values a message fills in (counts, paths, names) are free. Between + them, the words must be printed together by one string, each pair of + them at least, or beside a value that string inserts.""" + + def pair_printed(first: str, second: str) -> bool: + pair = f"{first} {second}".rstrip(".,:;") + forms = {pair, re.sub(r"s\b", "", pair), re.sub(r"s( |$)", r"\1", pair), + f"{first} \x01", f"\x01 {second}".rstrip(".,:;")} + return any(form in corpus for form in forms) + + stale = [] + for span in sorted({re.sub(r"\s+", " ", s) for s in re.findall(r"`([^`]+)`", _prose(text))}): + if NOT_A_MESSAGE.match(span) or len(span.split()) < 3 or span in FOREIGN_MESSAGES: + continue + words = span + for given in OS_WORDS: + words = words.replace(given, "1") + run: list[str] = [] + for word in [*words.split(" "), "1"]: + if word and not VALUE.match(word): + run.append(word) + continue + joined = " ".join(run) + if len(run) > 1 and joined.rstrip(".,:;") not in corpus and not all( + pair_printed(a, b) for a, b in zip(run, run[1:])): + stale.append(f"{span!r}: {joined!r}") + run = [] + return stale + + +def unserved_one_liners(page: str, text: str) -> list[str]: + """One-line commands whose address the site does not serve. The raw + address of the repository stays as the fallback of the One line + section, and must name a bootstrap the repository holds.""" + import sitebootstrap + from siteguides import INSTALL_BASE + + served = {f"{INSTALL_BASE}/{name}" for name in sitebootstrap.SOURCES} + fallback = _section(text, "One line") if page == "installer.md" else "" + stale = [] + for line in text.splitlines(): + if not re.search(r"\|\s*(?:sh|iex)\b|\\$|scriptblock\]::Create", line): + continue + for url in re.findall(r"https?://[^\s)`'\"]+", line): + if url in served: + continue + raw = re.fullmatch(rf"{re.escape(sitebootstrap.REPO_RAW)}/main/(install\.(?:sh|ps1))", url) + if raw and line in fallback and (ROOT / raw.group(1)).is_file(): + continue + stale.append(f"{url}: {line.strip()}") + return stale + + +class InstallerPagesFollowTheCode(unittest.TestCase): + """The installer page described an installer that had moved on, and + nothing failed. Options, variables, messages and addresses are words + the code owns, so each page is read against the code here.""" + + def test_every_option_is_the_programs(self): + for name in OPTION_PAGES: + default = "install.py" if name in INSTALLER_PAGES else None + with self.subTest(page=name): + self.assertEqual(stale_options(_page(name), default), []) + + def test_an_option_the_program_lacks_fails(self): + old = "Add `--resume` to carry on.\n\n```bash\npython install.py --keep-going\n```\n" + self.assertEqual(len(stale_options(old, "install.py")), 2) + self.assertEqual(stale_options("`curl --tlsv1.2` and `--check`.\n", "install.py"), []) + self.assertEqual(stale_options("```bash\npython scripts/verify.py --all\n```\n", None), []) + self.assertEqual(len(stale_options("```bash\npython scripts/verify.py --sideways\n```\n", + None)), 1) + + def test_every_variable_is_read(self): + import urllib.request + + for variable in STDLIB_VARIABLES: + self.assertIn(variable.lower(), inspect.getsource(urllib.request.proxy_bypass_environment)) + for name in INSTALLER_PAGES: + with self.subTest(page=name): + self.assertEqual(stale_variables(_page(name)), []) + + def test_a_variable_nothing_reads_fails(self): + self.assertEqual(stale_variables("Set `RETROBIOS_MIRROR` to another host.\n"), + ["RETROBIOS_MIRROR"]) + + def test_every_quoted_message_is_printed(self): + corpus = _literals() + for name in INSTALLER_PAGES: + with self.subTest(page=name): + self.assertEqual(unprinted_messages(_page(name), corpus), []) + + def test_a_message_the_installer_does_not_print_fails(self): + corpus = _literals() + self.assertEqual(unprinted_messages("It ends `Everything is in place.` here.\n", corpus), []) + self.assertEqual(len(unprinted_messages("It says `All BIOS files are present.`\n", corpus)), 1) + self.assertEqual(unprinted_messages( + "`E:\\ holds 38 of these files: a run without --check copies them.`\n", corpus), []) + + def test_every_one_liner_is_served(self): + for name in OPTION_PAGES: + with self.subTest(page=name): + self.assertEqual(unserved_one_liners(name, _page(name)), []) + + def test_a_one_liner_the_site_does_not_serve_fails(self): + """Before the site served `i`, every page gave the raw address.""" + old = ("## One line\n\nIntro.\n\n## Passing options\n\n```bash\ncurl -fsSL " + "https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh \\\n" + " | sh -s -- --platform retroarch\n```\n\n" + "```bash\ncurl -fsSL https://abdess.github.io/retrobios/install | sh\n```\n") + self.assertEqual(len(unserved_one_liners("installer.md", old)), 2) + + if __name__ == "__main__": unittest.main() From 084bce08f9801b07e788bd3cb8482b88e6e058aa Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:17:47 +0100 Subject: [PATCH 3/6] docs: bring the installer pages up to the code --- wiki/faq.md | 16 +++++----- wiki/first-install.md | 2 +- wiki/installer.md | 70 ++++++++++++++++++++--------------------- wiki/technical-faq.md | 6 ++-- wiki/troubleshooting.md | 2 +- 5 files changed, 47 insertions(+), 49 deletions(-) diff --git a/wiki/faq.md b/wiki/faq.md index 9c2268f73..779a5af66 100644 --- a/wiki/faq.md +++ b/wiki/faq.md @@ -9,7 +9,7 @@ terms: ios, iphone, ipad ## Do I need BIOS files? - **Yes** for PlayStation 1 and 2, Saturn and Neo Geo. -- **Better with it** for Dreamcast: some games need it. +- **Better with it** for Dreamcast. Some games need it. - **Usually not** for NES or Super Nintendo. [What is a BIOS?](what-are-bios-files.md) @@ -43,7 +43,7 @@ Usually. The command installs each setup's own, like `neogeo.zip`. ## My arcade game fails (which MAME version?) { #my-arcade-game-does-not-start } -Game sets must match the emulator's version: +Game sets must match the emulator's version. See [the list](technical-faq.md#which-mame-version-do-the-arcade-bios-sets-match). ## Keys, firmware or BIOS? @@ -61,7 +61,7 @@ Game sets must match the emulator's version: - It never touches games or saves. - It keeps each file it replaces in `retrobios-backup`, for `--restore`. -Read them: [install.sh](https://github.com/Abdess/retrobios/blob/main/install.sh), +Read them. [install.sh](https://github.com/Abdess/retrobios/blob/main/install.sh), [install.ps1](https://github.com/Abdess/retrobios/blob/main/install.ps1), [install.py](https://github.com/Abdess/retrobios/blob/main/install.py). @@ -89,17 +89,17 @@ curl -fsSL https://abdess.github.io/retrobios/i \ ## No internet on the device? -Fill it from a computer or point `--from-pack` at your ZIP: [Without a network](installer.md#without-a-network-offline-install). +Fill it from a computer, or point `--from-pack` at your ZIP. See [Without a network](installer.md#without-a-network-offline-install). ## Lost or wiped card? -`--from` copies BIOS files from an old card or backup: +`--from` copies BIOS files from an old card or backup. See [Copy from an old card](installer.md#copy-from-an-old-card). ## My setup is not in the list - Name your emulator's BIOS folder with `--dest`. -- ES-DE, LaunchBox: the command fills their emulators' folders. +- ES-DE or LaunchBox? The command fills their emulators' folders. - iPhone, iPad (iOS): no terminal. [Pick your consoles and copy them to the app](../which-pack.md#iphone-and-ipad). @@ -109,11 +109,11 @@ Some come with the emulator. ## My game shows a black screen -Often a missing BIOS: [When a game does not start](troubleshooting.md). +Often a missing BIOS. See [When a game does not start](troubleshooting.md). ## Can I copy my own console's BIOS? { #copy-your-own-bios } -Yes: [PCSX2](https://pcsx2.net/docs/setup/bios/) for PlayStation 2, +Yes. See [PCSX2](https://pcsx2.net/docs/setup/bios/) for PlayStation 2, [melonDS](https://melonds.kuribo64.net/faq.php) for DS and DSi. ## Is this legal? diff --git a/wiki/first-install.md b/wiki/first-install.md index f4d884288..fab2d9241 100644 --- a/wiki/first-install.md +++ b/wiki/first-install.md @@ -159,7 +159,7 @@ curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --platform retropie - ### Error: the downloaded installer does not match its expected fingerprint. -Nothing ran: something changed the file. Try another network. +Nothing ran, because something changed the file. Try another network. ### Cannot create ... diff --git a/wiki/installer.md b/wiki/installer.md index 477fae82f..f8353d65c 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -254,7 +254,9 @@ green, files missing or left out yellow, failures and a refused folder red, and secondary text dim. Only the 16 basic colours are used, the ones a handheld's console has. `NO_COLOR` set to any value but an empty one turns colour off, as do `TERM=dumb` or no `TERM`, a terminal whose description names fewer than 8 colours -and an output that is not a terminal. The text stays the same. The line-by-line +and an output that is not a terminal. The text stays the same. `TERM=dumb`, no +`TERM` outside Windows, or a terminal that moves no cursor the ANSI way +(`TERM=vt52`) also replace the live progress line with a line per file. The line-by-line questions paint the end of a run the same way. However the screen ends (Enter on the result, `q`, Ctrl+C, Ctrl+D, SIGTERM, a @@ -321,12 +323,12 @@ file list is downloaded once and handed to each run. ## What the installer does 1. Detects the host OS and the platforms installed on it. -2. Fetches `install/.json` from the same revision the bootstrap - verified the installer against, so the file list and the code reading it - always come from one commit. -3. Prints the file count and total size, then a safety notice counting the - entries the collection cannot serve, how many of those the platform marks - required, and why. At a terminal, the +2. Fetches `install/.json` from `main`, the newest file list, + whatever commit the installer itself came from. `RETROBIOS_REF` reads it + from a tag or a commit instead (see [For packagers](#for-packagers)). +3. Prints the file count and total size, then a `Not installable:` line + counting the files the collection does not hold and how many of them are + required. At a terminal, the [full-screen interface](#the-full-screen-interface) then offers **Install** for everything and **Choose systems** for fewer, and `--plain` asks the same as questions (see below). @@ -473,9 +475,6 @@ Checking existing files... ``` -`Not installable` counts the files of the list that the collection does not -hold. They are named at the end of the run. - On a first install nothing is in place. On a later run the line counts what is already there, and only the rest is downloaded. A download of 1 GB or more is confirmed first, the size and the folder in the question. @@ -838,7 +837,7 @@ lists, a game engine's own data. The manifest marks each of these files and names what it left out with its category (ten names, `--verbose` for all): ```text - 2436 game data files left out (--no-game-data): ports/doom/doom1.wad, ports/doom2/doom2.wad, ... + 2912 game data files left out (--no-game-data), 150 of them not installable: ports/CaveStory/Doukutsu.exe, ports/dinothawr/dinothawr.game, ports/doom/doom1.wad, ... ``` It is the cut `generate_pack.py --no-game-data` makes, and a test holds the @@ -854,7 +853,7 @@ they need. The cores are the `_libretro.so` (`.dll`, `.dylib`, way with `--dest` or without, `cores/` beside RetroArch when the setting is unset, a `retroarch.cfg` next to the system directory for a portable install, and the folders RetroPie (`/opt/retropie/libretrocores`) -and Lakka (`/usr/lib/libretro`, `/storage/cores`) install into. A core file +and Lakka (`/usr/lib/libretro`, `/storage/cores`, `/tmp/cores`) install into. A core file is named the buildbot's way (`mednafen_psx`), and the manifest maps each name to the profile its files are filed under (`beetle_psx`). @@ -949,15 +948,11 @@ what the options name, everything by default. -`--check` reads the same file list, hashes the destination and exits, with -code 1 when a file is missing or does not match and 0 when every one does. Wrong -copies are listed first and in full, then the missing ones whose -reason it found, then the first ten other missing ones (`--verbose` lists them -all). See [Why a file is refused](#why-a-file-is-refused). `--dry-run` counts the data trees in its total. Without -it, a file already present with the expected hash is left untouched, and one -whose contents do not match is re-downloaded and replaced by the verified copy, -the old one kept beside the BIOS folder. -Files the list does not name are never read, moved or deleted. +`--check` exits with code 1 when a file is missing or does not match, and 0 +when every one does. What it lists is under +[Why a file is refused](#why-a-file-is-refused). An install leaves a file with +the expected hash untouched and replaces any other, the old one kept beside +the BIOS folder. Files the list does not name are never read, moved or deleted. ## Platform detection @@ -967,7 +962,8 @@ system built on another one (KOLOS on JELOS, dArkOS on ArkOS, spruceOS on a TrimUI) matches both and is the one meant. Two unrelated systems matching are both named: at a terminal the run asks which one, otherwise it stops and asks for `--dest`. The run names the system as it names itself, with the file -list it gets: `Found AmberELEC at /storage/roms/bios (RetroArch files)`. +list it gets when that list is another system's: +`Found JELOS at /storage/roms/bios (RetroArch files)`. `--list-platforms` lists every system below. Where a system's RetroArch config is named, its `system_directory` is the @@ -985,9 +981,9 @@ rewrites both to `/roms2/bios`: a second card is followed through them. | `/recalbox/recalbox.version` or `/usr/bin/recalbox-settings` | Recalbox | Recalbox | `/recalbox/share/bios` | | `/opt/muos` | muOS | RetroArch | `system_directory` of `/opt/muos/share/info/config/retroarch.cfg`, else of the default it is copied from: `/run/muos/storage/bios`, which muOS binds to `MUOS/bios` on the second card when that card holds one, else the first. Cores are read from that config's `libretro_directory` (`/opt/muos/share/core`) | | `/mnt/mmc/MUOS/` | muOS | RetroArch | `/mnt/mmc/MUOS/bios` | -| `/etc/os-release` `NAME=AmberELEC` (CHAos writes the same) | AmberELEC | AmberELEC | `/storage/roms` (its list names `bios/`, `ports/` and `neogeo/` there) | +| `/etc/os-release` `NAME=AmberELEC` (CHAos writes the same) | AmberELEC | AmberELEC | `/storage/roms` (its list names `bios/` and `neogeo/` there) | | `/usr/bin/batocera-info` naming PAN4ELEC, on AmberELEC | PAN4ELEC | AmberELEC | as AmberELEC | -| `/etc/os-release` `NAME=EmuELEC` | EmuELEC | EmuELEC | as AmberELEC | +| `/etc/os-release` `NAME=EmuELEC` | EmuELEC | EmuELEC | `/storage/roms` (its list names `bios/`, `ports/` and `neogeo/` there) | | `/etc/os-release` `OS_NAME=JELOS` | JELOS | RetroArch | `/storage/.config/retroarch/retroarch.cfg`, else `/storage/roms/bios` | | `/usr/lib/systemd/system/jelos-ready.service`, on JELOS | KOLOS | RetroArch | as JELOS | | `/etc/os-release` `OS_NAME=UnofficialOS` | UnofficialOS | RetroArch | as JELOS | @@ -1154,14 +1150,15 @@ DuckStation and PCSX2, for one), the installer fills their own BIOS folders: the folders of the [standalone copies](#standalone-copies) list that exist on the machine, with the files that list names, taken from the EmuDeck file list and then RetroArch's. At a terminal it asks first. `--check` and `--dry-run` -work as for a platform. With `--system`, `--core`, `--emulator`, `--region` or -`--target` the run asks for a platform as before, since those name a -platform's lists. +work as for a platform. With `--system`, `--core`, `--emulator`, `--region`, +`--target`, `--installed-cores` or `--declared-only` the run asks for a +platform as before, since those name a platform's lists. When a platform named with `--platform` is not found on the machine, the destination falls back to the directory its detection looks for: -`/userdata/bios` for Batocera, `/recalbox/share/bios` for Recalbox, -`/storage/system` for Lakka, `~/retrodeck` for RetroDECK, `~/Emulation/bios` +`/userdata/bios` for Batocera and KNULLI, `/recalbox/share/bios` for Recalbox, +`/storage/system` for Lakka, `/storage/roms/bios` for ROCKNIX, +`/storage/roms` for AmberELEC and EmuELEC, `~/retrodeck` for RetroDECK, `~/Emulation/bios` for EmuDeck (`C:\Emulation\bios` on Windows), `/media/fat/games` for MiSTer, `~/RetroPie/BIOS` for RetroPie, `/romm/library/bios` for RomM, `C:\RetroBat\bios` for RetroBat, and RetroArch's own default system @@ -1269,7 +1266,8 @@ declare them (Batocera, EmuDeck, Recalbox, RetroArch, RetroBat, RetroDECK): | Files | Copied to | |-------|-----------| -| `prod.keys`, `title.keys` | yuzu, eden, citron, suyu and Ryujinx key directories | +| `prod.keys` | yuzu, eden, citron, suyu and Ryujinx key directories | +| `title.keys` | yuzu, eden, citron and suyu key directories | | `Citra/sysdata/aes_keys.txt`, `Citra/sysdata/boot9.bin` | Azahar `sysdata` | | `scph*.bin` | DuckStation `bios` | | `ps2-*.bin` | PCSX2 `bios`, native and Flatpak | @@ -1763,8 +1761,8 @@ RetroArch folder that holds only `scph5501.bin`, in the order written: | `RETROBIOS_INSTALL_URL` | Where the bootstrap fetches `install.py`. HTTPS only | | `RETROBIOS_INSTALL_SHA256` | The SHA-256 the bootstrap requires of that installer, 64 hex characters. When either of these two is set and the fingerprint does not match, the error names the variable instead of suggesting a retry | | `RETROBIOS_BOOTSTRAP` | Set by `install.sh` (`sh`) and `install.ps1` (`ps1`) for the temporary installer they run, so that its hints give back the one-line command rather than `python3 install.py` | -| `NO_COLOR` | Plain text, no colour, even at a terminal, when set to any value but an empty one. `TERM=dumb`, or no `TERM` outside Windows, also turns the live progress line and the colour off, as does a terminal whose description moves no cursor the ANSI way (`TERM=vt52`). A description naming fewer than 8 colours keeps the live line and drops the colour. The full screen then draws the same text without colour: the row in focus is marked with `>` as well as reverse video, and every state is written in words | -| `TERM`, `TERMINFO`, `TERMINFO_DIRS` | Decide whether the full screen opens. `TERM` unset or `dumb` (outside Windows) gives the questions line by line. Otherwise the terminal's description is read where ncurses reads it: `TERMINFO`, then `~/.terminfo`, then each folder of `TERMINFO_DIRS`, then the system's own. A description whose cursor moves are not the ANSI ones the screen writes (`TERM=vt52`) gives the questions too, and `--tui` there stops with the reason | +| `NO_COLOR` | Set to any value but an empty one: no colour, even at a terminal. The full screen then marks the row in focus with `>` as well as reverse video. What else turns colour off is under [The full-screen interface](#the-full-screen-interface) | +| `TERM`, `TERMINFO`, `TERMINFO_DIRS` | Decide whether the full screen opens, as [The full-screen interface](#the-full-screen-interface) says. The terminal's description is read where ncurses reads it: `TERMINFO`, then `~/.terminfo`, then each folder of `TERMINFO_DIRS`, then the system's own | | `FORCE_COLOR`, `CLICOLOR_FORCE` | Colour even when the output is a pipe or a file, set to any value but `0` or an empty one. `NO_COLOR` wins over both, and the progress line stays one line per file | | `SSL_CERT_FILE`, `SSL_CERT_DIR` | The trusted certificates to check servers with, for a Python that finds none of its own | | `RETROBIOS_PLAIN`, `ACCESSIBLE` | Set to anything but `0`: the questions come line by line, as with `--plain`, instead of the full-screen interface | @@ -1853,10 +1851,10 @@ finds here. `--help` lists the options with examples. ### No file list -The installer reads its manifests from the revision the bootstrap checked it -against. 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 +The installer reads its file lists from `main`, or from the ref +`RETROBIOS_REF` names. A platform name is checked before anything is +downloaded, so a "No file list" message comes from that ref: a tag that does +not exist, or one older than the platform. Unset it, or pick a tag from the releases page ([Environment overrides](#environment-overrides)). ### Upper and lower case diff --git a/wiki/technical-faq.md b/wiki/technical-faq.md index 27a018bb0..ee8723759 100644 --- a/wiki/technical-faq.md +++ b/wiki/technical-faq.md @@ -88,9 +88,9 @@ that (`emulators/beetle_psx.yml`, notes). The real file still ships, because the stand-in is not perfect: some games glitch or do not start. A profile entry records it as `hle_fallback: true`. That flag does not decide -`required`: 13 of the 778 entries that carry it are required, among them Beetle -PSX's three region BIOS. Verification reports a missing `hle_fallback` file at -INFO either way. +`required`: some entries that carry it are required, such as Yabause's +`saturn_bios.bin`. Verification reports a missing `hle_fallback` file at INFO +either way. ## How often are packs updated? diff --git a/wiki/troubleshooting.md b/wiki/troubleshooting.md index 96309059f..50c3acd22 100644 --- a/wiki/troubleshooting.md +++ b/wiki/troubleshooting.md @@ -69,7 +69,7 @@ version. - **A BIOS setting?** Some emulators can skip the BIOS. In PCSX ReARMed, set **BIOS Selection** to **Auto**. - **The screen says "Please insert PlayStation CD-ROM"?** The BIOS works. - The game did not load: open its `.cue` or `.m3u` file, not a `.bin`. + The game did not load. Open its `.cue` or `.m3u` file, not a `.bin`. ## If the command fails { #installation-script-fails } From ad4521094c2ee4a8c1019777f64042db47c4834a Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:24:46 +0100 Subject: [PATCH 4/6] docs: current names and counts in pack options --- wiki/advanced-usage.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/wiki/advanced-usage.md b/wiki/advanced-usage.md index c3edd6ae5..814a6d03c 100644 --- a/wiki/advanced-usage.md +++ b/wiki/advanced-usage.md @@ -10,8 +10,9 @@ list a platform would have if it were written from the emulators' code) and verification. The [latest release](https://github.com/Abdess/retrobios/releases/latest) -carries one pack for each platform, and that pack holds everything the -platform runs: the BIOS list it publishes, plus every file its emulator cores +carries a pack for the setups most people download (`release_pack: true` in +`platforms/_registry.yml`), and the installer serves the others. A pack holds +everything its platform runs: the BIOS list it publishes, plus every file its emulator cores load, read from their source code. There is no lighter variant to choose, because a pack that leaves out a file a core needs means a game that does not boot, with no message saying why. It is not a guarantee either: source @@ -169,10 +170,11 @@ A slot is a system **and** a declared region: the Japanese and American PlayStation BIOS are not alternatives to each other, so their ranks are never compared. -Where no such list is declared, the group is left untouched and counted: +Where no such list is declared, the group is left untouched and counted. The +RetroArch build above, with `--region us`, printed: ``` - 63 slot(s) with no declared order: every candidate kept + 104 slot(s) with no declared order: every candidate kept ``` Two rules keep a declared order from overreaching: @@ -196,7 +198,7 @@ though `scph5501.bin` would otherwise win at 5. `--one-per-slot` works with or without `--region`, requires `--platform` or `--all`, and is refused with `--manifest`, `--emulator`, `--system` and `--from-md5` rather than silently ignored. It tags the output name -(`Recalbox_10.0.8_OnePerSlot_BIOS_Pack.zip`) so a narrowed build never +(`Recalbox_10.1.1_OnePerSlot_BIOS_Pack.zip`) so a narrowed build never overwrites the full one, and the pack README says the pack was narrowed. @@ -209,7 +211,7 @@ Every dimension that removes files appears in the output filename and in the pack README, because the five places that name an artefact (the pack, the `--split` parts, the `_Split` directory, the grouped-alias rename and the manifest) all build from one list (`_narrowings` in `generate_pack.py`). A pack narrowed three ways is called -`Recalbox_10.0.8_NorthAmerica_OnePerSlot_Required_BIOS_Pack.zip` and opens with +`Recalbox_10.1.1_NorthAmerica_OnePerSlot_Required_BIOS_Pack.zip` and opens with a `PACK TYPE: Narrowed` block listing the three. Adding a new way to narrow a pack means adding it to that list, so it cannot reach users nameless or unannounced. @@ -531,7 +533,7 @@ python scripts/pipeline.py --offline --with-export ### Combining flags -Flags compose freely: +These flags compose (`--source` and `--all-variants` are refused with `--skip-packs`): ```bash python scripts/pipeline.py --offline --skip-packs --skip-docs --with-truth --target switch From 38b716d4cbfdfc5f0c4a5eb857c6f66390adc76e Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:59:07 +0100 Subject: [PATCH 5/6] fix: list the launchers with the new installer --- install/launchers/index.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/install/launchers/index.json b/install/launchers/index.json index 19f5a91c4..99ea3d23c 100644 --- a/install/launchers/index.json +++ b/install/launchers/index.json @@ -27,8 +27,8 @@ { "dest": "roms/ports/retrobios/install.py", "repo_path": "install.py", - "size": 984900, - "sha256": "403b6e8a7efcd24858b0f46ffa19c2b9012cdeef149768729d8e48d0b68a296a", + "size": 984807, + "sha256": "87d93c52753fedb9739816c628ba9109f92f6c5cc0aa6e1c5c3c3d2252244666", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 1038077, - "sha256": "49b9de6afa23afda4f6c786fa0968c36af1bd2ec9b52cb0927df4c527dcc701c" + "size": 1037984, + "sha256": "9ee54c49d165e3e285f3f62f3f1d4521584655a9726672cf5ff45c58a2ba3db2" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 984900, - "sha256": "403b6e8a7efcd24858b0f46ffa19c2b9012cdeef149768729d8e48d0b68a296a", + "size": 984807, + "sha256": "87d93c52753fedb9739816c628ba9109f92f6c5cc0aa6e1c5c3c3d2252244666", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 984900, - "sha256": "403b6e8a7efcd24858b0f46ffa19c2b9012cdeef149768729d8e48d0b68a296a", + "size": 984807, + "sha256": "87d93c52753fedb9739816c628ba9109f92f6c5cc0aa6e1c5c3c3d2252244666", "executable": false }, { From 8ab300176ec383f05703781c61a211870b45d34e Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:59:07 +0100 Subject: [PATCH 6/6] docs: keep the lead-in colons the faq tests hold --- wiki/faq.md | 14 +++++++------- wiki/troubleshooting.md | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/wiki/faq.md b/wiki/faq.md index 779a5af66..c199069a7 100644 --- a/wiki/faq.md +++ b/wiki/faq.md @@ -43,7 +43,7 @@ Usually. The command installs each setup's own, like `neogeo.zip`. ## My arcade game fails (which MAME version?) { #my-arcade-game-does-not-start } -Game sets must match the emulator's version. See +Game sets must match the emulator's version: [the list](technical-faq.md#which-mame-version-do-the-arcade-bios-sets-match). ## Keys, firmware or BIOS? @@ -61,7 +61,7 @@ Game sets must match the emulator's version. See - It never touches games or saves. - It keeps each file it replaces in `retrobios-backup`, for `--restore`. -Read them. [install.sh](https://github.com/Abdess/retrobios/blob/main/install.sh), +Read them: [install.sh](https://github.com/Abdess/retrobios/blob/main/install.sh), [install.ps1](https://github.com/Abdess/retrobios/blob/main/install.ps1), [install.py](https://github.com/Abdess/retrobios/blob/main/install.py). @@ -89,17 +89,17 @@ curl -fsSL https://abdess.github.io/retrobios/i \ ## No internet on the device? -Fill it from a computer, or point `--from-pack` at your ZIP. See [Without a network](installer.md#without-a-network-offline-install). +Fill it from a computer or point `--from-pack` at your ZIP: [Without a network](installer.md#without-a-network-offline-install). ## Lost or wiped card? -`--from` copies BIOS files from an old card or backup. See +`--from` copies BIOS files from an old card or backup: [Copy from an old card](installer.md#copy-from-an-old-card). ## My setup is not in the list - Name your emulator's BIOS folder with `--dest`. -- ES-DE or LaunchBox? The command fills their emulators' folders. +- ES-DE or LaunchBox? It fills their emulators' folders. - iPhone, iPad (iOS): no terminal. [Pick your consoles and copy them to the app](../which-pack.md#iphone-and-ipad). @@ -109,11 +109,11 @@ Some come with the emulator. ## My game shows a black screen -Often a missing BIOS. See [When a game does not start](troubleshooting.md). +Often a missing BIOS: [When a game does not start](troubleshooting.md). ## Can I copy my own console's BIOS? { #copy-your-own-bios } -Yes. See [PCSX2](https://pcsx2.net/docs/setup/bios/) for PlayStation 2, +Yes: [PCSX2](https://pcsx2.net/docs/setup/bios/) for PlayStation 2, [melonDS](https://melonds.kuribo64.net/faq.php) for DS and DSi. ## Is this legal? diff --git a/wiki/troubleshooting.md b/wiki/troubleshooting.md index 50c3acd22..4452074a6 100644 --- a/wiki/troubleshooting.md +++ b/wiki/troubleshooting.md @@ -69,7 +69,7 @@ version. - **A BIOS setting?** Some emulators can skip the BIOS. In PCSX ReARMed, set **BIOS Selection** to **Auto**. - **The screen says "Please insert PlayStation CD-ROM"?** The BIOS works. - The game did not load. Open its `.cue` or `.m3u` file, not a `.bin`. + The game did not load, so open its `.cue` or `.m3u` file, not a `.bin`. ## If the command fails { #installation-script-fails }