chore: merge installer page refresh

This commit is contained in:
Abdessamad Derraz committed 2026-11-04 05:52:02 +01:00
commit 33add072cb
12 files changed
+329 -64

No files matched your search

+1 -1
View File
@@ -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 = "15763a260495b0b7c9cdfc43a36cf6aec50577bd423ae44976394552b13120db"
$defaultInstallSha256 = "9c8f86b6bd6e1cb2a2a114e77bc7c1c30b390db675ddbfaadce1af3b337fa772"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+4 -4
View File
@@ -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 = (
@@ -2672,7 +2672,7 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
# default is <exe folder>\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:
+1 -1
View File
@@ -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="15763a260495b0b7c9cdfc43a36cf6aec50577bd423ae44976394552b13120db"
DEFAULT_INSTALL_SHA256="9c8f86b6bd6e1cb2a2a114e77bc7c1c30b390db675ddbfaadce1af3b337fa772"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 1079771,
"sha256": "15763a260495b0b7c9cdfc43a36cf6aec50577bd423ae44976394552b13120db",
"size": 1079678,
"sha256": "9c8f86b6bd6e1cb2a2a114e77bc7c1c30b390db675ddbfaadce1af3b337fa772",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 1132948,
"sha256": "9915dccec657903ac3c4f70f4e2b71e4388e8554cb3c958a02962c2aaf603d56"
"size": 1132855,
"sha256": "b1da6d7af0cb31d17eafea3c34142f66bff3890bdb74e6aa0faf070ad2f6b92b"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 1079771,
"sha256": "15763a260495b0b7c9cdfc43a36cf6aec50577bd423ae44976394552b13120db",
"size": 1079678,
"sha256": "9c8f86b6bd6e1cb2a2a114e77bc7c1c30b390db675ddbfaadce1af3b337fa772",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 1079771,
"sha256": "15763a260495b0b7c9cdfc43a36cf6aec50577bd423ae44976394552b13120db",
"size": 1079678,
"sha256": "9c8f86b6bd6e1cb2a2a114e77bc7c1c30b390db675ddbfaadce1af3b337fa772",
"executable": false
},
{
+250
View File
@@ -1428,5 +1428,255 @@ class SiteOutputIsTheStarlightProject(unittest.TestCase):
self.assertFalse(self.BARE_DOCS.search("`website/src/content/docs/`"))
# 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"(?<![\w-])(--[a-z][a-z0-9-]+)")
SCRIPT = re.compile(r"(?<![\w/.-])(?:scripts/)?([a-z_]+\.(?:py|sh))\b")
# Programs whose own options a page quotes: what follows them is theirs.
FOREIGN_COMMANDS = ("curl --", "xcode-select", "wget", "git ", "npm ", "gh ", "uvx ", "sha256sum")
# Variables the standard library reads for the installer, which install.py
# never spells: NO_PROXY reaches urllib through getproxies().
STDLIB_VARIABLES = {"NO_PROXY": "urllib.request"}
# Messages a page quotes from another program, with the program.
FOREIGN_MESSAGES = {"A positional parameter cannot be found": "PowerShell, on `irm ... | iex --x`"}
# Words the operating system hands the installer to print (strerror).
OS_WORDS = ("Permission denied",)
def _prose(text: str) -> 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"(?<![\w$%.-])([A-Z][A-Z0-9]*_[A-Z0-9_]+|NO_COLOR|TERM|ACCESSIBLE)"
r"(?![\w.(])", span))
return sorted(name for name in found if name not in sources and name not in STDLIB_VARIABLES)
def _literals() -> 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()
+15
View File
@@ -3666,6 +3666,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"}
+9 -7
View File
@@ -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
+2 -2
View File
@@ -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)
@@ -99,7 +99,7 @@ Fill it from a computer or point `--from-pack` at your ZIP: [Without a network](
## 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? 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).
+1 -1
View File
@@ -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 ...
+34 -36
View File
@@ -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/<platform>.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/<platform>.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...
```
<!-- /generated: run-index -->
`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.
@@ -881,7 +880,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
@@ -897,7 +896,7 @@ they need. The cores are the `<name>_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`).
@@ -994,15 +993,11 @@ what the options name, everything by default.
<!-- /generated: options -->
`--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
@@ -1012,7 +1007,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
@@ -1030,9 +1026,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 |
@@ -1201,14 +1197,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
@@ -1316,7 +1313,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 |
@@ -1820,8 +1818,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 |
@@ -1910,10 +1908,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
+3 -3
View File
@@ -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?
+1 -1
View File
@@ -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 }