chore: merge installer fixes, fifth pass

This commit is contained in:
Abdessamad Derraz committed 2026-11-07 00:11:09 +01:00
commit 16f8c869a5
35 files changed
+11023 -330

No files matched your search

+4 -2
View File
@@ -36,6 +36,8 @@ curl -fsSL https://abdess.github.io/retrobios/i | sh -s -- --platform retroarch
&([scriptblock]::Create((irm https://abdess.github.io/retrobios/i.ps1))) --check
```
![Recording of the one-line command in a terminal. The installer finds an ArkOS SD card with 6 games, plans 43 BIOS files (15.1 MB) for them, installs them and shows the summary.](docs_assets/demo/install.gif)
The script opens a full-screen installer, driven with the arrow keys and Enter, or with a controller on a handheld. It finds your setup and its BIOS folder, downloads only the missing files and checks each one. It can also copy the BIOS files from your old SD card.
Every option, and what the installer detects and writes, is on the [installer page](https://abdess.github.io/retrobios/wiki/installer/#passing-options).
@@ -124,7 +126,7 @@ Checked by is the test your platform runs on its own, replicated here from its s
## Build your own pack
Clone the repo and generate packs for any platform, emulator, or system:
Packs are built from a clone, and the clone carries the collection: its checkout alone is over 7.7 GB. `git clone --depth 1 https://github.com/Abdess/retrobios.git` leaves the history out. Files kept as release assets are downloaded when a pack needs them. Then generate packs for any platform, emulator, or system:
```bash
# The pack of a platform, as released
@@ -206,4 +208,4 @@ The scripts and tooling are released under the [MIT License](LICENSE).
The BIOS and firmware files are not covered by that license: they are third-party system software, preserved and provided for personal backup, archival, and interoperability with emulation software. [NOTICE](NOTICE) sets out their status and how to ask for a file to be removed.
The reasoning, and where it is weakest, is in [the project's legal position](https://abdess.github.io/retrobios/wiki/legal/).
*Auto-generated on 2026-11-06T15:07:54Z*
*Auto-generated on 2026-11-06T23:09:06Z*
+13 -13
View File
@@ -38,7 +38,7 @@ notes: |
FS-UAE is an Amiga emulator by Frode Solheim, based on UAE/WinUAE.
Standalone: the launcher walks ROMs/Kickstart, ROMs/Kickstarts, ROM/Kickstart
and ROM/Kickstarts under the data directory, with lowercase variants on Linux,
and ROM/Kickstarts under the data directory (data_root), with lowercase variants on Linux,
and the Amiga Forever shared ROM folder. Every .rom, .bin or model-suffixed
file up to 8 MB is handed to the emulator with its CRC32 and SHA-1. rom.key
is read from the folder of the encrypted ROM being loaded.
@@ -69,7 +69,7 @@ notes: |
files:
- name: "amiga-os-130.rom"
path: "fsuae/Kickstarts/amiga-os-130.rom"
standalone_path: "amiga-os-130.rom"
standalone_path: "ROMs/Kickstarts/amiga-os-130.rom"
load_from: {libretro: save_dir}
system: amiga
required: false
@@ -83,7 +83,7 @@ files:
- name: "amiga-os-204.rom"
path: "fsuae/Kickstarts/amiga-os-204.rom"
standalone_path: "amiga-os-204.rom"
standalone_path: "ROMs/Kickstarts/amiga-os-204.rom"
load_from: {libretro: save_dir}
system: amiga
required: false
@@ -97,7 +97,7 @@ files:
- name: "amiga-os-205.rom"
path: "fsuae/Kickstarts/amiga-os-205.rom"
standalone_path: "amiga-os-205.rom"
standalone_path: "ROMs/Kickstarts/amiga-os-205.rom"
load_from: {libretro: save_dir}
system: amiga
required: false
@@ -111,7 +111,7 @@ files:
- name: "amiga-os-120.rom"
path: "fsuae/Kickstarts/amiga-os-120.rom"
standalone_path: "amiga-os-120.rom"
standalone_path: "ROMs/Kickstarts/amiga-os-120.rom"
load_from: {libretro: save_dir}
system: amiga
required: false
@@ -125,7 +125,7 @@ files:
- name: "amiga-os-310-a1200.rom"
path: "fsuae/Kickstarts/amiga-os-310-a1200.rom"
standalone_path: "amiga-os-310-a1200.rom"
standalone_path: "ROMs/Kickstarts/amiga-os-310-a1200.rom"
load_from: {libretro: save_dir}
system: amiga
required: false
@@ -139,7 +139,7 @@ files:
- name: "amiga-os-310-a3000.rom"
path: "fsuae/Kickstarts/amiga-os-310-a3000.rom"
standalone_path: "amiga-os-310-a3000.rom"
standalone_path: "ROMs/Kickstarts/amiga-os-310-a3000.rom"
load_from: {libretro: save_dir}
system: amiga
required: false
@@ -153,7 +153,7 @@ files:
- name: "amiga-os-310-a4000.rom"
path: "fsuae/Kickstarts/amiga-os-310-a4000.rom"
standalone_path: "amiga-os-310-a4000.rom"
standalone_path: "ROMs/Kickstarts/amiga-os-310-a4000.rom"
load_from: {libretro: save_dir}
system: amiga
required: false
@@ -167,7 +167,7 @@ files:
- name: "amiga-os-310-cd32.rom"
path: "fsuae/Kickstarts/amiga-os-310-cd32.rom"
standalone_path: "amiga-os-310-cd32.rom"
standalone_path: "ROMs/Kickstarts/amiga-os-310-cd32.rom"
load_from: {libretro: save_dir}
system: amiga
required: false
@@ -181,7 +181,7 @@ files:
- name: "amiga-ext-310-cd32.rom"
path: "fsuae/Kickstarts/amiga-ext-310-cd32.rom"
standalone_path: "amiga-ext-310-cd32.rom"
standalone_path: "ROMs/Kickstarts/amiga-ext-310-cd32.rom"
load_from: {libretro: save_dir}
system: amiga
required: false
@@ -194,7 +194,7 @@ files:
- name: "cd32fmv.rom"
path: "fsuae/Kickstarts/cd32fmv.rom"
standalone_path: "cd32fmv.rom"
standalone_path: "ROMs/Kickstarts/cd32fmv.rom"
load_from: {libretro: save_dir}
system: amiga
required: false
@@ -207,7 +207,7 @@ files:
- name: "amiga-ext-130-cdtv.rom"
path: "fsuae/Kickstarts/amiga-ext-130-cdtv.rom"
standalone_path: "amiga-ext-130-cdtv.rom"
standalone_path: "ROMs/Kickstarts/amiga-ext-130-cdtv.rom"
load_from: {libretro: save_dir}
system: amiga
required: false
@@ -220,7 +220,7 @@ files:
- name: "rom.key"
path: "fsuae/Kickstarts/rom.key"
standalone_path: "rom.key"
standalone_path: "ROMs/Kickstarts/rom.key"
load_from: {libretro: save_dir}
system: amiga
found_by: name
+14 -3
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 = "b17220355845692a087bbc29cdb4a1008f68b1423d1126f34f00fa0259b53184"
$defaultInstallSha256 = "25a88f9a078717afbe34419481d222d2f6f42c4bf24b4ec64bc3faa9561ea01c"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
@@ -53,8 +53,19 @@
"Check the network connection and any proxy, then run the same command again. " +
"Nothing was run and nothing was written.") -f $url, $_.Exception.Message
}
if ((Get-Item -LiteralPath $temporary).Length -gt $maximumInstallerBytes) {
throw "Downloaded installer exceeds the size limit."
$downloaded = (Get-Item -LiteralPath $temporary).Length
if ($downloaded -gt $maximumInstallerBytes) {
# A page served in place of the file says the network
# answered, not this project: the one cause a user can act on.
$tooLarge = ("The downloaded installer is {0} bytes, more than the {1} this script accepts. " +
"Nothing was run and nothing was written. A network that answers with a page of its own " +
"(a sign-in portal, a proxy) gives this: open any web page to sign in, then run the same " +
"command again.") -f $downloaded, $maximumInstallerBytes
if ([Environment]::GetEnvironmentVariable("RETROBIOS_INSTALL_URL")) {
$tooLarge += " RETROBIOS_INSTALL_URL is set in this session and names another installer: " +
"remove it to use the installer this script names."
}
throw $tooLarge
}
$actual = (Get-FileHash -LiteralPath $temporary -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -ne $expected.ToLowerInvariant()) {
+202 -31
View File
@@ -1200,7 +1200,9 @@ def _parse_os_release() -> dict[str, str]:
# Set under WSL: a Windows path read from a config or a rule names a drive
# that Linux sees under /mnt (drvfs). C:\\RetroBat\\bios was used as a relative
# path and nothing Windows-side was ever found from WSL.
_WSL = {"active": False, "root": "/mnt"}
# unasked: why Windows could not be asked for its folders, said once by
# detection.
_WSL = {"active": False, "root": "/mnt", "unasked": ""}
_WINDOWS_VARIABLES = ("USERPROFILE", "APPDATA", "LOCALAPPDATA", "ProgramFiles", "ProgramFiles(x86)")
@@ -1255,13 +1257,27 @@ def _enter_wsl() -> None:
capture_output=True, text=True, timeout=10, stdin=subprocess.DEVNULL,
cwd="/mnt/c" if Path("/mnt/c").is_dir() else None,
)
except (OSError, subprocess.SubprocessError):
except (OSError, subprocess.SubprocessError) as exc:
# The others would wait as long: detection is told the Windows
# side was not searched, never that nothing is there.
_windows_unasked("cmd.exe", exc)
return
value = result.stdout.strip()
if value and not value.startswith("%"):
os.environ[variable] = str(_host_path(value))
def _windows_unasked(tool: str, exc: Exception) -> None:
"""Keep why a Windows program run from WSL gave no answer: an interop
slow to start or turned off hid every Windows setup, and the run said
none was installed."""
if isinstance(exc, subprocess.TimeoutExpired):
why = f"{tool} did not answer in {exc.timeout:g} s"
else:
why = f"{tool} could not run: {getattr(exc, 'strerror', None) or exc}"
_WSL["unasked"] = _WSL["unasked"] or why
def _expand_retroarch_path(value: str, app_dir: Path, home: Path | None = None) -> Path:
"""Expand the notations RetroArch writes into its config values.
@@ -1308,13 +1324,22 @@ def _steam_libraries() -> list[Path]:
Steam lists the others in steamapps/libraryfolders.vdf, each as a
"path" line. A RetroArch installed on D:/SteamLibrary was not found
when only Program Files was searched.
when only Program Files was searched. The client's own folder is the
SteamPath value it writes under HKCU\\Software\\Valve\\Steam, read the
same way by Playnite (PlayniteExtensions 886468f6,
source/Libraries/SteamLibrary/Steam.cs:34-38): a client installed in
D:\\Steam hid every library.
"""
roots: list[Path] = []
steam_path = _read_registry("HKCU\\Software\\Valve\\Steam", "SteamPath")
candidates = [_host_path(steam_path.replace("/", "\\"))] if steam_path else []
for var in ("ProgramFiles(x86)", "ProgramFiles"):
base = os.environ.get(var, "")
if base and _is_dir(Path(base) / "Steam"):
roots.append(Path(base) / "Steam")
if base:
candidates.append(Path(base) / "Steam")
for candidate in candidates:
if candidate not in roots and _is_dir(candidate):
roots.append(candidate)
libraries = list(roots)
for root in roots:
vdf = root / "steamapps" / "libraryfolders.vdf"
@@ -1385,6 +1410,49 @@ def _macos_retroarch_base() -> Path:
return Path.home() / "Documents" / "RetroArch"
def _retroarch_appimage_dirs(home: Path) -> list[Path]:
"""The system folder of each RetroArch AppImage kept with a portable
folder, up to two folders below home.
The buildbot's Linux build unpacks RetroArch-Linux-x86_64.AppImage beside
RetroArch-Linux-x86_64.AppImage.home/.config/retroarch. The AppImage
runtime sets HOME to <AppImage>.home and XDG_CONFIG_HOME to
<AppImage>.config when those folders exist (type2-runtime fedfad51,
src/runtime/runtime.c:1449-1462), so this RetroArch never reads
~/.config/retroarch. The archive holds no retroarch.cfg: until the first
run writes one, the system folder is the base folder's (frontend_unix_get_env
in platform_unix.c).
"""
images: list[Path] = []
for depth in ("", "*/", "*/*/"):
for suffix in (".home", ".config"):
try:
hits = sorted(home.glob(f"{depth}*.AppImage{suffix}"))
except OSError:
continue
for hit in hits:
image = hit.with_name(hit.name[:-len(suffix)])
if image not in images and _is_file(image):
images.append(image)
dirs: list[Path] = []
for image in images:
portable_home = image.with_name(image.name + ".home")
portable_config = image.with_name(image.name + ".config")
if _is_dir(portable_config):
base = portable_config / "retroarch"
elif _is_dir(portable_home) and "XDG_CONFIG_HOME" not in os.environ:
base = portable_home / ".config" / "retroarch"
else:
continue
tilde = portable_home if _is_dir(portable_home) else home
system = _parse_retroarch_system_dir(base / "retroarch.cfg", base, tilde)
if system is None and _is_dir(base):
system = base / "system"
if system is not None and system not in dirs:
dirs.append(system)
return dirs
def _unix_retroarch_base() -> Path:
"""RetroArch's base folder on Linux: $XDG_CONFIG_HOME/retroarch when the
variable is set, else ~/.config/retroarch (fill_pathname_application_data
@@ -2521,6 +2589,9 @@ def refused_words(folder: Path, why: str, shown: str = "") -> str:
"""The line said for a folder detection could not look in. shown is
the folder as the screen writes it (~/.config/EmuDeck), the full path
by default."""
if _WSL["active"] and why == _WSL["unasked"] and folder == Path(_WSL["root"]):
return (f"Could not look for setups on the Windows side: {why}. "
"Run it again once Windows answers, or name the BIOS folder with --dest.")
if folder in _DETECTION_UNREADABLE:
return (f"Could not look for setups: {shown or folder} could not be read ({why}). "
"A failing card or a link that loops gives this: check it, or name the "
@@ -2555,6 +2626,22 @@ def _note_refused(path: Path, exc: OSError) -> None:
print(f" {screen_text(words)}", file=sys.stderr)
def _note_windows_unasked() -> None:
"""Say once, as a folder detection could not look in, that Windows did
not answer: its drives are then searched without its folders."""
why = _WSL["unasked"]
if (not why or not getattr(_SETUP_SEARCH, "active", False)
or getattr(_SETUP_SEARCH, "quiet", False)):
return
refused = (Path(_WSL["root"]), why)
if refused in _DETECTION_REFUSED:
return
_DETECTION_REFUSED.append(refused)
words = refused_words(*refused)
EVENTS.emit("unsearched", folder=str(refused[0]), reason=why, message=words)
print(f" {screen_text(words)}", file=sys.stderr)
def _detection_guarded(detect: Callable[..., list]) -> Callable[..., list]:
"""Detection whose probes treat a folder this user cannot open as
absent, and say so once.
@@ -2694,7 +2781,9 @@ def _read_registry(key: str, value: str) -> str | None:
capture_output=True, text=True, errors="replace", timeout=10,
stdin=subprocess.DEVNULL, cwd="/mnt/c" if Path("/mnt/c").is_dir() else None,
)
except (OSError, subprocess.SubprocessError):
except (OSError, subprocess.SubprocessError) as exc:
_windows_unasked("reg.exe", exc)
_note_windows_unasked()
return None
match = re.search(
rf"^\s*{re.escape(value)}\s+REG_(?:EXPAND_)?SZ\s+(.+?)\s*$", result.stdout, re.M
@@ -2734,6 +2823,8 @@ def _detect_by_path(os_type: str, found: list[tuple[str, Path]]) -> None:
def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
"""Detect installed emulator platforms and their BIOS directories."""
found: list[tuple[str, Path]] = []
if os_type == "wsl":
_note_windows_unasked()
if os_type == "android":
found.extend(_detect_android())
@@ -2800,6 +2891,8 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
found.append(("retroarch", ra_dir))
break
found.extend(("retroarch", ra_dir) for ra_dir in _retroarch_appimage_dirs(home))
if os_type == "darwin":
# open_default_config_file reads <Application Support>/RetroArch/config
# (configuration.c). A value left at default is the documents folder,
@@ -2835,15 +2928,9 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
if ra_dir:
found.append(("retroarch", ra_dir))
# RetroArch from Steam: a portable install under the default library
for var in ("ProgramFiles(x86)", "ProgramFiles"):
program_files = os.environ.get(var, "")
if not program_files:
continue
steam_cfg = (
Path(program_files) / "Steam" / "steamapps" / "common"
/ "RetroArch" / "retroarch.cfg"
)
# RetroArch from Steam: a portable install in a Steam library
for library in _steam_libraries():
steam_cfg = library / "steamapps" / "common" / "RetroArch" / "retroarch.cfg"
ra_dir = _parse_retroarch_system_dir(steam_cfg)
if ra_dir and not any(name == "retroarch" for name, _ in found):
found.append(("retroarch", ra_dir))
@@ -3895,6 +3982,12 @@ def _validate_manifest(data: object, plat: str) -> dict:
_safe_relative_path(
entry["source"], f"standalone_copies[{index}].source"
)
if "as" in entry and (
"file" not in entry or not isinstance(entry["as"], str)
or PurePosixPath(_safe_relative_path(
entry["as"], f"standalone_copies[{index}].as")).name != entry["as"]
):
raise ValueError(f"invalid standalone copy name at {where}")
if "pattern" in entry:
pattern = entry["pattern"]
if (
@@ -5626,6 +5719,8 @@ def _download_one(
except OSError as exc:
if _write_refused(exc):
return f["dest"], False, NOT_WRITABLE
if _drive_full(exc):
return f["dest"], False, DRIVE_FULL
return f["dest"], False, exc.strerror or str(exc)
try:
in_place_folder = dest.is_dir()
@@ -5838,6 +5933,10 @@ def _download_one(
# place is not a folder this user cannot write.
reason = READ_ONLY if _read_only_file(dest) else NOT_WRITABLE
refused = True
elif _drive_full(exc):
# No attempt frees space either. A private copy is still
# removed below: it holds the space the next file needs.
reason = DRIVE_FULL
elif _transfer_failure(exc):
reason = NetworkReason(network_words(exc)[1])
else:
@@ -5856,7 +5955,7 @@ def _download_one(
progress.add(-counted)
if resumed_count:
progress.forget_resumed(resumed_count)
if refused:
if refused or reason == DRIVE_FULL:
return f["dest"], False, reason
if attempt < MAX_RETRIES and delay and _stop.wait(delay):
return f["dest"], False, "stopped"
@@ -5972,6 +6071,21 @@ def _claim_partial(path: Path) -> BinaryIO | None:
return handle
def _release_claim(handle: BinaryIO, path: Path) -> None:
"""Free a file taken with _claim_partial and remove it. Removed while
still held where the system allows it, so no other run takes the name
in between. Windows refuses to remove an open file: there it is closed
first, and one another run opened meanwhile is left to that run."""
if not _NO_RENAME_WHILE_OPEN:
path.unlink(missing_ok=True)
handle.close()
if _NO_RENAME_WHILE_OPEN:
try:
path.unlink(missing_ok=True)
except OSError:
pass # another run holds it now and removes it when done
class _Unclosed:
"""The locked partial copy, written through without closing it."""
@@ -6012,6 +6126,16 @@ def _locations(paths: list[Path]) -> str:
NOT_WRITABLE = "folder not writable"
# A write the drive had no room for: the same run fails the same way until
# space is freed or another drive is named.
DRIVE_FULL = "the drive is full"
_FULL_ERRNOS = (errno.ENOSPC, getattr(errno, "EDQUOT", errno.ENOSPC))
def _drive_full(exc: BaseException) -> bool:
return isinstance(exc, OSError) and exc.errno in _FULL_ERRNOS
def _write_refused(exc: OSError) -> bool:
# A sharing violation is a file another process holds open, which a
# later attempt can find released: no refusal of the folder.
@@ -6490,6 +6614,9 @@ def _report_outcome(
Path(dest).parent for dest, reason in failures or []
if Path(dest).is_absolute() and reason == NOT_WRITABLE
})]
if any(reason == DRIVE_FULL for _dest, reason in failures or []):
advice.append(f" The drive holding {_locations(paths)} is full: free up space, or "
"install to another drive with --dest, then run again.")
if any(reason == READ_ONLY for _dest, reason in failures or []):
advice.append(" Files marked read-only are in the way: clear that attribute "
"(or delete them), then run again.")
@@ -6593,7 +6720,8 @@ def _report_outcome(
retry = _retry_words(failures)
if retry:
say(retry)
elif not all(str(reason).startswith(NOT_KEPT) for _dest, reason in failures or []):
elif not all(str(reason).startswith(NOT_KEPT) or reason == DRIVE_FULL
for _dest, reason in failures or []):
say("Run the same command again to retry only those.")
for line in same_lines:
say(line)
@@ -7271,6 +7399,22 @@ def data_directory_state(entry: dict, bios_path: Path,
return root, needed, present
def data_space_bytes(data_states: list[tuple[dict, tuple[Path, dict[str, dict], int]]]) -> int:
"""What the data trees take on the drive while they are rebuilt: every
file they lack, plus the largest source archive, which sits in its tree
until that tree is read. Only the BIOS files were held against the free
space: 18.8 KB passed on a card with 20 MB left, and 81.8 MB followed.
Nothing is written for them offline without a pack."""
if LOCAL.pack is None and _offline_only():
return 0
files = sum(meta.get("size", 0) for _entry, (_root, needed, _present) in data_states
for meta in needed.values())
if LOCAL.pack is not None:
return files
return files + max((data_fetch_bytes(entry, state[1]) for entry, state in data_states
if state[1]), default=0)
def data_fetch_bytes(entry: dict, needed: dict[str, dict]) -> int:
"""What getting a tree's missing files costs: its source archive, which
comes whole for one file, or from a pack the files themselves. A list
@@ -7304,6 +7448,8 @@ def install_data_directory(entry: dict, bios_path: Path,
return counts
_writing.set()
fetched: Path | None = None
lock: BinaryIO | None = None
lock_path = root / f".{entry['ref']}.archive.lock"
try:
root.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(dir=root) as scratch:
@@ -7313,7 +7459,11 @@ def install_data_directory(entry: dict, bios_path: Path,
else:
# Beside the tree, outside the scratch folder: a transfer
# cut short is resumed by the next attempt or the next run.
kept = root / f".{entry['ref']}.archive.part"
# Two runs into one folder truncated or deleted the archive
# the other was still writing or reading: the run that finds
# it locked downloads into its scratch folder instead.
lock = _claim_partial(lock_path)
kept = root / f".{entry['ref']}.archive.part" if lock is not None else None
archive = _fetch_archive(entry["url"], Path(scratch), progress, kept)
fetched = archive
members = _archive_members(archive, entry)
@@ -7342,9 +7492,12 @@ def install_data_directory(entry: dict, bios_path: Path,
except (urllib.error.URLError, OSError, ValueError, tarfile.TarError) as exc:
# The tree's folder refused, not the source: said as the BIOS files
# say it, never as an archive that could not be read.
# A full drive is said as one too: "could not read <url>: [Errno
# 28]" put the cause on the source, and the end said to run again.
counts["error"] = (NOT_WRITABLE if isinstance(exc, OSError)
and not isinstance(exc, urllib.error.URLError)
and _write_refused(exc) and not _folder_writable(root)
else DRIVE_FULL if _drive_full(exc)
else str(getattr(exc, "reason", None) or exc))
except _MEMBER_ERRORS as exc:
# A damaged, encrypted or unsupported member, in words.
@@ -7353,6 +7506,8 @@ def install_data_directory(entry: dict, bios_path: Path,
if fetched is not None:
# Read whole: the next run fetches the archive afresh.
_drop_archive(fetched, fetched.with_name(fetched.name + ".source"))
if lock is not None:
_release_claim(lock, lock_path)
counts["absent"] = len(needed)
return counts
@@ -7482,7 +7637,9 @@ def standalone_plan(
continue
chosen = plan.setdefault(folder, {})
for entry in sources:
name = PurePosixPath(entry["dest"]).name
# `as`: the name the emulator reads, when it is not the
# platform's (melonDS's dsisd.bin, xemu's eeprom.bin).
name = copy.get("as") or PurePosixPath(entry["dest"]).name
chosen.setdefault(name, {**entry, "dest": name})
return [(folder, list(entries.values())) for folder, entries in plan.items() if entries]
@@ -7562,7 +7719,7 @@ def _install_standalone_folders(
if args.check:
if pending:
print("\n" + _check_fetch_line(pending, "its folder"))
sys.exit(1)
sys.exit(3)
return True
if args.dry_run:
return True
@@ -7646,7 +7803,7 @@ def do_standalone_copies(
continue
resolved_dir = target_dir.resolve()
for src in sources:
dest = target_dir / src.name
dest = target_dir / (entry.get("as") or src.name)
linked = dest.is_symlink()
# A crafted source name would climb out of the directory.
if not linked and not _within(dest, resolved_dir):
@@ -7656,7 +7813,7 @@ def do_standalone_copies(
failures.append((str(dest), OUTSIDE_TARGET))
continue
try:
KEPT.keep(resolved_dir, PurePosixPath(src.name), dest, {}, src)
KEPT.keep(resolved_dir, PurePosixPath(dest.name), dest, {}, src)
except _NotKept as exc:
print(f" ERROR: {dest}: {NOT_KEPT} ({exc})")
failed += 1
@@ -13285,8 +13442,8 @@ def build_parser() -> argparse.ArgumentParser:
"--target",
metavar="NAME",
help="hardware target for core filtering (switch, rpi4, ...). "
"On Batocera, Recalbox, ROCKNIX, Lakka, KNULLI and RetroPie the board is read "
"from the system",
"On AmberELEC, EmuELEC, Batocera, Recalbox, ROCKNIX, Lakka, KNULLI and RetroPie "
"the board is read from the system",
)
parser.add_argument(
"--every-board",
@@ -15899,6 +16056,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
print(line)
continue
space_checked = False
if to_download:
fetched = [f for f in to_download if id(f) not in copies]
if _offline_only() and fetched:
@@ -15995,9 +16153,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
print(" Press Ctrl+C to stop. Running the same command again "
"carries on from here.")
# Checked before the folder is created: a run stopped for space
# left an empty BIOS folder behind.
_check_free_space(bios_path, dl_size, plat_name, 1 if total_downloaded else 2,
on_its_card(plat_name, bios_path))
# left an empty BIOS folder behind. The data trees count too,
# written after the files on the same drive.
_check_free_space(bios_path, dl_size + data_space_bytes(data_states), plat_name,
1 if total_downloaded else 2, on_its_card(plat_name, bios_path))
space_checked = True
created = not bios_path.exists()
try:
bios_path.mkdir(parents=True, exist_ok=True)
@@ -16075,6 +16235,12 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
total_errors += 1
else:
writable.append((entry, state))
if writable and not space_checked:
# With every BIOS file in place, the trees alone: checked
# before the first archive is written.
_check_free_space(bios_path, data_space_bytes(writable), plat_name,
1 if total_downloaded else 2,
on_its_card(plat_name, bios_path))
if writable:
print("\nData directories:")
for entry, state in writable:
@@ -16115,8 +16281,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
f", {counts['changed'] + counts['absent']} no longer in "
"the source (the pack has them)")
print(line)
if counts["error"] == NOT_WRITABLE:
failures.append((entry["ref"], NOT_WRITABLE))
if counts["error"] in (NOT_WRITABLE, DRIVE_FULL):
if counts["error"] == DRIVE_FULL:
print(f" {entry['ref']}: {DRIVE_FULL} ({shown_path(bios_path)})",
file=sys.stderr)
failures.append((entry["ref"], counts["error"]))
total_errors += 1
elif counts["error"]:
print(f" {entry['ref']}: could not read "
@@ -16171,9 +16340,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
skipped=_skipped(omitted_seen), left_out=_left_out_events(left_out_seen),
kept=KEPT.summary(), interrupted=total_interrupted,
)
# A check scripted as `--check && launch` reads the exit code only.
# A check scripted as `--check && launch` reads the exit code only. Its
# own code, 3: with 1, a check that found files missing could not be
# told from a check that never ran (a list refused, a --from missing).
if args.check and check_pending:
sys.exit(1)
sys.exit(3)
if not args.check and not listing and not args.dry_run:
_report_outcome(
installed_paths,
+11 -2
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="b17220355845692a087bbc29cdb4a1008f68b1423d1126f34f00fa0259b53184"
DEFAULT_INSTALL_SHA256="25a88f9a078717afbe34419481d222d2f6f42c4bf24b4ec64bc3faa9561ea01c"
MAX_INSTALLER_BYTES=2097152
cleanup() {
@@ -94,7 +94,16 @@ if [ -z "$INSTALLER" ] || [ ! -f "$INSTALLER" ]; then
fi
actual_size=$(wc -c < "$TEMP_INSTALLER" | tr -d ' ')
if [ "$actual_size" -gt "$MAX_INSTALLER_BYTES" ]; then
echo "Error: downloaded installer exceeds the size limit." >&2
# A page served in place of the file says the network answered, not this
# project: the one cause a user can act on.
echo "Error: the downloaded installer is $actual_size bytes, more than the $MAX_INSTALLER_BYTES this script accepts." >&2
echo " Nothing was run and nothing was written. A network that answers with a page of" >&2
echo " its own (a sign-in portal, a proxy) gives this: open any web page to sign in," >&2
echo " then run the same command again." >&2
if [ -n "${RETROBIOS_INSTALL_URL:-}" ]; then
echo " RETROBIOS_INSTALL_URL is set in this environment and names another installer:" >&2
echo " unset it to use the installer this script names." >&2
fi
exit 1
fi
if command -v sha256sum >/dev/null 2>&1; then
+6 -3
View File
@@ -77381,7 +77381,8 @@
}
},
"strip_components": 1,
"archive_size": 5083767
"archive_size": 5083767,
"optional": true
},
{
"ref": "fbneo-samples",
@@ -77615,7 +77616,8 @@
}
},
"strip_components": 0,
"archive_size": 53006540
"archive_size": 53006540,
"optional": true
},
{
"ref": "freeintv-overlays",
@@ -77988,7 +77990,8 @@
}
},
"strip_components": 1,
"archive_size": 21311170
"archive_size": 21311170,
"optional": true
}
]
}
+1479 -50
View File
File diff suppressed because it is too large. Load diff
+1765 -19
View File
File diff suppressed because it is too large. Load diff
+6 -3
View File
@@ -83639,7 +83639,8 @@
}
},
"strip_components": 1,
"archive_size": 5083767
"archive_size": 5083767,
"optional": true
},
{
"ref": "fbneo-samples",
@@ -83873,7 +83874,8 @@
}
},
"strip_components": 0,
"archive_size": 53006540
"archive_size": 53006540,
"optional": true
},
{
"ref": "freeintv-overlays",
@@ -84246,7 +84248,8 @@
}
},
"strip_components": 1,
"archive_size": 21311170
"archive_size": 21311170,
"optional": true
}
]
}
+1 -1
View File
@@ -1 +1 @@
{"files":[["32X_G_BIOS.BIN",256,"bios"],["32X_M_BIOS.BIN",2048,"bios"],["32X_S_BIOS.BIN",1024,"bios"],["3do_arcade_saot.bin",524288,"bios"],["5200.rom",2048,"bios"],["64DD_IPL.bin",4194304,"bios"],["64DD_IPL_DEV.n64",4194304,"bios"],["64DD_IPL_JP.n64",4194304,"bios"],["64DD_IPL_US.n64",4194304,"bios"],["6570-036.rom",2048,"bios"],["68HC05.rom",8192,"bios"],["7800 BIOS (E).rom",16384,"bios"],["7800 BIOS (U).rom",4096,"bios"],["ATARIBAS.ROM",8192,"bios"],["ATARIOSA.ROM",10240,"bios"],["ATARIOSB.ROM",10240,"bios"],["ATARIXL.ROM",16384,"bios"],["AUDIO.ABS",6904,"game_data"],["AUDIOHED.BS1",1280,"game_data"],["AUDIOHED.BS6",1280,"game_data"],["AUDIOT.BS1",267636,"game_data"],["AUDIOT.BS6",267636,"game_data"],["Acorn/basic.rom",16384,"bios"],["Acorn/basic2.rom",16384,"bios"],["Amiga/Kickstart-v1.3-rev34.5-1987-Commodore-A500-A1000-A2000-CDTV.rom",262144,"bios"],["AppleII/apple2e-character.rom",4096,"bios"],["AppleIIgs/apple2gs.chr",4096,"bios"],["BB01R4_OS.ROM",16384,"bios"],["BBCMicro/dfs-2.26.rom",16384,"bios"],["BBCMicro/dfs09.rom",8192,"bios"],["BBCMicro/os12.rom",16384,"bios"],["BEGIN.ABS",12742,"game_data"],["BLOCKS.000",143876,"game_data"],["BLOOD.RFF",3536782,"game_data"],["BONUS.000",161022,"game_data"],["BONUS.0SC",51832,"game_data"],["BONUSMAP.000",6455,"game_data"],["BS-X.bin",1048576,"bios"],["CM32LN_CONTROL.ROM",65536,"bios"],["CM32L_CONTROL.ROM",65536,"bios"],["CM32L_PCM.ROM",1048576,"bios"],["CREDITS.ABS",15237,"game_data"],["Commodore1540/1540.bin",16384,"bios"],["Commodore1540/1541.bin",16384,"bios"],["Complex_4627.bin",1048576,"bios"],["Config/Music.xml",9224,"bios"],["D8039HLC.rom",2048,"bios"],["DOSBOX.SF2",32319396,"bios"],["DUKE.RTS",188954,"game_data"],["DUKE3D.GRP",11035779,"game_data"],["DiskII/state-machine-13.rom",256,"bios"],["DiskII/state-machine-16.rom",256,"bios"],["EGAGRAPH.ABS",320626,"game_data"],["END.0SC",17055,"game_data"],["Electron/ELK130E.rom",16384,"bios"],["Electron/basic.rom",16384,"bios"],["Enterprise/BASIC 1.0 - EPROM 1-2 (198x)(Enterprise).bin",8193,"bios"],["Enterprise/BASIC 1.0 - EPROM 2-2 (198x)(Enterprise).bin",8193,"bios"],["Enterprise/BASIC 1.1 - EPROM 1.1 (198x)(Enterprise).bin",16385,"bios"],["Enterprise/basic10.bin",16384,"bios"],["Enterprise/basic11.bin",16384,"bios"],["Enterprise/basic21.bin",16384,"bios"],["Enterprise/epdos.bin",32768,"bios"],["Enterprise/exdos.bin",16384,"bios"],["Enterprise/exos10.bin",32768,"bios"],["Enterprise/exos20.bin",32768,"bios"],["Enterprise/exos21.bin",32768,"bios"],["Enterprise/exos23.bin",65536,"bios"],["FINALE.ABS",11918,"game_data"],["FMT_ALL.ROM",4194496,"bios"],["FMT_DIC.ROM",524288,"bios"],["FMT_DOS.ROM",524288,"bios"],["FMT_F20.ROM",524288,"bios"],["FMT_FNT.ROM",262144,"bios"],["FMT_SYS.ROM",262144,"bios"],["FONT2.0FN",3819,"game_data"],["FONTBIG.0FN",6463,"game_data"],["FONTINY.0FN",3339,"game_data"],["FONTMN1.0FN",8982,"game_data"],["FONTMN2.0FN",6789,"game_data"],["FONTS.000",14542,"game_data"],["GAMEMAPS.ABS",20433,"game_data"],["GBA/gba_bios.bin",16384,"bios"],["GC/EUR/IPL.bin",2097152,"bios"],["GC/JAP/IPL.bin",2097152,"bios"],["GC/USA/IPL.bin",2097152,"bios"],["GC/dsp_coef.bin",4096,"bios"],["GC/dsp_rom.bin",8192,"bios"],["GC/font_japanese.bin",259626,"bios"],["GC/font_western.bin",6478,"bios"],["GUI.RFF",7145,"game_data"],["HUNTBGIN.RTC",54258,"game_data"],["HUNTBGIN.RTL",103316,"game_data"],["HUNTBGIN.WAD",6436871,"game_data"],["IANIM.BS1",18977,"game_data"],["IANIM.BS6",18977,"game_data"],["IANIM.VSI",18977,"game_data"],["INSTRUCT.0SC",145067,"game_data"],["JiffyDOS_1541-II.bin",16384,"bios"],["JiffyDOS_1571_repl310654.bin",32768,"bios"],["JiffyDOS_1581.bin",32768,"bios"],["JiffyDOS_C128.bin",16384,"bios"],["JiffyDOS_C64.bin",8192,"bios"],["JiffyDOS_SX-64.bin",8192,"bios"],["LEVEL0.000",20390,"game_data"],["LEVEL0.001",36234,"game_data"],["LEVEL0.002",20642,"game_data"],["Load/Firmware/rtl_bt/rtl8723a_fw.bin",24548,"bios"],["Load/Firmware/rtl_bt/rtl8723b_fw.bin",45048,"bios"],["Load/Firmware/rtl_bt/rtl8723d_fw.bin",52080,"bios"],["Load/Firmware/rtl_bt/rtl8761a_fw.bin",74488,"bios"],["Load/Firmware/rtl_bt/rtl8761bu_fw.bin",44484,"bios"],["Load/Firmware/rtl_bt/rtl8821a_fw.bin",37420,"bios"],["Load/Firmware/rtl_bt/rtl8821c_fw.bin",54636,"bios"],["Load/Firmware/rtl_bt/rtl8822b_fw.bin",51176,"bios"],["Load/Firmware/rtl_bt/rtl8822cu_fw.bin",38207,"bios"],["Load/Firmware/rtl_bt/rtl8851bu_fw.bin",49760,"bios"],["Load/Firmware/rtl_bt/rtl8852au_fw.bin",67188,"bios"],["Load/Firmware/rtl_bt/rtl8852btu_fw.bin",128140,"bios"],["Load/Firmware/rtl_bt/rtl8852bu_fw.bin",129733,"bios"],["Load/Firmware/rtl_bt/rtl8852cu_fw.bin",113336,"bios"],["Load/Firmware/rtl_bt/rtl8922au_fw.bin",71360,"bios"],["Load/WiiSD.raw",134217728,"bios"],["LucidaConsole_12.abc",2332,"bios"],["LucidaConsole_12.tga",131090,"bios"],["MACRO.1",1032,"game_data"],["MAINCHAR.000",156281,"game_data"],["MAPHEAD.BS1",834,"game_data"],["MAPTEMP.BS1",90786,"game_data"],["MAR_EX0.ROM",524288,"bios"],["MAR_EX1.ROM",524288,"bios"],["MAR_EX2.ROM",524288,"bios"],["MAR_Line truncated
{"files":[["32X_G_BIOS.BIN",256,"bios"],["32X_M_BIOS.BIN",2048,"bios"],["32X_S_BIOS.BIN",1024,"bios"],["3do_arcade_saot.bin",524288,"bios"],["5200.rom",2048,"bios"],["64DD_IPL.bin",4194304,"bios"],["64DD_IPL_DEV.n64",4194304,"bios"],["64DD_IPL_JP.n64",4194304,"bios"],["64DD_IPL_US.n64",4194304,"bios"],["6570-036.rom",2048,"bios"],["68HC05.rom",8192,"bios"],["7800 BIOS (E).rom",16384,"bios"],["7800 BIOS (U).rom",4096,"bios"],["ATARIBAS.ROM",8192,"bios"],["ATARIOSA.ROM",10240,"bios"],["ATARIOSB.ROM",10240,"bios"],["ATARIXL.ROM",16384,"bios"],["AUDIO.ABS",6904,"game_data"],["AUDIOHED.BS1",1280,"game_data"],["AUDIOHED.BS6",1280,"game_data"],["AUDIOT.BS1",267636,"game_data"],["AUDIOT.BS6",267636,"game_data"],["Acorn/basic.rom",16384,"bios"],["Acorn/basic2.rom",16384,"bios"],["Amiga/Kickstart-v1.3-rev34.5-1987-Commodore-A500-A1000-A2000-CDTV.rom",262144,"bios"],["AppleII/apple2e-character.rom",4096,"bios"],["AppleIIgs/apple2gs.chr",4096,"bios"],["BB01R4_OS.ROM",16384,"bios"],["BBCMicro/dfs-2.26.rom",16384,"bios"],["BBCMicro/dfs09.rom",8192,"bios"],["BBCMicro/os12.rom",16384,"bios"],["BEGIN.ABS",12742,"game_data"],["BLOCKS.000",143876,"game_data"],["BLOOD.RFF",3536782,"game_data"],["BONUS.000",161022,"game_data"],["BONUS.0SC",51832,"game_data"],["BONUSMAP.000",6455,"game_data"],["BS-X.bin",1048576,"bios"],["CM32LN_CONTROL.ROM",65536,"bios"],["CM32L_CONTROL.ROM",65536,"bios"],["CM32L_PCM.ROM",1048576,"bios"],["CREDITS.ABS",15237,"game_data"],["Commodore1540/1540.bin",16384,"bios"],["Commodore1540/1541.bin",16384,"bios"],["Complex_4627.bin",1048576,"bios"],["Config/Music.xml",9224,"bios"],["D8039HLC.rom",2048,"bios"],["DOSBOX.SF2",32319396,"bios"],["DUKE.RTS",188954,"game_data"],["DUKE3D.GRP",11035779,"game_data"],["DiskII/state-machine-13.rom",256,"bios"],["DiskII/state-machine-16.rom",256,"bios"],["EGAGRAPH.ABS",320626,"game_data"],["END.0SC",17055,"game_data"],["Electron/ELK130E.rom",16384,"bios"],["Electron/basic.rom",16384,"bios"],["Enterprise/BASIC 1.0 - EPROM 1-2 (198x)(Enterprise).bin",8193,"bios"],["Enterprise/BASIC 1.0 - EPROM 2-2 (198x)(Enterprise).bin",8193,"bios"],["Enterprise/BASIC 1.1 - EPROM 1.1 (198x)(Enterprise).bin",16385,"bios"],["Enterprise/basic10.bin",16384,"bios"],["Enterprise/basic11.bin",16384,"bios"],["Enterprise/basic21.bin",16384,"bios"],["Enterprise/epdos.bin",32768,"bios"],["Enterprise/exdos.bin",16384,"bios"],["Enterprise/exos10.bin",32768,"bios"],["Enterprise/exos20.bin",32768,"bios"],["Enterprise/exos21.bin",32768,"bios"],["Enterprise/exos23.bin",65536,"bios"],["FINALE.ABS",11918,"game_data"],["FMT_ALL.ROM",4194496,"bios"],["FMT_DIC.ROM",524288,"bios"],["FMT_DOS.ROM",524288,"bios"],["FMT_F20.ROM",524288,"bios"],["FMT_FNT.ROM",262144,"bios"],["FMT_SYS.ROM",262144,"bios"],["FONT2.0FN",3819,"game_data"],["FONTBIG.0FN",6463,"game_data"],["FONTINY.0FN",3339,"game_data"],["FONTMN1.0FN",8982,"game_data"],["FONTMN2.0FN",6789,"game_data"],["FONTS.000",14542,"game_data"],["GAMEMAPS.ABS",20433,"game_data"],["GBA/gba_bios.bin",16384,"bios"],["GC/EUR/IPL.bin",2097152,"bios"],["GC/JAP/IPL.bin",2097152,"bios"],["GC/USA/IPL.bin",2097152,"bios"],["GC/dsp_coef.bin",4096,"bios"],["GC/dsp_rom.bin",8192,"bios"],["GC/font_japanese.bin",259626,"bios"],["GC/font_western.bin",6478,"bios"],["GUI.RFF",7145,"game_data"],["HUNTBGIN.RTC",54258,"game_data"],["HUNTBGIN.RTL",103316,"game_data"],["HUNTBGIN.WAD",6436871,"game_data"],["IANIM.BS1",18977,"game_data"],["IANIM.BS6",18977,"game_data"],["IANIM.VSI",18977,"game_data"],["INSTRUCT.0SC",145067,"game_data"],["JiffyDOS_1541-II.bin",16384,"bios"],["JiffyDOS_1571_repl310654.bin",32768,"bios"],["JiffyDOS_1581.bin",32768,"bios"],["JiffyDOS_C128.bin",16384,"bios"],["JiffyDOS_C64.bin",8192,"bios"],["JiffyDOS_SX-64.bin",8192,"bios"],["LEVEL0.000",20390,"game_data"],["LEVEL0.001",36234,"game_data"],["LEVEL0.002",20642,"game_data"],["Load/Firmware/rtl_bt/rtl8723a_fw.bin",24548,"bios"],["Load/Firmware/rtl_bt/rtl8723b_fw.bin",45048,"bios"],["Load/Firmware/rtl_bt/rtl8723d_fw.bin",52080,"bios"],["Load/Firmware/rtl_bt/rtl8761a_fw.bin",74488,"bios"],["Load/Firmware/rtl_bt/rtl8761bu_fw.bin",44484,"bios"],["Load/Firmware/rtl_bt/rtl8821a_fw.bin",37420,"bios"],["Load/Firmware/rtl_bt/rtl8821c_fw.bin",54636,"bios"],["Load/Firmware/rtl_bt/rtl8822b_fw.bin",51176,"bios"],["Load/Firmware/rtl_bt/rtl8822cu_fw.bin",38207,"bios"],["Load/Firmware/rtl_bt/rtl8851bu_fw.bin",49760,"bios"],["Load/Firmware/rtl_bt/rtl8852au_fw.bin",67188,"bios"],["Load/Firmware/rtl_bt/rtl8852btu_fw.bin",128140,"bios"],["Load/Firmware/rtl_bt/rtl8852bu_fw.bin",129733,"bios"],["Load/Firmware/rtl_bt/rtl8852cu_fw.bin",113336,"bios"],["Load/Firmware/rtl_bt/rtl8922au_fw.bin",71360,"bios"],["Load/WiiSD.raw",134217728,"bios"],["LucidaConsole_12.abc",2332,"bios"],["LucidaConsole_12.tga",131090,"bios"],["MACRO.1",1032,"game_data"],["MAINCHAR.000",156281,"game_data"],["MAPHEAD.BS1",834,"game_data"],["MAPTEMP.BS1",90786,"game_data"],["MAR_EX0.ROM",524288,"bios"],["MAR_EX1.ROM",524288,"bios"],["MAR_EX2.ROM",524288,"bios"],["MAR_Line truncated
+1 -1
View File
@@ -1 +1 @@
{"files":[["32X_G_BIOS.BIN",256,"bios"],["32X_M_BIOS.BIN",2048,"bios"],["32X_S_BIOS.BIN",1024,"bios"],["3do_arcade_saot.bin",524288,"bios"],["3ds/Citra/sysdata/aes_keys.txt",7357,"bios"],["3ds/Citra/sysdata/boot9.bin",65536,"bios"],["3ds/Citra/sysdata/seeddb.bin",53264,"bios"],["3ds/Citra/sysdata/shared_font.bin",3145728,"bios"],["5200.rom",2048,"bios"],["64DD_IPL.bin",4194304,"bios"],["64DD_IPL_DEV.n64",4194304,"bios"],["64DD_IPL_JP.n64",4194304,"bios"],["64DD_IPL_US.n64",4194304,"bios"],["6570-036.rom",2048,"bios"],["68HC05.rom",8192,"bios"],["7800 BIOS (E).rom",16384,"bios"],["7800 BIOS (U).rom",4096,"bios"],["ATARIBAS.ROM",8192,"bios"],["ATARIOSA.ROM",10240,"bios"],["ATARIOSB.ROM",10240,"bios"],["ATARIXL.ROM",16384,"bios"],["Acorn/basic.rom",16384,"bios"],["Amiga/Kickstart-v1.3-rev34.5-1987-Commodore-A500-A1000-A2000-CDTV.rom",262144,"bios"],["AppleII/apple2e-character.rom",4096,"bios"],["AppleIIgs/apple2gs.chr",4096,"bios"],["BB01R4_OS.ROM",16384,"bios"],["BBCMicro/dfs09.rom",8192,"bios"],["BLOCKS.000",143876,"game_data"],["BLOOD.RFF",3536782,"game_data"],["BONUS.000",161022,"game_data"],["BONUS.0SC",51832,"game_data"],["BONUSMAP.000",6455,"game_data"],["BS-X.bin",1048576,"bios"],["CARTS.SHA",33152,"bios"],["CM32LN_CONTROL.ROM",65536,"bios"],["CM32L_CONTROL.ROM",65536,"bios"],["CM32L_PCM.ROM",1048576,"bios"],["CMOS.ROM",52,"bios"],["CYRILLIC.FNT",2048,"bios"],["Citra/sysdata/aes_keys.txt",7357,"bios"],["Citra/sysdata/boot9.bin",65536,"bios"],["Citra/sysdata/seeddb.bin",53264,"bios"],["Citra/sysdata/shared_font.bin",3145728,"bios"],["Commodore1540/1540.bin",16384,"bios"],["Commodore1540/1541.bin",16384,"bios"],["Complex_4627.bin",1048576,"bios"],["D8039HLC.rom",2048,"bios"],["DEFAULT.FNT",2048,"bios"],["DISK.ROM",16384,"bios"],["DOSBOX.SF2",32319396,"bios"],["DUKE.RTS",188954,"game_data"],["DUKE3D.GRP",11035779,"game_data"],["Databases/colecodb.xml",158187,"bios"],["Databases/msxcaswavdb.xml",819679,"bios"],["Databases/msxromdb.xml",930986,"bios"],["Databases/msxsysromdb.xml",157542,"bios"],["Databases/segadb.xml",82318,"bios"],["Databases/svidb.xml",261070,"bios"],["DiskII/state-machine-13.rom",256,"bios"],["DiskII/state-machine-16.rom",256,"bios"],["END.0SC",17055,"game_data"],["Electron/ELK130E.rom",16384,"bios"],["Electron/basic.rom",16384,"bios"],["FMPAC.ROM",65536,"bios"],["FMT_ALL.ROM",4194496,"bios"],["FMT_DIC.ROM",524288,"bios"],["FMT_DOS.ROM",524288,"bios"],["FMT_F20.ROM",524288,"bios"],["FMT_FNT.ROM",262144,"bios"],["FMT_SYS.ROM",262144,"bios"],["FONT2.0FN",3819,"game_data"],["FONTBIG.0FN",6463,"game_data"],["FONTINY.0FN",3339,"game_data"],["FONTMN1.0FN",8982,"game_data"],["FONTMN2.0FN",6789,"game_data"],["FONTS.000",14542,"game_data"],["GBA/gba_bios.bin",16384,"bios"],["GC/EUR/IPL.bin",2097152,"bios"],["GC/EUR/IPL_TRI.bin",2097152,"bios"],["GC/JAP/IPL.bin",2097152,"bios"],["GC/JAP/IPL_TRI.bin",2097152,"bios"],["GC/USA/IPL.bin",2097152,"bios"],["GC/USA/IPL_TRI.bin",2097152,"bios"],["GC/dsp_coef.bin",4096,"bios"],["GC/dsp_rom.bin",8192,"bios"],["GC/font_japanese.bin",259626,"bios"],["GC/font_western.bin",6478,"bios"],["GMASTER.ROM",16384,"bios"],["GMASTER2.ROM",131072,"bios"],["GUI.RFF",7145,"game_data"],["HUNTBGIN.RTC",54258,"game_data"],["HUNTBGIN.RTL",103316,"game_data"],["HUNTBGIN.WAD",6436871,"game_data"],["Help/Script56.chm",1351013,"bios"],["INSTRUCT.0SC",145067,"game_data"],["INTERNAT.FNT",2048,"bios"],["ITALIC.FNT",2048,"bios"],["JAPANESE.FNT",2048,"bios"],["JiffyDOS_1541-II.bin",16384,"bios"],["JiffyDOS_1571_repl310654.bin",32768,"bios"],["JiffyDOS_1581.bin",32768,"bios"],["JiffyDOS_C128.bin",16384,"bios"],["JiffyDOS_C64.bin",8192,"bios"],["JiffyDOS_SX-64.bin",8192,"bios"],["KANJI.ROM",131072,"bios"],["KOREAN.FNT",2048,"bios"],["LEVEL0.000",20390,"game_data"],["LEVEL0.001",36234,"game_data"],["LEVEL0.002",20642,"game_data"],["Load/Firmware/rtl_bt/rtl8723a_fw.bin",24548,"bios"],["Load/Firmware/rtl_bt/rtl8723b_fw.bin",45048,"bios"],["Load/Firmware/rtl_bt/rtl8723d_fw.bin",52080,"bios"],["Load/Firmware/rtl_bt/rtl8761a_fw.bin",74488,"bios"],["Load/Firmware/rtl_bt/rtl8761bu_fw.bin",44484,"bios"],["Load/Firmware/rtl_bt/rtl8821a_fw.bin",37420,"bios"],["Load/Firmware/rtl_bt/rtl8821c_fw.bin",54636,"bios"],["Load/Firmware/rtl_bt/rtl8822b_fw.bin",51176,"bios"],["Load/Firmware/rtl_bt/rtl8822cu_fw.bin",38207,"bios"],["Load/Firmware/rtl_bt/rtl8851bu_fw.bin",49760,"bios"],["Load/Firmware/rtl_bt/rtl8852au_fw.bin",67188,"bios"],["Load/Firmware/rtl_bt/rtl8852btu_fw.bin",128140,"bios"],["Load/Firmware/rtl_bt/rtl8852bu_fw.bin",129733,"bios"],["Load/Firmware/rtl_bt/rtl8852cu_fw.bin",113336,"bios"],["Load/Firmware/rtl_bt/rtl8922au_fw.bin",71360,"bios"],["Load/WiiSD.raw",134217728,"bios"],["MACRO.1",1032,"game_data"],["MAINCHAR.000",156281,"game_data"],["MAR_EX0.ROM",524288,"bios"],["MAR_EX1.ROM",524288,"bios"],["MAR_EX2.ROM",524288,"bios"],["MAR_EX3.ROM",524288,"bios"],["MENU.000",124306,"game_data"],["MENUSNG.PSM",79609,"game_data"],["MSX.ROM",32768,"bios"],["MSX/disk.rom",16384,"bios"],["MSX/fmpac.rom",Line truncated
{"files":[["32X_G_BIOS.BIN",256,"bios"],["32X_M_BIOS.BIN",2048,"bios"],["32X_S_BIOS.BIN",1024,"bios"],["3do_arcade_saot.bin",524288,"bios"],["3ds/Citra/sysdata/aes_keys.txt",7357,"bios"],["3ds/Citra/sysdata/boot9.bin",65536,"bios"],["3ds/Citra/sysdata/seeddb.bin",53264,"bios"],["3ds/Citra/sysdata/shared_font.bin",3145728,"bios"],["5200.rom",2048,"bios"],["64DD_IPL.bin",4194304,"bios"],["64DD_IPL_DEV.n64",4194304,"bios"],["64DD_IPL_JP.n64",4194304,"bios"],["64DD_IPL_US.n64",4194304,"bios"],["6570-036.rom",2048,"bios"],["68HC05.rom",8192,"bios"],["7800 BIOS (E).rom",16384,"bios"],["7800 BIOS (U).rom",4096,"bios"],["ATARIBAS.ROM",8192,"bios"],["ATARIOSA.ROM",10240,"bios"],["ATARIOSB.ROM",10240,"bios"],["ATARIXL.ROM",16384,"bios"],["Acorn/basic.rom",16384,"bios"],["Amiga/Kickstart-v1.3-rev34.5-1987-Commodore-A500-A1000-A2000-CDTV.rom",262144,"bios"],["AppleII/apple2e-character.rom",4096,"bios"],["AppleIIgs/apple2gs.chr",4096,"bios"],["BB01R4_OS.ROM",16384,"bios"],["BBCMicro/dfs09.rom",8192,"bios"],["BLOCKS.000",143876,"game_data"],["BLOOD.RFF",3536782,"game_data"],["BONUS.000",161022,"game_data"],["BONUS.0SC",51832,"game_data"],["BONUSMAP.000",6455,"game_data"],["BS-X.bin",1048576,"bios"],["CARTS.SHA",33152,"bios"],["CM32LN_CONTROL.ROM",65536,"bios"],["CM32L_CONTROL.ROM",65536,"bios"],["CM32L_PCM.ROM",1048576,"bios"],["CMOS.ROM",52,"bios"],["CYRILLIC.FNT",2048,"bios"],["Citra/sysdata/aes_keys.txt",7357,"bios"],["Citra/sysdata/boot9.bin",65536,"bios"],["Citra/sysdata/seeddb.bin",53264,"bios"],["Citra/sysdata/shared_font.bin",3145728,"bios"],["Commodore1540/1540.bin",16384,"bios"],["Commodore1540/1541.bin",16384,"bios"],["Complex_4627.bin",1048576,"bios"],["D8039HLC.rom",2048,"bios"],["DEFAULT.FNT",2048,"bios"],["DISK.ROM",16384,"bios"],["DOSBOX.SF2",32319396,"bios"],["DUKE.RTS",188954,"game_data"],["DUKE3D.GRP",11035779,"game_data"],["Databases/colecodb.xml",158187,"bios"],["Databases/msxcaswavdb.xml",819679,"bios"],["Databases/msxromdb.xml",930986,"bios"],["Databases/msxsysromdb.xml",157542,"bios"],["Databases/segadb.xml",82318,"bios"],["Databases/svidb.xml",261070,"bios"],["DiskII/state-machine-13.rom",256,"bios"],["DiskII/state-machine-16.rom",256,"bios"],["END.0SC",17055,"game_data"],["Electron/ELK130E.rom",16384,"bios"],["Electron/basic.rom",16384,"bios"],["FMPAC.ROM",65536,"bios"],["FMT_ALL.ROM",4194496,"bios"],["FMT_DIC.ROM",524288,"bios"],["FMT_DOS.ROM",524288,"bios"],["FMT_F20.ROM",524288,"bios"],["FMT_FNT.ROM",262144,"bios"],["FMT_SYS.ROM",262144,"bios"],["FONT2.0FN",3819,"game_data"],["FONTBIG.0FN",6463,"game_data"],["FONTINY.0FN",3339,"game_data"],["FONTMN1.0FN",8982,"game_data"],["FONTMN2.0FN",6789,"game_data"],["FONTS.000",14542,"game_data"],["GBA/gba_bios.bin",16384,"bios"],["GC/EUR/IPL.bin",2097152,"bios"],["GC/EUR/IPL_TRI.bin",2097152,"bios"],["GC/JAP/IPL.bin",2097152,"bios"],["GC/JAP/IPL_TRI.bin",2097152,"bios"],["GC/USA/IPL.bin",2097152,"bios"],["GC/USA/IPL_TRI.bin",2097152,"bios"],["GC/dsp_coef.bin",4096,"bios"],["GC/dsp_rom.bin",8192,"bios"],["GC/font_japanese.bin",259626,"bios"],["GC/font_western.bin",6478,"bios"],["GMASTER.ROM",16384,"bios"],["GMASTER2.ROM",131072,"bios"],["GUI.RFF",7145,"game_data"],["HUNTBGIN.RTC",54258,"game_data"],["HUNTBGIN.RTL",103316,"game_data"],["HUNTBGIN.WAD",6436871,"game_data"],["Help/Script56.chm",1351013,"bios"],["INSTRUCT.0SC",145067,"game_data"],["INTERNAT.FNT",2048,"bios"],["ITALIC.FNT",2048,"bios"],["JAPANESE.FNT",2048,"bios"],["JiffyDOS_1541-II.bin",16384,"bios"],["JiffyDOS_1571_repl310654.bin",32768,"bios"],["JiffyDOS_1581.bin",32768,"bios"],["JiffyDOS_C128.bin",16384,"bios"],["JiffyDOS_C64.bin",8192,"bios"],["JiffyDOS_SX-64.bin",8192,"bios"],["KANJI.ROM",131072,"bios"],["KOREAN.FNT",2048,"bios"],["LEVEL0.000",20390,"game_data"],["LEVEL0.001",36234,"game_data"],["LEVEL0.002",20642,"game_data"],["Load/Firmware/rtl_bt/rtl8723a_fw.bin",24548,"bios"],["Load/Firmware/rtl_bt/rtl8723b_fw.bin",45048,"bios"],["Load/Firmware/rtl_bt/rtl8723d_fw.bin",52080,"bios"],["Load/Firmware/rtl_bt/rtl8761a_fw.bin",74488,"bios"],["Load/Firmware/rtl_bt/rtl8761bu_fw.bin",44484,"bios"],["Load/Firmware/rtl_bt/rtl8821a_fw.bin",37420,"bios"],["Load/Firmware/rtl_bt/rtl8821c_fw.bin",54636,"bios"],["Load/Firmware/rtl_bt/rtl8822b_fw.bin",51176,"bios"],["Load/Firmware/rtl_bt/rtl8822cu_fw.bin",38207,"bios"],["Load/Firmware/rtl_bt/rtl8851bu_fw.bin",49760,"bios"],["Load/Firmware/rtl_bt/rtl8852au_fw.bin",67188,"bios"],["Load/Firmware/rtl_bt/rtl8852btu_fw.bin",128140,"bios"],["Load/Firmware/rtl_bt/rtl8852bu_fw.bin",129733,"bios"],["Load/Firmware/rtl_bt/rtl8852cu_fw.bin",113336,"bios"],["Load/Firmware/rtl_bt/rtl8922au_fw.bin",71360,"bios"],["Load/WiiSD.raw",134217728,"bios"],["MACRO.1",1032,"game_data"],["MAINCHAR.000",156281,"game_data"],["MAR_EX0.ROM",524288,"bios"],["MAR_EX1.ROM",524288,"bios"],["MAR_EX2.ROM",524288,"bios"],["MAR_EX3.ROM",524288,"bios"],["MENU.000",124306,"game_data"],["MENUSNG.PSM",79609,"game_data"],["MSX.ROM",32768,"bios"],["MSX/disk.rom",16384,"bios"],["MSX/fmpac.rom",Line truncated
+1 -1
View File
@@ -1 +1 @@
{"arcade":{"fbneo":{"file":"arcade/fbneo.json","gzip_size":115955,"sha256":"88961c9a6be804772baa767f6ee3a9162478c30c60fe55c8363d8fe4fbc909da","size":423362},"flycast":{"file":"arcade/flycast.json","gzip_size":1841,"sha256":"957189a6e993fda08e21c90ffc6ad94aed9ca9e6dd9ed8c7724e29db036791cb","size":5441},"mame":{"file":"arcade/mame.json","gzip_size":154527,"sha256":"b12010301293a218ecfac45706d81a26c3ac0c27eebeca7409bc8f6ce0163e9e","size":604648},"mame0258":{"file":"arcade/mame0258.json","gzip_size":141151,"sha256":"7ecff1979ed2ac123ea3ba4825dde6e2c31906415165fabefc9af93fa48a6de4","size":556059},"mame0278":{"file":"arcade/mame0278.json","gzip_size":148595,"sha256":"7881e3516f451ec83e886aaf41ebac9d60f497e1f954f49749d52890d7955cbf","size":582797},"mame2000":{"file":"arcade/mame2000.json","gzip_size":8624,"sha256":"f0d69b0872ecfc2845eeb7304543e5b17c11db82060b5514c206b6962ed08263","size":23316},"mame2003":{"file":"arcade/mame2003.json","gzip_size":17456,"sha256":"3b44e2a92c80a8f104b7781fcc1c776d025ad2cf4986638efa3ea041baa9a56d","size":48929},"mame2003_plus":{"file":"arcade/mame2003_plus.json","gzip_size":21077,"sha256":"2857283556fa49c20d8160ed113465bf0c2e643ed0b8acd2d38266a78be8e25e","size":62373},"mame2010":{"file":"arcade/mame2010.json","gzip_size":30869,"sha256":"50b1b381b4b07463f69f1f4c512dd1c98a78d4d0e9c3aeaf49b7b1b007b42152","size":96528},"mame2015":{"file":"arcade/mame2015.json","gzip_size":86990,"sha256":"afeb8c32f651421f74c6de057b0c0b837acd14c533b5160bb31f4411e0afffec","size":360117},"mame2016":{"file":"arcade/mame2016.json","gzip_size":101249,"sha256":"1f75147977d9d082a38b8f8e47a02b301f3d6e6db97c9b9282f3e7f27f26ee47","size":406190}},"containers":{"7z":{"description":"Per-file CRCs and sizes sit in the header at the end of the archive, LZMA-compressed by default (kEncodedHeader). One LZMA decode of the header, independent of the archive size, gives them. Python's lzma is an optional module.","re_read":true,"signature":{"bytes_hex":"377abcaf271c","offset":0},"source_ref":"ip7z/7zip@b88c6bc0 DOC/7zFormat.txt:171-189, DOC/7zFormat.txt:318-334, CPP/7zip/Archive/7z/7zHandlerOut.cpp:859"},"cd-sector":{"description":"User data of a CD sector. In a 2048-byte image (.iso) sector N starts at N * 2048. In a raw 2352-byte image it starts at N * 2352 plus 16 for Mode 1 and plus 24 for Mode 2 Form 1 (12 sync bytes, a 4-byte header, and for Mode 2 an 8-byte subheader).","re_read":true,"sector_size":{"iso":2048,"raw":2352},"source_ref":"stenzek/duckstation@526b4a12 src/util/iso_reader.cpp:157-171, src/util/cd_image.h:28-34","user_data_offset":{"mode1":16,"mode2":24}},"chd-v5":{"description":"MAME compressed hunks, big-endian. The header holds sizes, codecs and hashes only, never the system or the region. The overall SHA-1 is what MAME software lists record. Neither SHA-1 equals a Redump track hash: chdman hashes 2448-byte frames (subcode included, zero-filled), pads each track to 4 frames and byte-swaps audio, all tracks in one stream.","fields":{"compressors":{"encoding":"fourcc4","length":16,"offset":16},"header_length":{"encoding":"u32be","length":4,"offset":8,"value":124},"hunk_bytes":{"encoding":"u32be","length":4,"offset":56},"logical_bytes":{"encoding":"u64be","length":8,"offset":32},"map_offset":{"encoding":"u64be","length":8,"offset":40},"metadata_offset":{"encoding":"u64be","length":8,"offset":48},"overall_sha1":{"encoding":"hex","length":20,"offset":84},"parent_sha1":{"encoding":"hex","length":20,"offset":104},"raw_sha1":{"encoding":"hex","length":20,"offset":64},"unit_bytes":{"encoding":"u32be","length":4,"offset":60},"version":{"encoding":"u32be","length":4,"offset":12,"value":5}},"magic":{"bytes":"MComprHD","offset":0},"media_tags":{"CHCD":"cd","CHGD":"gd-rom","CHGT":"gd-rom","CHT2":"cd","CHTR":"cd","DVD ":"dvd","GDDD":"hard-disk"},"metadata_entry":{"description":"16-byte entry header, payload follows. Linked list from metadata_offset.","flags":{"encoding":"u8","length":1,"offset":4},"length":{"encoding":"u24be","length":3,"offset":5},"next":{"encoding":"u64be","length":8,"offset":8},"tag":{"encoding":"ascii","length":4,"offset":0}},"re_read":true,"source_ref":"mamedev/mame@f34f0250 src/lib/util/chd.h:148-162, src/lib/util/chd.cpp:2021-2061, src/lib/util/chd.cpp:2822-2831, src/lib/util/chd.h:212-235, src/lib/util/chd.cpp:1709-1749, src/tools/chdman.cpp:443-445, src/tools/chdman.cpp:2195-2203, src/lib/util/cdrom.cpp:2669-2672, src/emu/romload.cpp:1211"},"gamecube-disc":{"description":"Where disc offset 0 sits inside each container Dolphin opens.","formats":{"ciso":{"disc_header_at":32768,"magic":"CISO"},"gcz":{"disc_header_at":null,"magic_u32le":2970337281,"note":"block 0 is zlib unless bit 63 of its pointer is set"},"iso":{"disc_header_at":0,"magic":null},"rvz":{"disc_header_at":88,"magic":"RVZ\u0001","note":"first 0x80 disc bytes copied into header 2"},"wbfs":{"disc_header_at":null,"magic":"WBFS","note":"disc offset 0 at wlba[0] << wbfs_sector_shift"},"wia":{"disc_header_at":88,"magic":"WIA\u0001","note":Line truncated
{"arcade":{"fbneo":{"file":"arcade/fbneo.json","gzip_size":115955,"sha256":"88961c9a6be804772baa767f6ee3a9162478c30c60fe55c8363d8fe4fbc909da","size":423362},"flycast":{"file":"arcade/flycast.json","gzip_size":1841,"sha256":"957189a6e993fda08e21c90ffc6ad94aed9ca9e6dd9ed8c7724e29db036791cb","size":5441},"mame":{"file":"arcade/mame.json","gzip_size":154527,"sha256":"b12010301293a218ecfac45706d81a26c3ac0c27eebeca7409bc8f6ce0163e9e","size":604648},"mame0258":{"file":"arcade/mame0258.json","gzip_size":141151,"sha256":"7ecff1979ed2ac123ea3ba4825dde6e2c31906415165fabefc9af93fa48a6de4","size":556059},"mame0278":{"file":"arcade/mame0278.json","gzip_size":148595,"sha256":"7881e3516f451ec83e886aaf41ebac9d60f497e1f954f49749d52890d7955cbf","size":582797},"mame2000":{"file":"arcade/mame2000.json","gzip_size":8624,"sha256":"f0d69b0872ecfc2845eeb7304543e5b17c11db82060b5514c206b6962ed08263","size":23316},"mame2003":{"file":"arcade/mame2003.json","gzip_size":17456,"sha256":"3b44e2a92c80a8f104b7781fcc1c776d025ad2cf4986638efa3ea041baa9a56d","size":48929},"mame2003_plus":{"file":"arcade/mame2003_plus.json","gzip_size":21077,"sha256":"2857283556fa49c20d8160ed113465bf0c2e643ed0b8acd2d38266a78be8e25e","size":62373},"mame2010":{"file":"arcade/mame2010.json","gzip_size":30869,"sha256":"50b1b381b4b07463f69f1f4c512dd1c98a78d4d0e9c3aeaf49b7b1b007b42152","size":96528},"mame2015":{"file":"arcade/mame2015.json","gzip_size":86990,"sha256":"afeb8c32f651421f74c6de057b0c0b837acd14c533b5160bb31f4411e0afffec","size":360117},"mame2016":{"file":"arcade/mame2016.json","gzip_size":101249,"sha256":"1f75147977d9d082a38b8f8e47a02b301f3d6e6db97c9b9282f3e7f27f26ee47","size":406190}},"containers":{"7z":{"description":"Per-file CRCs and sizes sit in the header at the end of the archive, LZMA-compressed by default (kEncodedHeader). One LZMA decode of the header, independent of the archive size, gives them. Python's lzma is an optional module.","re_read":true,"signature":{"bytes_hex":"377abcaf271c","offset":0},"source_ref":"ip7z/7zip@b88c6bc0 DOC/7zFormat.txt:171-189, DOC/7zFormat.txt:318-334, CPP/7zip/Archive/7z/7zHandlerOut.cpp:859"},"cd-sector":{"description":"User data of a CD sector. In a 2048-byte image (.iso) sector N starts at N * 2048. In a raw 2352-byte image it starts at N * 2352 plus 16 for Mode 1 and plus 24 for Mode 2 Form 1 (12 sync bytes, a 4-byte header, and for Mode 2 an 8-byte subheader).","re_read":true,"sector_size":{"iso":2048,"raw":2352},"source_ref":"stenzek/duckstation@526b4a12 src/util/iso_reader.cpp:157-171, src/util/cd_image.h:28-34","user_data_offset":{"mode1":16,"mode2":24}},"chd-v5":{"description":"MAME compressed hunks, big-endian. The header holds sizes, codecs and hashes only, never the system or the region. The overall SHA-1 is what MAME software lists record. Neither SHA-1 equals a Redump track hash: chdman hashes 2448-byte frames (subcode included, zero-filled), pads each track to 4 frames and byte-swaps audio, all tracks in one stream.","fields":{"compressors":{"encoding":"fourcc4","length":16,"offset":16},"header_length":{"encoding":"u32be","length":4,"offset":8,"value":124},"hunk_bytes":{"encoding":"u32be","length":4,"offset":56},"logical_bytes":{"encoding":"u64be","length":8,"offset":32},"map_offset":{"encoding":"u64be","length":8,"offset":40},"metadata_offset":{"encoding":"u64be","length":8,"offset":48},"overall_sha1":{"encoding":"hex","length":20,"offset":84},"parent_sha1":{"encoding":"hex","length":20,"offset":104},"raw_sha1":{"encoding":"hex","length":20,"offset":64},"unit_bytes":{"encoding":"u32be","length":4,"offset":60},"version":{"encoding":"u32be","length":4,"offset":12,"value":5}},"magic":{"bytes":"MComprHD","offset":0},"media_tags":{"CHCD":"cd","CHGD":"gd-rom","CHGT":"gd-rom","CHT2":"cd","CHTR":"cd","DVD ":"dvd","GDDD":"hard-disk"},"metadata_entry":{"description":"16-byte entry header, payload follows. Linked list from metadata_offset.","flags":{"encoding":"u8","length":1,"offset":4},"length":{"encoding":"u24be","length":3,"offset":5},"next":{"encoding":"u64be","length":8,"offset":8},"tag":{"encoding":"ascii","length":4,"offset":0}},"re_read":true,"source_ref":"mamedev/mame@f34f0250 src/lib/util/chd.h:148-162, src/lib/util/chd.cpp:2021-2061, src/lib/util/chd.cpp:2822-2831, src/lib/util/chd.h:212-235, src/lib/util/chd.cpp:1709-1749, src/tools/chdman.cpp:443-445, src/tools/chdman.cpp:2195-2203, src/lib/util/cdrom.cpp:2669-2672, src/emu/romload.cpp:1211"},"gamecube-disc":{"description":"Where disc offset 0 sits inside each container Dolphin opens.","formats":{"ciso":{"disc_header_at":32768,"magic":"CISO"},"gcz":{"disc_header_at":null,"magic_u32le":2970337281,"note":"block 0 is zlib unless bit 63 of its pointer is set"},"iso":{"disc_header_at":0,"magic":null},"rvz":{"disc_header_at":88,"magic":"RVZ\u0001","note":"first 0x80 disc bytes copied into header 2"},"wbfs":{"disc_header_at":null,"magic":"WBFS","note":"disc offset 0 at wlba[0] << wbfs_sector_shift"},"wia":{"disc_header_at":88,"magic":"WIA\u0001","note":Line truncated
+84 -34
View File
@@ -29,10 +29,10 @@
}
],
"standalone_copies": [],
"total_files": 9573,
"total_size": 9667998582,
"pack_files": 13217,
"pack_size": 9771555925,
"total_files": 9578,
"total_size": 9669573515,
"pack_files": 13222,
"pack_size": 9773130858,
"total_omitted": 425,
"omitted_files": [
{
@@ -11578,11 +11578,7 @@
"size": 262144,
"repo_path": "bios/Commodore/Amiga/cd32fmv.rom",
"cores": [
"amiberry",
"fsuae"
],
"systems": [
"commodore-amiga"
"amiberry"
]
},
{
@@ -35803,6 +35799,19 @@
"microsoft-msx"
]
},
{
"dest": "ROMs/Kickstarts/cd32fmv.rom",
"sha1": "03ca81c7a7b259cf64bc9582863eca0f6529f435",
"sha256": "680e654880c891a3b03e6107ed30b10903efca16c1dac1b4c8e7835f4e37acb6",
"size": 262144,
"repo_path": "bios/Commodore/Amiga/cd32fmv.rom",
"cores": [
"fsuae"
],
"systems": [
"commodore-amiga"
]
},
{
"dest": "fuse/48.rom",
"sha1": "5ea7c2b824672e914525d1d5c419d71b84a426a2",
@@ -111884,7 +111893,6 @@
"repo_path": "bios/Commodore/Amiga/rom.key",
"cores": [
"amiberry",
"fsuae",
"puae",
"puae2021",
"uae4arm"
@@ -116367,6 +116375,19 @@
"freej2me"
]
},
{
"dest": "ROMs/Kickstarts/rom.key",
"sha1": "801bbab32e3d37386dd1636d4f1d6fa7e21d5874",
"sha256": "baf2366ae04337d37e7b37ebfb5cd7af7e4ffddeb6b017bf2e5fec43d87764e0",
"size": 2069,
"repo_path": "bios/Commodore/Amiga/rom.key",
"cores": [
"fsuae"
],
"systems": [
"commodore-amiga"
]
},
{
"dest": "fuse/128p-0.rom",
"sha1": "d07fcdeca892ee80494d286ea9ea5bf3928a1aca",
@@ -141234,11 +141255,7 @@
"size": 524288,
"repo_path": "bios/Commodore/Amiga/amiga-os-310-cd32.rom",
"cores": [
"amiberry",
"fsuae"
],
"systems": [
"commodore-amiga"
"amiberry"
]
},
{
@@ -141248,11 +141265,7 @@
"size": 524288,
"repo_path": "bios/Commodore/Amiga/amiga-ext-310-cd32.rom",
"cores": [
"amiberry",
"fsuae"
],
"systems": [
"commodore-amiga"
"amiberry"
]
},
{
@@ -141262,11 +141275,7 @@
"size": 262144,
"repo_path": "bios/Commodore/Amiga/amiga-ext-130-cdtv.rom",
"cores": [
"amiberry",
"fsuae"
],
"systems": [
"commodore-amiga"
"amiberry"
]
},
{
@@ -141496,7 +141505,7 @@
]
},
{
"dest": "amiga-os-130.rom",
"dest": "ROMs/Kickstarts/amiga-os-130.rom",
"sha1": "891e9a547772fe0c6c19b610baf8bc4ea7fcb785",
"sha256": "ee05862d8102a08436ac4056da7d549db31625c7d47b24dfb7b3c9a5c113ca53",
"size": 262144,
@@ -141509,7 +141518,7 @@
]
},
{
"dest": "amiga-os-204.rom",
"dest": "ROMs/Kickstarts/amiga-os-204.rom",
"sha1": "c5839f5cb98a7a8947065c3ed2f14f5f42e334a1",
"sha256": "d0b70e8a1772614b897f92c33cb299bed3fc8e3de488fc12f67f97fc2486eb79",
"size": 524288,
@@ -141522,7 +141531,7 @@
]
},
{
"dest": "amiga-os-205.rom",
"dest": "ROMs/Kickstarts/amiga-os-205.rom",
"sha1": "02843c4253bbd29aba535b0aa3bd9a85034ecde4",
"sha256": "8d57d6e9d976df42d91ff9d10a5eedaede1a00e3b4b34cad750ca5ff7229f28a",
"size": 524288,
@@ -141535,7 +141544,7 @@
]
},
{
"dest": "amiga-os-120.rom",
"dest": "ROMs/Kickstarts/amiga-os-120.rom",
"sha1": "11f9e62cf299f72184835b7b2a70a16333fc0d88",
"sha256": "87cddb1f499e32758de20145e73031a84bab299e3f6e5c8487e76d02b2ee9d16",
"size": 262144,
@@ -141548,7 +141557,7 @@
]
},
{
"dest": "amiga-os-310-a1200.rom",
"dest": "ROMs/Kickstarts/amiga-os-310-a1200.rom",
"sha1": "e21545723fe8374e91342617604f1b3d703094f1",
"sha256": "6d43840d4099a74170ea0f0425b6257c3891ebcaa39c4d1840075a9ab22b5707",
"size": 524288,
@@ -141561,7 +141570,7 @@
]
},
{
"dest": "amiga-os-310-a3000.rom",
"dest": "ROMs/Kickstarts/amiga-os-310-a3000.rom",
"sha1": "f8e210d72b4c4853e0c9b85d223ba20e3d1b36ee",
"sha256": "f2c9307aacb92b203a3135a35185deffad6c66cfa8d79920b235d7cf9a5222c4",
"size": 524288,
@@ -141574,7 +141583,7 @@
]
},
{
"dest": "amiga-os-310-a4000.rom",
"dest": "ROMs/Kickstarts/amiga-os-310-a4000.rom",
"sha1": "5fe04842d04a489720f0f4bb0e46948199406f49",
"sha256": "71236ed62c85394a3ad7d0d8d65405bac7a10083e06ba6b82120dacc7a00fe9a",
"size": 524288,
@@ -141586,6 +141595,45 @@
"commodore-amiga"
]
},
{
"dest": "ROMs/Kickstarts/amiga-os-310-cd32.rom",
"sha1": "3525be8887f79b5929e017b42380a79edfee542d",
"sha256": "3586355514947b5ab48c8755dc004fab94f188b19e7bce491c8f9693499e830b",
"size": 524288,
"repo_path": "bios/Commodore/Amiga/amiga-os-310-cd32.rom",
"cores": [
"fsuae"
],
"systems": [
"commodore-amiga"
]
},
{
"dest": "ROMs/Kickstarts/amiga-ext-310-cd32.rom",
"sha1": "5bef3d628ce59cc02a66e6e4ae0da48f60e78f7f",
"sha256": "d964826432559994ffe329f643fd4e42dbbff313362d9434945725d7119cae87",
"size": 524288,
"repo_path": "bios/Commodore/Amiga/amiga-ext-310-cd32.rom",
"cores": [
"fsuae"
],
"systems": [
"commodore-amiga"
]
},
{
"dest": "ROMs/Kickstarts/amiga-ext-130-cdtv.rom",
"sha1": "7ba40ffa17e500ed9fed041f3424bd81d9c907be",
"sha256": "6e48bb768539e8dae352553e5fdfa860682edaf9e26b105fadd8e26103171765",
"size": 262144,
"repo_path": "bios/Commodore/Amiga/amiga-ext-130-cdtv.rom",
"cores": [
"fsuae"
],
"systems": [
"commodore-amiga"
]
},
{
"dest": "kronos/saturn_bios.bin",
"sha1": "2b8cb4f87580683eb4d760e4ed210813d667f0a2",
@@ -157920,7 +157968,8 @@
}
},
"strip_components": 1,
"archive_size": 5083767
"archive_size": 5083767,
"optional": true
},
{
"ref": "fbneo-samples",
@@ -158154,7 +158203,8 @@
}
},
"strip_components": 0,
"archive_size": 53006540
"archive_size": 53006540,
"optional": true
},
{
"ref": "lrps2",
+6 -3
View File
@@ -130664,7 +130664,8 @@
}
},
"strip_components": 1,
"archive_size": 5083767
"archive_size": 5083767,
"optional": true
},
{
"ref": "fbneo-samples",
@@ -130898,7 +130899,8 @@
}
},
"strip_components": 0,
"archive_size": 53006540
"archive_size": 53006540,
"optional": true
},
{
"ref": "freeintv-overlays",
@@ -131271,7 +131273,8 @@
}
},
"strip_components": 1,
"archive_size": 21311170
"archive_size": 21311170,
"optional": true
},
{
"ref": "yaps2-resources",
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 1113711,
"sha256": "b17220355845692a087bbc29cdb4a1008f68b1423d1126f34f00fa0259b53184",
"size": 1122406,
"sha256": "25a88f9a078717afbe34419481d222d2f6f42c4bf24b4ec64bc3faa9561ea01c",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 1167338,
"sha256": "3ca24c5ba64437c1b2554238910b38463c160e74fcd59db675bac3fccd426d67"
"size": 1176033,
"sha256": "05a543e24f2c996d79bea8c95d95789669b92574ad45ffe60df88eb4c80a553c"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 1113711,
"sha256": "b17220355845692a087bbc29cdb4a1008f68b1423d1126f34f00fa0259b53184",
"size": 1122406,
"sha256": "25a88f9a078717afbe34419481d222d2f6f42c4bf24b4ec64bc3faa9561ea01c",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 1113711,
"sha256": "b17220355845692a087bbc29cdb4a1008f68b1423d1126f34f00fa0259b53184",
"size": 1122406,
"sha256": "25a88f9a078717afbe34419481d222d2f6f42c4bf24b4ec64bc3faa9561ea01c",
"executable": false
},
{
+8 -4
View File
@@ -18958,7 +18958,8 @@
}
},
"strip_components": 1,
"archive_size": 5083767
"archive_size": 5083767,
"optional": true
},
{
"ref": "fbneo-cheats",
@@ -32688,7 +32689,8 @@
}
},
"strip_components": 1,
"archive_size": 5083767
"archive_size": 5083767,
"optional": true
},
{
"ref": "fbneo-samples",
@@ -32922,7 +32924,8 @@
}
},
"strip_components": 0,
"archive_size": 53006540
"archive_size": 53006540,
"optional": true
},
{
"ref": "fbneo-samples",
@@ -33156,7 +33159,8 @@
}
},
"strip_components": 0,
"archive_size": 53006540
"archive_size": 53006540,
"optional": true
}
]
}
+4 -2
View File
@@ -19366,7 +19366,8 @@
}
},
"strip_components": 1,
"archive_size": 5083767
"archive_size": 5083767,
"optional": true
},
{
"ref": "fbneo-samples",
@@ -19600,7 +19601,8 @@
}
},
"strip_components": 0,
"archive_size": 53006540
"archive_size": 53006540,
"optional": true
}
]
}
+1385 -18
View File
File diff suppressed because it is too large. Load diff
+1976 -19
View File
File diff suppressed because it is too large. Load diff
+1401 -19
View File
File diff suppressed because it is too large. Load diff
+1947 -19
View File
File diff suppressed because it is too large. Load diff
+6 -3
View File
@@ -146451,7 +146451,8 @@
}
},
"strip_components": 1,
"archive_size": 5083767
"archive_size": 5083767,
"optional": true
},
{
"ref": "fbneo-samples",
@@ -146685,7 +146686,8 @@
}
},
"strip_components": 0,
"archive_size": 53006540
"archive_size": 53006540,
"optional": true
},
{
"ref": "freeintv-overlays",
@@ -147058,7 +147060,8 @@
}
},
"strip_components": 1,
"archive_size": 21311170
"archive_size": 21311170,
"optional": true
},
{
"ref": "yaps2-resources",
+6 -3
View File
@@ -109427,7 +109427,8 @@
}
},
"strip_components": 1,
"archive_size": 5083767
"archive_size": 5083767,
"optional": true
},
{
"ref": "fbneo-samples",
@@ -109661,7 +109662,8 @@
}
},
"strip_components": 0,
"archive_size": 53006540
"archive_size": 53006540,
"optional": true
},
{
"ref": "freeintv-overlays",
@@ -110034,7 +110036,8 @@
}
},
"strip_components": 1,
"archive_size": 21311170
"archive_size": 21311170,
"optional": true
},
{
"ref": "lrps2",
+18 -14
View File
@@ -117,26 +117,28 @@ _standalone_copies: &standalone_copies
- '%APPDATA%\Dolphin Emulator\GC\JAP'
darwin:
- $HOME/Library/Application Support/Dolphin/GC/JAP
# User/GC, then Sys/GC (DSPLLE.cpp:106-113, libretro/dolphin 3aac3f05).
- file: dsp_rom.bin
emulator: dolphin
targets:
linux:
- $HOME/.local/share/dolphin-emu
- $HOME/.var/app/org.DolphinEmu.dolphin-emu/data/dolphin-emu
- $HOME/.local/share/dolphin-emu/GC
- $HOME/.var/app/org.DolphinEmu.dolphin-emu/data/dolphin-emu/GC
windows:
- '%APPDATA%\Dolphin Emulator'
- '%APPDATA%\Dolphin Emulator\GC'
darwin:
- $HOME/Library/Application Support/Dolphin
- $HOME/Library/Application Support/Dolphin/GC
# User/GC, then Sys/GC (DSPLLE.cpp:106-113, libretro/dolphin 3aac3f05).
- file: dsp_coef.bin
emulator: dolphin
targets:
linux:
- $HOME/.local/share/dolphin-emu
- $HOME/.var/app/org.DolphinEmu.dolphin-emu/data/dolphin-emu
- $HOME/.local/share/dolphin-emu/GC
- $HOME/.var/app/org.DolphinEmu.dolphin-emu/data/dolphin-emu/GC
windows:
- '%APPDATA%\Dolphin Emulator'
- '%APPDATA%\Dolphin Emulator\GC'
darwin:
- $HOME/Library/Application Support/Dolphin
- $HOME/Library/Application Support/Dolphin/GC
# PPSSPP
- file: PPSSPP/ppge_atlas.zim
targets:
@@ -148,13 +150,14 @@ _standalone_copies: &standalone_copies
darwin:
- $HOME/Library/Application Support/PPSSPP/PSP/SYSTEM
# Flycast (Dreamcast)
# Linux reads the data folder itself (core/linux-dist/main.cpp:74-97,
# core/stdclass.cpp:95-107, flycast e88b0ced). Windows reads data\ beside
# flycast.exe (core/windows/winmain.cpp:56-77), a folder no rule can name.
- file: dc/dc_boot.bin
targets:
linux:
- $HOME/.local/share/flycast/data
- $HOME/.local/share/flycast
- $HOME/.var/app/org.flycast.Flycast/data/flycast
windows:
- '%APPDATA%\flycast\data'
# shell/apple/emulator-osx/emulator-osx/osx-main.mm:83-101 (flycast 6bb0fc3e).
darwin:
- $HOME/Library/Application Support/Flycast/data
@@ -162,13 +165,14 @@ _standalone_copies: &standalone_copies
# 143-159, flycast a9bd1c74), writable from Termux up to Android 10.
android:
- $EXTERNAL_STORAGE/Android/data/com.flycast.emulator/files/data
# Linux reads the data folder itself (core/linux-dist/main.cpp:74-97,
# core/stdclass.cpp:95-107, flycast e88b0ced). Windows reads data\ beside
# flycast.exe (core/windows/winmain.cpp:56-77), a folder no rule can name.
- file: dc/dc_nvmem.bin
targets:
linux:
- $HOME/.local/share/flycast/data
- $HOME/.local/share/flycast
- $HOME/.var/app/org.flycast.Flycast/data/flycast
windows:
- '%APPDATA%\flycast\data'
# shell/apple/emulator-osx/emulator-osx/osx-main.mm:83-101 (flycast 6bb0fc3e).
darwin:
- $HOME/Library/Application Support/Flycast/data
+2 -2
View File
@@ -443,9 +443,9 @@ class Sandbox:
(system / "system" / "gba_bios.bin").write_bytes(data["gba_bios.bin"])
with zipfile.ZipFile(system / "PSX BIOS.zip", "w") as archive:
archive.writestr("extra.bin", data["pcsx/extra.bin"])
# A check that flags a file ends on 1, as documented.
# A check that flags a file ends on 3, as documented.
return self.run("retroarch", "--check", "--manifest", str(listed), home=home,
codes=(1,))
codes=(3,))
old = home / "old"
old.mkdir()
for dest in ("scph5500.bin", "scph5501.bin"):
+120 -6
View File
@@ -33,7 +33,7 @@ import re
import shutil
import tempfile
import zipfile
from pathlib import Path
from pathlib import Path, PurePosixPath
sys.path.insert(0, os.path.dirname(__file__))
from common import resolve_platform_cores as _platform_cores
@@ -2923,8 +2923,13 @@ def _run_manifest_mode(
alias_registry = registry.get("platforms", {}).get(alias_plat, {})
alias_install = alias_registry.get("install", {})
alias_manifest["detect"] = alias_install.get("detect", [])
alias_manifest["standalone_copies"] = alias_install.get(
"standalone_copies", []
# Placed and completed as the representative's are:
# copied raw, an alias's copies named no source.
alias_manifest["standalone_copies"] = _standalone_copies(
alias_install.get("standalone_copies", []),
{name: emu_profiles[name]
for name in _platform_cores(alias_cfg, emu_profiles)},
manifest["files"],
)
_write_manifest_if_changed(alias_path, alias_manifest)
print(f" {alias_path}: alias of {representative}")
@@ -3978,6 +3983,116 @@ def _placed_copies(copies: list[dict], dests: list[str]) -> list[dict]:
return placed
# data_root keys of the profiles, as the copy targets name each system.
_DATA_ROOT_SYSTEMS = {"linux": "linux", "macos": "darwin", "windows": "windows",
"android": "android"}
def _data_root_target(system: str, text: str) -> str | None:
"""A profile's data_root on one system as a copy target, or None when it
names no place a run can find: a folder beside an executable, a working
directory, a folder picked in the app. Of `A, else B` the alternative
under the home is kept, the default the registry's targets write.
"""
for choice in (part.strip() for part in text.split(", else ")):
if "<" in choice:
continue
if system == "windows" and re.match(r"^%[A-Za-z0-9_()]+%/", choice):
return choice.replace("/", "\\")
if system == "android" and choice.startswith("Android/data/"):
return "$EXTERNAL_STORAGE/" + choice
if system in ("linux", "darwin"):
if choice.startswith("~/"):
return "$HOME/" + choice[2:]
if choice.startswith("$HOME/"):
return choice
return None
def _profile_copies(
profiles: dict[str, dict], dests: list[str], declared: list[dict],
) -> list[dict]:
"""The standalone copies the emulator profiles document: each file with
a standalone_path, into the data_root of each system that names a place.
The registry's list was kept by hand for eight emulators, while the
profiles give the folder of 61 with the code that builds it: a machine
with standalone Cemu only got none of its keys. A target the registry
already gives for the same name is left to it, and a file the manifest
does not carry, or carries under the save directory, has no source. A
file found by content (found_by) sits wherever the folder the user set
for the scan points: Amiberry reads its ROMs from rom_path, which
amiberry.conf moves, so its name under data_root proves nothing.
"""
from fnmatch import fnmatch # noqa: PLC0415
taken: list[tuple[str, str, str, bool]] = []
for copy in declared:
name = copy.get("pattern") or PurePosixPath(copy.get("file", "")).name
for system, targets in (copy.get("targets") or {}).items():
taken += [(system, target, name, "pattern" in copy) for target in targets]
def covered(system: str, target: str, name: str) -> bool:
return any(
system == t_system and target == t_target
and (fnmatch(name, t_name) if is_pattern else name == t_name)
for t_system, t_target, t_name, is_pattern in taken
)
by_tail: dict[str, list[str]] = {}
for dest in dests:
parts = dest.split("/")
for start in range(len(parts)):
by_tail.setdefault("/".join(parts[start:]), []).append(dest)
copies: list[dict] = []
for profile_name in sorted(profiles):
profile = profiles[profile_name]
roots = {
_DATA_ROOT_SYSTEMS[key]: target
for key, value in (profile.get("data_root") or {}).items()
if key in _DATA_ROOT_SYSTEMS and isinstance(value, dict)
and (target := _data_root_target(_DATA_ROOT_SYSTEMS[key], value.get("path", "")))
}
if not roots:
continue
for entry in profile.get("files", []):
standalone = entry.get("standalone_path")
if (not standalone or standalone.endswith("/") or entry.get("mode") == "libretro"
or entry.get("type") == "directory" or entry.get("found_by") == "content"):
continue
held = by_tail.get(entry.get("path") or entry["name"], [])
if len(held) != 1:
continue
source = held[0]
where = PurePosixPath(standalone)
targets: dict[str, list[str]] = {}
for system, root in roots.items():
sep = "\\" if system == "windows" else "/"
target = root if str(where.parent) == "." else (
root + sep + str(where.parent).replace("/", sep))
if not covered(system, target, where.name):
targets[system] = [target]
if not targets:
continue
copy = {"file": source, "targets": targets}
if where.name != PurePosixPath(source).name:
copy["as"] = where.name
copies.append(copy)
return copies
def _standalone_copies(
declared: list[dict], profiles: dict[str, dict], manifest_files: list[dict],
) -> list[dict]:
"""A manifest's standalone copies: the registry's, placed where this
manifest puts each file, then the ones the profiles document. A platform
whose registry entry declares none gets none."""
if not declared:
return []
dests = [entry["dest"] for entry in manifest_files if entry.get("root") != "save"]
return _placed_copies(declared, dests) + _profile_copies(profiles, dests, declared)
def _stable_hashes(local_path: str, db: dict) -> dict:
"""The file's hashes, held against the scan that built the database.
@@ -5020,9 +5135,8 @@ def generate_manifest(
"generated": now,
"base_destination": base_dest,
"detect": detect,
"standalone_copies": _placed_copies(
standalone_copies,
[entry["dest"] for entry in manifest_files if entry.get("root") != "save"],
"standalone_copies": _standalone_copies(
standalone_copies, manifest_profiles, manifest_files,
),
"total_files": len(manifest_files),
"total_size": total_size,
+3 -1
View File
@@ -56,8 +56,10 @@ STEPS: tuple[Step, ...] = (
Step("python scripts/generate_site.py", "the pages the site tests read"),
Step("npm ci --prefix website", "the site's packages, Node 24",
needs="npm", provider="Node 24"),
# Node's compile cache lands in /tmp, a 4 GB tmpfs here: one build
# filled it and every later command failed with ENOSPC.
Step("npm run build --prefix website", "the built site",
needs="npm", provider="Node 24"),
needs="npm", provider="Node 24", env={"NODE_DISABLE_COMPILE_CACHE": "1"}),
Step("python -m unittest discover -s tests -t .", "the suite",
env={"RETROBIOS_OFFLINE": "1"}),
Step("python3.8 -m unittest tests.test_install_tui", "the full screen on the installer's floor",
+80
View File
@@ -733,6 +733,86 @@ class StandaloneCopiesNameTheirSource(unittest.TestCase):
self.assertEqual(placed, copies)
class StandaloneCopiesComeFromTheProfiles(unittest.TestCase):
"""The copies were a hand-kept list of eight emulators while 61 profiles
give their data_root with the code that builds it: a machine with only
standalone Cemu got none of its keys."""
CEMU = {
"data_root": {
"linux": {"path": "$XDG_DATA_HOME/Cemu, else ~/.local/share/Cemu",
"source_ref": "x"},
"windows": {"path": "%APPDATA%/Cemu", "source_ref": "x"},
"macos": {"path": "~/Library/Application Support/Cemu", "source_ref": "x"},
},
"files": [
{"name": "keys.txt", "path": "Cemu/keys.txt", "standalone_path": "keys.txt"},
{"name": "CafeCn.ttf", "path": "Cemu/mlc01/CafeCn.ttf",
"standalone_path": "mlc01/sys/CafeCn.ttf"},
{"name": "gone.bin", "path": "Cemu/gone.bin", "standalone_path": "gone.bin"},
{"name": "core.bin", "path": "Cemu/core.bin", "standalone_path": "core.bin",
"mode": "libretro"},
],
}
def _copies(self, profiles, dests, declared=()):
from generate_pack import _profile_copies
return _profile_copies(profiles, dests, list(declared))
def test_each_file_goes_under_the_data_root(self):
copies = self._copies({"cemu": self.CEMU},
["Cemu/keys.txt", "Cemu/mlc01/CafeCn.ttf", "Cemu/core.bin"])
self.assertEqual(copies, [
{"file": "Cemu/keys.txt", "targets": {
"linux": ["$HOME/.local/share/Cemu"], "windows": ["%APPDATA%\\Cemu"],
"darwin": ["$HOME/Library/Application Support/Cemu"]}},
{"file": "Cemu/mlc01/CafeCn.ttf", "targets": {
"linux": ["$HOME/.local/share/Cemu/mlc01/sys"],
"windows": ["%APPDATA%\\Cemu\\mlc01\\sys"],
"darwin": ["$HOME/Library/Application Support/Cemu/mlc01/sys"]}},
])
def test_a_place_no_run_can_name_is_left_out(self):
melonds = {
"data_root": {
"windows": {"path": "<folder of melonDS.exe>", "source_ref": "x"},
"linux": {"path": "$XDG_CONFIG_HOME/melonDS, else ~/.config/melonDS",
"source_ref": "x"},
},
"files": [{"name": "dsi_sd_card.bin", "standalone_path": "dsisd.bin"}],
}
(copy,) = self._copies({"melonds": melonds}, ["dsi_sd_card.bin"])
self.assertEqual(copy, {"file": "dsi_sd_card.bin",
"targets": {"linux": ["$HOME/.config/melonDS"]},
"as": "dsisd.bin"})
def test_the_registry_keeps_what_it_already_names(self):
declared = [{"pattern": "k*.txt", "targets": {"linux": ["$HOME/.local/share/Cemu"]}}]
(copy, _font) = self._copies({"cemu": self.CEMU},
["Cemu/keys.txt", "Cemu/mlc01/CafeCn.ttf"], declared)
self.assertNotIn("linux", copy["targets"])
self.assertIn("windows", copy["targets"])
def test_a_file_found_by_content_is_left_to_the_scan(self):
amiberry = {
"data_root": {"linux": {"path": "~/Amiberry", "source_ref": "x"}},
"files": [{"name": "cd32fmv.rom", "path": "roms/cd32fmv.rom",
"standalone_path": "cd32fmv.rom", "found_by": "content"}],
}
self.assertEqual(self._copies({"amiberry": amiberry}, ["roms/cd32fmv.rom"]), [])
def test_the_published_retroarch_list_reaches_cemu(self):
import json
manifest = json.loads((ROOT / "install" / "retroarch.json").read_text())
files = {copy["file"]: copy for copy in manifest["standalone_copies"]
if "file" in copy}
for name in ("Cemu/keys.txt", "Cemu/otp.bin", "Cemu/seeprom.bin"):
with self.subTest(name=name):
self.assertEqual(files[name]["targets"]["linux"], ["$HOME/.local/share/Cemu"])
class ADamagedArchiveIsNotPacked(unittest.TestCase):
"""A member whose bytes fail their CRC stops the build. The builder once
copied the damaged archive into the pack "for verify to flag", which
+412 -10
View File
@@ -8,6 +8,7 @@ import functools
import hashlib
import http.server
import importlib.util
import errno
import io
import json
import os
@@ -28,7 +29,7 @@ from types import SimpleNamespace
import unittest.mock
import urllib.error
import zipfile
from pathlib import Path
from pathlib import Path, PurePosixPath
REPO_ROOT = Path(__file__).resolve().parent.parent
_spec = importlib.util.spec_from_file_location("install", REPO_ROOT / "install.py")
@@ -413,6 +414,84 @@ class TestFreeSpaceGuard(unittest.TestCase):
self.assertLess(check, source.index("bios_path.mkdir(parents=True, exist_ok=True)"))
class TheDataTreesCountInTheFreeSpace(unittest.TestCase):
"""The space check held only the BIOS files: 18.8 KB passed on a card
with 20 MB free, then 55.4 MB of archives came and 81.8 MB were written,
against the page's promise to refuse before writing anything."""
def _run(self, *frees_after_bios: int) -> list[tuple[int, str, list]]:
"""One run per free size, in one folder: the second finds the BIOS
files the first installed."""
from types import SimpleNamespace
from test_install_selection import FakeRepository # noqa: PLC0415
member = os.urandom(4096)
results = []
with FakeRepository() as repo, unittest.mock.patch.dict(os.environ):
listed = json.loads((repo.root / "install" / "retroarch.json").read_text())
listed["data_directories"] = [{
"ref": "tree", "dest": "tree", "type": "zip", "archive_size": 50_000_000,
"url": "https://example.invalid/tree.zip", "systems": ["nintendo-gba"],
"files": {"Sys/a.bin": {"sha1": hashlib.sha1(member).hexdigest(),
"size": len(member)}}}]
(repo.root / "install" / "retroarch.json").write_text(json.dumps(listed))
bios = sum(len(payload) for payload, *_ in FakeRepository.PAYLOADS.values())
base = repo.env()["RETROBIOS_BASE_URL"]
for after in frees_after_bios:
out = io.StringIO()
fetched: list = []
free = bios + after
with unittest.mock.patch.object(
sys, "argv", ["install.py", "--platform", "retroarch", "--dest",
str(repo.dest), "--no-input"]), \
unittest.mock.patch.object(install, "MANIFEST_URL",
base + "/install/{platform}.json"), \
unittest.mock.patch.object(install, "RAW_FILE_URL", base + "/{path}"), \
unittest.mock.patch.object(install.shutil, "disk_usage",
lambda _p, free=free: SimpleNamespace(free=free)), \
unittest.mock.patch.object(
install, "install_data_directory",
lambda entry, *a, fetched=fetched, **k: fetched.append(entry["ref"]) or {
"present": 0, "installed": 1, "changed": 0, "absent": 0,
"error": "", "not_kept": []}), \
contextlib.redirect_stdout(out), contextlib.redirect_stderr(out):
try:
install.main()
code = 0
except SystemExit as exc:
code = exc.code
results.append((code, out.getvalue(), repo.installed() + fetched))
return results
def test_a_card_short_for_the_trees_is_refused_before_any_write(self):
((code, said, written),) = self._run(20_000_000)
self.assertEqual(code, 2, said)
self.assertIn("Not enough space", said)
self.assertEqual(written, [])
def test_room_for_the_trees_goes_on(self):
((code, said, written),) = self._run(60_000_000)
self.assertEqual(code, 0, said)
self.assertIn("tree", written)
def test_the_trees_alone_are_checked_too(self):
"""Every BIOS file in place: the trees were written unchecked."""
_first, (code, said, written) = self._run(60_000_000, -1)
self.assertEqual(code, 2, said)
self.assertIn("Not enough space", said)
self.assertNotIn("tree", written)
def test_the_bytes_counted(self):
state = (Path("x"), {"a": {"size": 10}, "b": {"size": 5}}, 0)
entry = {"archive_size": 1000}
with unittest.mock.patch.object(install.LOCAL, "pack", None), \
unittest.mock.patch.object(install, "_offline_only", return_value=False):
self.assertEqual(install.data_space_bytes([(entry, state), (entry, state)]), 1030)
with unittest.mock.patch.object(install.LOCAL, "pack", object()):
self.assertEqual(install.data_space_bytes([(entry, state)]), 15)
class AMissingDestIsSaid(unittest.TestCase):
"""--dest ~/.config/retroarh/system (a typo) was created with every
missing parent and reported done, and --check on it said only
@@ -1432,7 +1511,7 @@ class InstallerStaysOnPython38AndTheStdlib(unittest.TestCase):
cwd=REPO_ROOT, env=env, capture_output=True, text=True, timeout=300,
check=False,
)
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr)
self.assertIn("to download:", proc.stdout)
def test_no_syntax_a_312_parser_lets_through(self):
@@ -4024,6 +4103,50 @@ class DataDirectoriesAreInstalled(unittest.TestCase):
install.install_data_directory(self._entry(), bios)
self.assertEqual([p.name for p in (bios / "dolphin-emu").iterdir()], ["Sys"])
def test_a_second_run_leaves_the_kept_archive_alone(self):
"""Two runs into one BIOS folder shared .dolphin-sys.archive.part: the
one that finished first deleted the archive the other was still
writing, and the other truncated the one being read."""
work = Path(_tempdir(self))
archive = self._zip(work)
bios = work / "system"
root = bios / "dolphin-emu"
root.mkdir(parents=True)
other = root / ".dolphin-sys.archive.part"
other.write_bytes(b"another run is writing this")
kept: list = []
def fetch(url, directory, progress=None, keep=None):
kept.append(keep)
copy = Path(directory) / "private.zip"
shutil.copy(archive, copy)
return copy
held = install._claim_partial(root / ".dolphin-sys.archive.lock")
self.addCleanup(held.close)
with unittest.mock.patch.object(install, "_fetch_archive", fetch):
counts = install.install_data_directory(self._entry(), bios)
self.assertEqual(kept, [None])
self.assertEqual(counts["installed"], 1)
self.assertEqual(other.read_bytes(), b"another run is writing this")
self.assertTrue((root / ".dolphin-sys.archive.lock").exists())
def test_a_lone_run_keeps_its_archive_beside_the_tree(self):
work = Path(_tempdir(self))
archive = self._zip(work)
bios = work / "system"
kept: list = []
def fetch(url, directory, progress=None, keep=None):
kept.append(keep)
shutil.copy(archive, keep)
return keep
with unittest.mock.patch.object(install, "_fetch_archive", fetch):
install.install_data_directory(self._entry(), bios)
self.assertEqual(kept, [bios / "dolphin-emu" / ".dolphin-sys.archive.part"])
self.assertEqual(sorted(p.name for p in (bios / "dolphin-emu").iterdir()), ["Sys"])
def test_the_run_ends_on_the_archive_bytes_too(self):
from test_install_selection import FakeRepository # noqa: PLC0415
@@ -4365,6 +4488,85 @@ class ARefusedStandaloneCopyIsNamed(unittest.TestCase):
class AFullDriveIsSaidAsOne(unittest.TestCase):
"""A card filling up during a tree rebuild read `fbneo-cheats: could not
read https://...: [Errno 28] No space left on device`, putting the cause
on the source, and the run ended on `Run the same command again`."""
def test_the_tree_reports_the_drive(self):
work = Path(_tempdir(self))
entry = {"ref": "fbneo-cheats", "dest": "fbneo/cheats", "type": "zip",
"url": "https://example.invalid/c.zip", "systems": ["arcade"],
"files": {"1941r1.ini": {"sha1": "1" * 40, "size": 4}}}
class Full(io.RawIOBase):
def readable(self):
return True
def readinto(self, buffer):
return 0
def members(_archive, _entry):
yield "1941r1.ini", 4, lambda: io.BufferedReader(Full())
def full(*_args, **_kwargs):
raise OSError(errno.ENOSPC, "No space left on device")
with unittest.mock.patch.object(
install, "_fetch_archive",
lambda url, d, progress=None, keep=None: Path(d) / "a"), \
unittest.mock.patch.object(install, "_archive_members", members), \
unittest.mock.patch.object(install, "_install_data_member", full):
counts = install.install_data_directory(entry, work)
self.assertEqual(counts["error"], install.DRIVE_FULL)
def test_the_end_names_space_not_a_retry(self):
bios = Path(_tempdir(self))
out = io.StringIO()
with contextlib.redirect_stdout(out), self.assertRaises(SystemExit) as ended:
install._report_outcome([bios], 7, 0, 1, 0, False,
failures=[("fbneo-cheats", install.DRIVE_FULL)])
text = out.getvalue()
self.assertEqual(ended.exception.code, 1)
self.assertIn("is full: free up space, or install to another drive with --dest", text)
self.assertNotIn("retry only those", text)
def test_a_bios_file_on_a_full_drive_is_not_retried(self):
bios = Path(_tempdir(self))
attempts: list = []
def urlopen(*_args, **_kwargs):
attempts.append(1)
raise OSError(errno.ENOSPC, "No space left on device")
entry = {"dest": "a.bin", "repo_path": "bios/a.bin", "size": 4,
"sha1": "1" * 40, "sha256": "2" * 64}
with unittest.mock.patch.object(install.urllib.request, "urlopen", urlopen), \
unittest.mock.patch.object(install, "_network_allowed", lambda url: None), \
unittest.mock.patch.object(install, "_retry_delay", lambda *a: 0):
result = install._download_one(entry, bios)
self.assertEqual(result[1:], (False, install.DRIVE_FULL))
self.assertEqual(len(attempts), 1)
class ACheckThatFindsFilesMissingIsNoFailure(unittest.TestCase):
"""--check ended on 1 for files missing as for a list refused or a
--from folder that does not exist: a scheduled check could not tell a
BIOS folder to complete from a tool that never ran."""
def test_missing_is_3_and_a_failure_stays_1(self):
from test_install_selection import FakeRepository # noqa: PLC0415
with FakeRepository() as repo:
missing = repo.run("--no-input", "--check")
nowhere = repo.run("--no-input", "--check", "--from", str(repo.root / "nope"))
repo.run("--no-input")
complete = repo.run("--no-input", "--check")
self.assertEqual(missing.returncode, 3, missing.stdout + missing.stderr)
self.assertEqual(nowhere.returncode, 1, nowhere.stdout + nowhere.stderr)
self.assertEqual(complete.returncode, 0, complete.stdout + complete.stderr)
class OneEmulatorsFilesInOneCommand(unittest.TestCase):
"""An emulator page offers one command for that emulator's files.
--core kept the platform's whole list, and narrowing to cores kept the
@@ -4483,6 +4685,198 @@ class WindowsSeenFromWsl(unittest.TestCase):
self.assertEqual(os.environ["ProgramFiles"], "")
def test_a_steam_client_outside_program_files_is_found(self):
"""A Steam client in D:\\Steam hid every library: only Program Files
was searched, and Steam's RetroArch was not detected."""
empty = self.root / "c" / "Program Files (x86)"
empty.mkdir(parents=True)
common = self.root / "d" / "steam" / "steamapps" / "common" / "RetroArch"
common.mkdir(parents=True)
(common / "retroarch.exe").write_bytes(b"MZ")
(common / "retroarch.cfg").write_text('system_directory = "default"\n')
home = Path(_tempdir(self))
cleared = {name: "" for name in (*install._WINDOWS_VARIABLES, "SCOOP", "SCOOP_GLOBAL",
"ProgramData")}
asked: list = []
def registry(key, value):
asked.append((key, value))
return "d:/steam" if (key, value) == ("HKCU\\Software\\Valve\\Steam",
"SteamPath") else None
with unittest.mock.patch.dict(os.environ, {**cleared, "HOME": str(home),
"ProgramFiles(x86)": str(empty)}), \
unittest.mock.patch.object(Path, "home", return_value=home), \
unittest.mock.patch.object(install, "_read_registry", registry):
self.assertEqual(install._steam_libraries(), [self.root / "d" / "steam"])
found = install.detect_platforms("wsl")
self.assertIn(("retroarch", common / "system"), found)
def test_windows_not_answering_is_said_not_taken_for_nothing(self):
"""cmd.exe timing out on the first variable ended the import without
a word, and detection said no setup was installed."""
calls: list = []
def slow(argv, **kwargs):
calls.append(argv)
raise subprocess.TimeoutExpired(argv, kwargs.get("timeout", 10))
cleared = {name: "" for name in install._WINDOWS_VARIABLES}
home = Path(_tempdir(self))
err = io.StringIO()
with unittest.mock.patch.dict(os.environ, {**cleared, "HOME": str(home)}), \
unittest.mock.patch.object(install.subprocess, "run", slow), \
unittest.mock.patch.object(Path, "home", return_value=home), \
unittest.mock.patch.object(install, "_DETECTION_REFUSED", []), \
unittest.mock.patch.object(install.EVENTS, "emit") as emit, \
contextlib.redirect_stderr(err):
install._enter_wsl()
self.assertEqual(len(calls), 1)
install.detect_platforms("wsl")
install.detect_platforms("wsl")
said = err.getvalue()
self.assertEqual(said.count("on the Windows side"), 1, said)
self.assertIn("cmd.exe did not answer in 10 s", said)
kinds = [c.args[0] for c in emit.call_args_list]
self.assertEqual(kinds.count("unsearched"), 1)
self.assertEqual(emit.call_args_list[kinds.index("unsearched")].kwargs["folder"],
str(self.root))
class StandaloneCopiesUnderTheirOwnName(unittest.TestCase):
"""melonDS reads dsisd.bin and xemu eeprom.bin, names the platforms
give dsi_sd_card.bin and xbox_eeprom.bin: a copy says the name the
emulator reads."""
def test_the_copy_takes_the_emulator_name(self):
bios = Path(_tempdir(self))
(bios / "xbox_eeprom.bin").write_bytes(b"eeprom")
folder = Path(_tempdir(self))
manifest = {"standalone_copies": [
{"file": "xbox_eeprom.bin", "as": "eeprom.bin", "targets": {"linux": [str(folder)]}}]}
install._validate_manifest({**self._published(), **manifest}, "emudeck")
self.assertEqual(install.do_standalone_copies(manifest, bios, "linux"), (1, 0, 0))
self.assertEqual((folder / "eeprom.bin").read_bytes(), b"eeprom")
self.assertFalse((folder / "xbox_eeprom.bin").exists())
plan = install.standalone_plan(
[{"files": [{"dest": "xbox_eeprom.bin", "sha1": "0" * 40, "size": 6}],
**manifest}], "linux")
self.assertEqual([entry["dest"] for entry in plan[0][1]], ["eeprom.bin"])
def _published(self) -> dict:
return json.loads((REPO_ROOT / "install" / "emudeck.json").read_text())
def test_a_name_that_climbs_is_refused(self):
for name in ("../eeprom.bin", "a/eeprom.bin", "", 3):
with self.subTest(name=name), \
self.assertRaisesRegex(ValueError, "standalone copy name|\\.as"):
install._validate_manifest({**self._published(), "standalone_copies": [
{"file": "x.bin", "as": name, "targets": {}}]}, "emudeck")
def test_cemu_alone_is_found_from_the_published_list(self):
home = Path(_tempdir(self))
(home / ".local" / "share" / "Cemu").mkdir(parents=True)
manifest = json.loads((REPO_ROOT / "install" / "retroarch.json").read_text())
with unittest.mock.patch.dict(os.environ, {"HOME": str(home)}):
plan = dict(install.standalone_plan([manifest], "linux"))
names = {entry["dest"] for entry in plan[home / ".local" / "share" / "Cemu"]}
self.assertLessEqual({"keys.txt", "otp.bin", "seeprom.bin"}, names)
class TheBoardSystemsAreNamedFromTheCode(unittest.TestCase):
"""--help and the wiki named six systems whose board is read, while the
code read eight: an AmberELEC user concluded --target was needed."""
def test_help_and_wiki_name_every_one(self):
names = [install.display_name(name) for name in install._BOARD_SOURCES]
help_text = " ".join(install.build_parser().format_help().split())
wiki = " ".join((REPO_ROOT / "wiki" / "installer.md").read_text().split())
paragraph = wiki[:wiki.index("RetroPie reads the Pi model") + len("RetroPie")][-400:]
for name in names:
with self.subTest(name=name):
self.assertIn(name, help_text[help_text.index("--target"):])
self.assertIn(name, paragraph)
class TheInstallerFitsTheOneLiners(unittest.TestCase):
"""install.sh and install.ps1 refuse an installer over 2 MiB, and
install.py grew from 596 KB to 1.1 MB in twelve days with nothing
holding it under that: past it, every one-line install would stop."""
def _caps(self) -> dict[str, int]:
import re
shell = (REPO_ROOT / "install.sh").read_text(encoding="utf-8")
ps1 = (REPO_ROOT / "install.ps1").read_text(encoding="utf-8")
units = {"": 1, "KB": 1024, "MB": 1024 ** 2}
number, unit = re.search(r"\$maximumInstallerBytes = (\d+)(KB|MB)?", ps1).groups()
return {"install.sh": int(re.search(r"^MAX_INSTALLER_BYTES=(\d+)$", shell, re.M)[1]),
"install.ps1": int(number) * units[unit or ""]}
def test_a_quarter_of_each_cap_stays_free(self):
size = (REPO_ROOT / "install.py").stat().st_size
for script, cap in self._caps().items():
with self.subTest(script=script):
self.assertLessEqual(
size, cap * 3 // 4,
f"install.py is {size} bytes, past three quarters of the {cap} {script} "
"accepts: shrink it or raise the cap in both one-liners")
def test_the_refusal_names_a_cause_and_a_way_out(self):
for script in ("install.sh", "install.ps1"):
text = " ".join((REPO_ROOT / script).read_text(encoding="utf-8").split())
with self.subTest(script=script):
self.assertIn("this script accepts", text)
self.assertIn("open any web page to sign in", text)
class RetroArchAppImageIsPortable(unittest.TestCase):
"""The buildbot's Linux RetroArch is an AppImage beside a .home folder
the AppImage runtime makes its HOME: the run found nothing and offered
~/.config/retroarch/system, a folder this RetroArch never reads."""
def _detect(self, home: Path, env: dict | None = None) -> list:
cleared = {"HOME": str(home), "XDG_CONFIG_HOME": ""}
with unittest.mock.patch.dict(os.environ, {**cleared, **(env or {})}), \
unittest.mock.patch.object(Path, "home", return_value=home):
os.environ.pop("XDG_CONFIG_HOME")
return install.detect_platforms("linux")
def _unpack(self, home: Path) -> Path:
folder = home / "Downloads" / "RetroArch-Linux-x86_64"
base = folder / "RetroArch-Linux-x86_64.AppImage.home" / ".config" / "retroarch"
(base / "system").mkdir(parents=True)
(folder / "RetroArch-Linux-x86_64.AppImage").write_bytes(b"\x7fELF")
return base
def test_the_unpacked_archive_is_found(self):
home = Path(_tempdir(self))
base = self._unpack(home)
self.assertIn(("retroarch", base / "system"), self._detect(home))
def test_its_config_is_read_with_its_own_home(self):
home = Path(_tempdir(self))
base = self._unpack(home)
(base / "retroarch.cfg").write_text('system_directory = "~/bios"\n')
portable = base.parent.parent
self.assertIn(("retroarch", portable / "bios"), self._detect(home))
def test_a_config_folder_beside_it_wins(self):
home = Path(_tempdir(self))
base = self._unpack(home)
config = base.parent.parent.with_name("RetroArch-Linux-x86_64.AppImage.config")
(config / "retroarch").mkdir(parents=True)
found = self._detect(home)
self.assertIn(("retroarch", config / "retroarch" / "system"), found)
self.assertNotIn(("retroarch", base / "system"), found)
def test_a_folder_without_its_appimage_is_left(self):
home = Path(_tempdir(self))
base = self._unpack(home)
(base.parent.parent.with_name("RetroArch-Linux-x86_64.AppImage")).unlink()
self.assertNotIn(("retroarch", base / "system"), self._detect(home))
class SteamRetroArchOnMacIsPortable(unittest.TestCase):
"""platform_darwin.m makes the Steam build portable (HAVE_STEAM) with no
portable.txt: a default system_directory is Application Support's, and
@@ -4561,7 +4955,7 @@ class TheInstallerSaysWhatItFound(unittest.TestCase):
self.assertEqual(self._run("--dest", str(dest), "--system", "atari-lynx", "--yes").returncode, 0)
(dest / "lynxboot.img").write_bytes(b"corrupt")
proc = self._run("--dest", str(dest), "--system", "atari-lynx", "--check")
self.assertEqual(proc.returncode, 1)
self.assertEqual(proc.returncode, 3)
self.assertIn("wrong copy: lynxboot.img", proc.stdout)
self.assertIn("without --check", proc.stdout)
@@ -5247,7 +5641,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun):
(dest / "scph5501.bin").write_bytes(b"junk")
proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(dest),
"--check", "--no-input")
self.assertEqual(proc.returncode, 1, proc.stderr)
self.assertEqual(proc.returncode, 3, proc.stderr)
listing = proc.stdout.split("to download:\n", 1)[1].splitlines()
self.assertEqual(listing[0].strip(), "wrong copy: scph5501.bin")
@@ -5279,7 +5673,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun):
shutil.copy(REPO_ROOT / repo_path, dest / "PPSSPP")
proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(dest),
"--system", "sony-psp", "--check", "--no-input")
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr)
self.assertNotIn("Everything is in place", proc.stdout)
found = re.search(r"(\d+) in place, (\d+) missing", proc.stdout)
self.assertIsNotNone(found, proc.stdout)
@@ -5293,7 +5687,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun):
proc = _install_run({**self.env, "RETROBIOS_INSTALL_OFFLINE": "1"}, "--platform",
"retroarch", "--dest", str(dest), "--check", "--no-input",
"--manifest", str(REPO_ROOT / "install" / "retroarch.json"))
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr)
missing = re.search(r"0 in place, (\d+) missing", proc.stdout).group(1)
self.assertIn(f"\n {missing} files not in place:\n", proc.stdout)
self.assertIn(f" {missing} files need the internet", proc.stdout)
@@ -5544,7 +5938,15 @@ class StandaloneEmulatorsWithoutAPlatformAreServed(unittest.TestCase, _LoopbackR
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
names = sorted(path.name for path in self.duckstation.iterdir())
self.assertIn("scph5501.bin", names)
self.assertTrue(all(name.startswith("scph") for name in names), names)
# Every image the DuckStation profile places in its bios folder,
# its PS2 and OpenBIOS images included, and nothing else.
import yaml # noqa: PLC0415
profile = yaml.safe_load((REPO_ROOT / "emulators" / "duckstation.yml").read_text())
named = {PurePosixPath(entry["standalone_path"]).name
for entry in profile["files"] if entry.get("standalone_path")}
self.assertEqual([name for name in names
if name not in named and not name.startswith("scph")], [])
proc = _install_run(self.env, "--no-input", "--check")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("Everything is in place.", proc.stdout)
@@ -6396,13 +6798,13 @@ class EventsDescribeTheRun(unittest.TestCase, _SyntheticTree):
def test_a_check_run(self):
proc, events = self._run("--check")
self.assertEqual(proc.returncode, 1)
self.assertEqual(proc.returncode, 3)
installer = events[0]["installer"]
self.assertEqual(events, self._head("check", installer) + [
{"event": "summary", "mode": "check", "installed": 0, "up_to_date": 1,
"pending": 3, "failed": [], "skipped": self.SKIPPED, "left_out": [], "kept": [],
"interrupted": False},
{"event": "end", "code": 1},
{"event": "end", "code": 3},
])
self.assertIn("1 in place, 2 missing, 1 wrong copy", proc.stderr)
self.assertFalse((self.bios / "c.bin").exists())
@@ -6618,7 +7020,7 @@ class TheInstallerPageIsWrittenFromTheCode(unittest.TestCase):
self.scratch.read_text(encoding="utf-8"))
def test_the_exit_codes_are_the_ones_install_py_exits_with(self):
self.assertEqual(self.generator.exit_codes(), [0, 1, 2, 130, 141])
self.assertEqual(self.generator.exit_codes(), [0, 1, 2, 3, 130, 141])
def test_a_missing_marker_stops_it(self):
page = self.scratch.read_text(encoding="utf-8")
+1 -1
View File
@@ -548,7 +548,7 @@ class AFolderThisUserCannotEnter(unittest.TestCase):
finally:
os.chmod(locked, 0o755)
self.assertNotIn("Traceback", proc.stderr)
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr)
self.assertIn("wrong copy: pcsx/extra.bin", proc.stdout)
self.assertIn("could not be read (Permission denied)", proc.stdout)
self.assertIn("missing: scph5501.bin", proc.stdout)
+2 -2
View File
@@ -221,7 +221,7 @@ class CopyFromAnOldCard(FromFolder):
self.keep("gba_bios.bin", GBA)
proc = self.run_installer("--check", "--manifest", str(self.list), "--from",
str(self.old))
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr)
self.assertIn(f"{self.old} holds 2 of these files: a run without --check "
"copies them.", proc.stdout)
self.assertIn(f"in {self.old}: BIOS/SCPH5501.BIN", proc.stdout)
@@ -607,7 +607,7 @@ class UnreadableCopies(FromFolder):
for options in (("--check",), ("--dry-run",), ()):
with self.subTest(options=options):
proc = self.install(*options)
self.assertEqual(proc.returncode, 1 if options != ("--dry-run",) else 0,
self.assertEqual(proc.returncode, {("--check",): 3, ("--dry-run",): 0}.get(options, 1),
proc.stdout + proc.stderr)
self.assertIn(line, proc.stdout)
self.assertNotIn("notes.txt", proc.stdout)
+2 -2
View File
@@ -120,7 +120,7 @@ class Scratch(unittest.TestCase):
def check(self) -> str:
proc = self.run_installer("--check", "--manifest", str(self.list), "--verbose")
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr)
return proc.stdout
def complete(self, *skip: str) -> None:
@@ -1037,7 +1037,7 @@ class OneOfflineVocabulary(Scratch):
""""Run the same command without --check to fetch them"."""
proc = self.run_installer("--check", "--offline", "--manifest", str(self.list))
out = proc.stdout + proc.stderr
self.assertEqual(proc.returncode, 1, out)
self.assertEqual(proc.returncode, 3, out)
self.assertNotIn("to fetch them", out)
self.assertIn("4 files need the internet, which --offline does not use.", out)
self.assertIn("Copy them from your old card or a folder (--from)", out)
+1 -1
View File
@@ -1728,7 +1728,7 @@ class SomethingElseAtADestination(unittest.TestCase):
with FakeRepository() as repo:
self.prepare(repo)
proc = repo.run("--check")
self.assertEqual(proc.returncode, 1, proc.stderr)
self.assertEqual(proc.returncode, 3, proc.stderr)
self.assertNotIn("Traceback", proc.stderr)
self.assertIn(" wrong copy: scph5500.bin\n a folder sits where this "
"file goes: move it away, then run again", proc.stdout)
+3 -3
View File
@@ -615,10 +615,10 @@ class ExitStatusAndPinnedInstaller(unittest.TestCase):
self.text = _wiki("installer.md")
self.source = (REPO_ROOT / "install.py").read_text(encoding="utf-8")
def test_the_check_event_tells_missing_from_failure(self):
def test_the_code_tells_missing_from_failure(self):
section = _flat(_section(self.text, "Exit status"))
self.assertIn("read the `check` event", section)
self.assertIn("`missing` and `wrong_hash` count what is left", section)
self.assertIn("`--check` tells missing files (`3`) from a failure (`1`) by its code", section)
self.assertIn("is left in `missing` and `wrong_hash`", section)
fields = re.search(r'"check": \(([^)]*)\)', self.source).group(1)
for field in ("missing", "wrong_hash"):
with self.subTest(field=field):
+45 -27
View File
@@ -354,7 +354,8 @@ file list is downloaded once and handed to each run.
transfer stops, and the next attempt or the next run requests only the
rest. A data tree's archive does the same beside the tree
(`.<ref>.archive.part`), asking with `If-Range` so that a server whose
archive changed sends it whole. A file already there with other contents
archive changed sends it whole. A second run into the same folder finds
that archive in use and downloads its own copy. A file already there with other contents
is moved aside first, see [Files you already had](#files-you-already-had).
8. Copies into standalone emulator directories, only with `--standalone-copies`.
@@ -444,7 +445,8 @@ The figures change with each release.
The size depends on the platform and changes with each release. The run prints
it before downloading, and asks first when the download reaches 1 GB. The
installer also measures the free space and refuses before writing anything if
there is not enough.
there is not enough. The count holds the BIOS files, every data file still to
come, and the largest source archive, which sits in its tree while it is read.
`curl` or `wget`, and Python 3.8 or newer, are checked by the command itself:
it stops with a message naming what is absent. PowerShell's execution policy
@@ -475,13 +477,13 @@ few minutes on a memory card, because every file is hashed:
```
Fetching file index for batocera...
23 files read from Batocera's save directory go to a folder per system under /userdata/saves
18170 files (9.2 GB)
3154 BIOS files (4.3 GB), 11544 game data files (4.8 GB) and 3472 data files (81.8 MB).
18175 files (9.2 GB)
3159 BIOS files (4.3 GB), 11544 game data files (4.8 GB) and 3472 data files (81.8 MB).
Game data: files a game or a game engine reads, of no use to the other cores. Data files: the files an emulator reads from a data folder of its own, such as Dolphin's Sys and PPSSPP's assets.
Not installable: 426 files are not in the collection, 80 of them required.
Checking existing files...
0 in place, 18170 missing, 0 wrong copies
0 in place, 18175 missing, 0 wrong copies
```
<!-- /generated: run-index -->
@@ -496,10 +498,10 @@ as the log of the run below:
<!-- generated: run-download -->
```
Downloading 14698 files (9.2 GB)...
Downloading 14703 files (9.2 GB)...
Press Ctrl+C to stop. Running the same command again carries on from here.
[1/14698] panafz1.bin ok
[2/14698] panafz10.bin ok
[1/14703] panafz1.bin ok
[2/14703] panafz10.bin ok
```
<!-- /generated: run-download -->
@@ -511,8 +513,8 @@ command again picks up where it stopped.
<!-- generated: run-end -->
```
Done. 18170 files installed.
Installed: 3154 BIOS files, 11544 game data files and 3472 data files.
Done. 18175 files installed.
Installed: 3159 BIOS files, 11544 game data files and 3472 data files.
BIOS folder: /userdata/bios
426 files were not installed: the collection does not hold them, or its copy fails their check.
Needed to start an emulator (1 file):
@@ -566,7 +568,7 @@ The same command with `--check` reads the folder and writes nothing:
```
Checking existing files...
18170 in place, 0 missing, 0 wrong copies
18175 in place, 0 missing, 0 wrong copies
Everything is in place.
```
@@ -575,7 +577,7 @@ Checking existing files...
`Everything is in place.` is said only when the data folders are complete
too. With the BIOS files in place and data files missing (PPSSPP's assets,
Dolphin's `Sys`), the line counts those instead (`169 data files to fetch`),
and `--check` exits with code 1. The files the collection does not hold are
and `--check` exits with code 3. The files the collection does not hold are
listed after it again, as long as they are absent from the folder. A number before `wrong copies` means a file is
in place with contents the platform does not accept. Running the install again
replaces it and keeps the old one, see
@@ -1011,8 +1013,8 @@ what the options name, everything by default.
<!-- /generated: options -->
`--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
`--check` exits with code 3 when a file is missing or does not match, 0
when every one does, and 1 when the check itself failed. 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.
@@ -1070,8 +1072,9 @@ AmberELEC, EmuELEC and KNULLI install the BIOS list they publish themselves. The
image or source of its Odroid Go builds is published, so neither its marker
files nor its folder could be read.
Batocera, Recalbox, ROCKNIX, Lakka and KNULLI also name the board they were built
for, RetroPie reads the Pi model as its setup script does (`x86_64` on a PC),
AmberELEC, EmuELEC, Batocera, Recalbox, ROCKNIX, Lakka and KNULLI also name the
board they were built for (AmberELEC and EmuELEC as `COREELEC_DEVICE` in
`/etc/os-release`), RetroPie reads the Pi model as its setup script does (`x86_64` on a PC),
and that board narrows the install unless `--target` or `--every-board` says
otherwise (see the options above). A board with no core list of its own is
named and gets the files of every board. KNULLI writes its board to
@@ -1161,7 +1164,9 @@ Under WSL both sides are searched. The Windows one is reached through its
`/mnt/<drive>` mount: a `C:\` path from a rule or a config becomes
`/mnt/c/...`, and the Windows variables the rules use (`USERPROFILE`,
`APPDATA`, `LOCALAPPDATA`, `ProgramFiles`) are read through `cmd.exe` when
WSL does not pass them. A forced `--platform retrobat` falls back to
WSL does not pass them. When `cmd.exe` or `reg.exe` gives no answer within
10 seconds or cannot run (interop turned off), the run says the Windows side
was not searched, never that nothing is installed there. A forced `--platform retrobat` falls back to
`/mnt/c/RetroBat/bios`. With nothing found, the folder question offers both
usual folders of a setup that has one on each side: RetroArch's in this Linux
(`~/.config/retroarch/system`, under Enter) and the Windows one
@@ -1176,10 +1181,11 @@ usual folders of a setup that has one on each side: RetroArch's in this Linux
| `~/.var/app/org.libretro.RetroArch/config/retroarch/retroarch.cfg` | RetroArch (Flatpak) | `system_directory` |
| `~/snap/retroarch/current/.config/retroarch/retroarch.cfg` | RetroArch (Snap) | `system_directory` |
| `~/.config/retroarch/retroarch.cfg` | RetroArch (native) | `system_directory` |
| `<name>.AppImage.home/.config/retroarch/retroarch.cfg` (or `<name>.AppImage.config/retroarch/`) beside an AppImage in `~`, or one or two folders below it | RetroArch (AppImage from the buildbot, whose portable folder becomes its home) | `system_directory`. Without a config, `system` in that `retroarch` folder |
| `~/Library/Application Support/RetroArch/config/retroarch.cfg` | RetroArch (macOS) | `system_directory`. At default, `~/Documents/RetroArch/system` (portable, Steam build included: `Application Support/RetroArch/system`) |
| `retroarch.cfg` beside `retroarch.exe` (`C:\RetroArch-Win64`, `C:\RetroArch`, `Program Files\RetroArch`) | RetroArch (Windows) | `system_directory`. At default or without a config, `system` beside the exe |
| `%APPDATA%\RetroArch\retroarch.cfg` | RetroArch (Windows) | `system_directory`, read when the exe folder has no config |
| `%ProgramFiles(x86)%\Steam\steamapps\common\RetroArch\retroarch.cfg` | RetroArch (Steam) | `system_directory` |
| `steamapps\common\RetroArch\retroarch.cfg` in every Steam library: the client's folder (`SteamPath` under `HKCU\Software\Valve\Steam`, else `%ProgramFiles(x86)%\Steam`) and the libraries its `libraryfolders.vdf` lists | RetroArch (Steam) | `system_directory` |
| `%LOCALAPPDATA%\Packages\1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_*\LocalState` | RetroArch (UWP/MSIX package) | `system_directory` of the `retroarch.cfg` there, `~` meaning that folder. By default `LocalState\system` |
| LaunchBox `Data\Emulators.xml` | RetroArch (portable) | the system directory that entry points at |
| LaunchBox `Data\Emulators.xml` naming `EmuHawk.exe` | BizHawk (portable) | `Firmware` beside `EmuHawk.exe` |
@@ -1331,8 +1337,9 @@ rather than from the platform's BIOS tree. `--standalone-copies` copies them
there after the install, and only then: without the flag nothing is written
outside the selected tree, apart from the files kept in `retrobios-backup`.
The manifest carries the list, fifteen entries on the six platforms that
declare them (Batocera, EmuDeck, Recalbox, RetroArch, RetroBat, RetroDECK):
The manifest carries the list on the six platforms that declare one
(Batocera, EmuDeck, Recalbox, RetroArch, RetroBat, RetroDECK). It has two
parts. The registry names fifteen entries, Flatpak folders included:
| Files | Copied to |
|-------|-----------|
@@ -1341,9 +1348,19 @@ declare them (Batocera, EmuDeck, Recalbox, RetroArch, RetroBat, RetroDECK):
| `Citra/sysdata/aes_keys.txt`, `Citra/sysdata/boot9.bin` | Azahar `sysdata` |
| `scph*.bin` | DuckStation `bios` |
| `ps2-*.bin` | PCSX2 `bios`, native and Flatpak |
| `GC/USA/IPL.bin`, `GC/EUR/IPL.bin`, `GC/JAP/IPL.bin`, `dsp_rom.bin`, `dsp_coef.bin` | Dolphin, per region for the IPL |
| `GC/USA/IPL.bin`, `GC/EUR/IPL.bin`, `GC/JAP/IPL.bin`, `dsp_rom.bin`, `dsp_coef.bin` | Dolphin `GC`, per region for the IPL |
| `PPSSPP/ppge_atlas.zim` | PPSSPP `PSP/SYSTEM` |
| `dc/dc_boot.bin`, `dc/dc_nvmem.bin` | Flycast `data`, native and Flatpak |
| `dc/dc_boot.bin`, `dc/dc_nvmem.bin` | Flycast's data folder, native and Flatpak |
The rest comes from the emulator profiles. A profile that gives the folder its
standalone build reads, per system, has each file with a
`standalone_path` copied to that path under the folder: Cemu's `keys.txt`,
`otp.bin` and `seeprom.bin` to `~/.local/share/Cemu`, xemu's EEPROM, melonDS's
DSi SD image. A copy carries `as` when the emulator reads the file under
another name (`eeprom.bin`, `dsisd.bin`). A folder no run can name (beside an
executable, picked in the app) is not a target, nor is a place the registry
already gives for the same file. A file the emulator finds by its contents in a
folder the user sets (Amiberry's ROMs) is left out.
An entry names one file or a glob, and one entry carries no file at all: when
an RPCS3 configuration directory is present it prints that `PS3UPDAT.PUP` is
@@ -1753,8 +1770,9 @@ fields, the installer refuses to narrow rather than install everything.
| Code | Meaning |
|------|---------|
| `0` | everything asked for is installed or already in place |
| `1` | a failure: the one-liner could not download or verify `install.py`, or found no Python 3.8 with its ssl, unicodedata and zlib modules (nothing was run), an unusable base URL, `--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 or were left out by `--offline` while others installed, `--check` found files to fetch, a `--from-pack` pack that cannot be read, lacks a part or is for another platform, a `--from` folder that does not exist or cannot be read, a BIOS folder that is a file or lies under one, a `--dest` that is not the card of the system named (another system's card, a folder inside one, a drive with none of its files where its card always has them, or a folder that does not exist), `--restore` could not put a file back, or `--add-app` could not put every file of the app on the card |
| `1` | a failure: the one-liner could not download or verify `install.py`, or found no Python 3.8 with its ssl, unicodedata and zlib modules (nothing was run), an unusable base URL, `--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 or were left out by `--offline` while others installed, a `--from-pack` pack that cannot be read, lacks a part or is for another platform, a `--from` folder that does not exist or cannot be read, a BIOS folder that is a file or lies under one, a `--dest` that is not the card of the system named (another system's card, a folder inside one, a drive with none of its files where its card always has them, or a folder that does not exist), `--restore` could not put a file back, or `--add-app` could not put every file of the app on the card |
| `2` | nothing was installed: every file the run tried failed (a BIOS folder that cannot be written, for one), or `--offline` left every file out, the drive has too little free space for the download (checked before any folder is created), or the question about a folder that does not exist, named with `--dest` or typed at the folder question, was answered no. Also an option the installer refused before any download: an unknown option, `--jobs` outside 1 to 32, `--target` with `--every-board`, `--target` with `--from-pack` or `--offline` (or `RETROBIOS_INSTALL_OFFLINE` set) when no core list per board is at hand (the pack carries none and none is beside `--manifest` as `targets/<platform>.json`), or for a platform that publishes none, which is said to have no hardware targets, `--list-targets` with `--offline` and neither, `--add-app` with `--check`, `--dry-run`, a listing, `--from-pack` or `--offline`. The message then starts with `install.py: error:`. Also an unknown platform, target, system, core or emulator, said in an `Error:` or `Unknown platform` line and an `error` event whose `stage` is `option`. Also `--add-app` for a folder on no card that takes the app, said in an `Error:` line and an `app` event before anything is written. With `--restore`, a folder that is not a dated folder inside `retrobios-backup`. Also a quit chosen before the install, as `gh` ends a cancelled prompt: `q` at a question, `q` or **Quit** on the full screen, which then keeps `RetroBIOS: quit. Nothing was installed.`, and the full screen closed with no button pressed (`RETROBIOS_IDLE_EXIT`). No code but `0` closes a Windows Terminal tab started on the installer itself, so such a tab stays open on a quit |
| `3` | `--check` ran and found files to fetch or wrong copies, nothing having failed. A failure takes `1` as before: a manifest refused, a `--from` folder that does not exist. Until November 2026 this case also ended on `1`, so a script that reads any code but `0` as "not ready" needs no change |
| `130` | stopped with Ctrl+C, or with Ctrl+C or Ctrl+D at a question, the terminal keeping `RetroBIOS: stopped. Nothing was installed.` before an install. Run the same command again to carry on |
| `141` | the output was piped to a reader that closed early (`--list-platforms \| head`) |
@@ -1777,9 +1795,9 @@ in the session.
Failures that are per-file, such as a download exhausting its three attempts,
are counted and reported at the end.
To tell missing files from a failure, read the `check` event of
[Machine-readable events](#machine-readable-events): `missing` and `wrong_hash`
count what is left.
`--check` tells missing files (`3`) from a failure (`1`) by its code. The
`check` event of [Machine-readable events](#machine-readable-events) counts what
is left in `missing` and `wrong_hash`.
## Machine-readable events
@@ -1798,7 +1816,7 @@ other schemas under `api/v1/schemas/`.
| Kind | Keys | When |
|------|------|------|
| `start` | `schema`, `installer`, `mode` | First line. `schema` is 1, `installer` the identity `--version` prints, `mode` one of `install`, `check`, `dry-run`, `list` |
| `unsearched` | `folder`, `reason`, `message` | Before `detect`, once for each folder the search for setups could not look in, this user being refused it or a read failing there (a failing card, a link that loops). What it would hold counts as absent, and the other places are still searched: `detect` then lists what they hold, and an empty `platforms` after an `unsearched` line means nothing was found where the installer could look. `reason` says why in words (`Permission denied`), `message` says it in a sentence |
| `unsearched` | `folder`, `reason`, `message` | Before `detect`, once for each folder the search for setups could not look in, this user being refused it or a read failing there (a failing card, a link that loops), or under WSL the Windows side when Windows did not answer (`folder` is then the drive mount, `/mnt`). What it would hold counts as absent, and the other places are still searched: `detect` then lists what they hold, and an empty `platforms` after an `unsearched` line means nothing was found where the installer could look. `reason` says why in words (`Permission denied`), `message` says it in a sentence |
| `detect` | `os`, `os_name`, `platforms` | Once the platforms are known. `os_name` comes from `/etc/os-release`. `platforms` lists `{platform, name, bios_dir}`, and is empty when nothing supported was found |
| `error` | `stage`, `reason`, `platform`, `message` | The file list could not be read (`stage` is `manifest`): `reason` is `network`, `not_found` or `invalid`, `clock` when the server's certificate was refused and the system clock reads a date before the installer was written, `no_ca` when the machine holds no list of trusted certificates (`SSL_CERT_FILE` names one), `no_ssl` when this Python was built without its `ssl` module and cannot use HTTPS, or `offline` when `--offline` forbids the request. A folder named with `--from` cannot be read (`stage` is `from`, `reason` `not_found`, `not_a_folder` for a file that is no ZIP, `unreadable`, or `system` for the root of the system, `/proc`, `/sys` or `/dev`). The BIOS folder sits under a folder this user cannot open (`stage` is `dest`, `reason` `unreadable`), or is a file or lies under one (`reason` `not_a_folder`), and the run ends with code 1 before anything is written. A platform, target, system, core or emulator named on the command line is unknown or cannot be used here (`stage` is `option`, `reason` `unknown`, `platform` the platform it was checked against or the name typed), refused before any download with code 2. The BIOS folder cannot be created (`stage` is `dest`, `reason` `not_writable`): a `summary` follows with what the run installed before it. Two systems are detected on one machine and no `--dest` names the folder (`stage` is `detect`, `reason` `conflict`). The platform named with `--platform` is found nowhere and no folder can be assumed for it: a card system with no card named, a portable emulator, Batocera or Recalbox named on a computer (their folder is the `bios` folder of their SHARE drive), or no usual folder on this system or one set aside behind a folder this user cannot open (`stage` is `detect`, `reason` `not_found`). So is a platform named by a file list or a pack, in a run without questions where it is found nowhere here. A `--dest` refused as the card of the system named carries the same `stage` and `reason`: another system's card, a folder inside a card, a drive with none of the files its card always has, or a folder that does not exist. `message` says it in words, and is the only thing that tells these cases apart |
| `scanned` | `folder`, `files`, `truncated`, `other_drives` | Each folder named with `--from`, once read and before `detect`. `files` counts the files found, `truncated` is true when the search stopped at 200,000 of them, and `other_drives` lists the folders below it on another drive, which were not entered |