mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-12 13:03:54 -06:00
chore: merge installer fixes, fifth pass
This commit is contained in:
commit
16f8c869a5
35 files changed
+11023
-330
No files matched your search
@@ -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
|
||||
```
|
||||
|
||||

|
||||
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+1765
-19
File diff suppressed because it is too large.
Load diff
@@ -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 @@
|
||||
{"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 @@
|
||||
{"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 @@
|
||||
{"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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+1976
-19
File diff suppressed because it is too large.
Load diff
+1401
-19
File diff suppressed because it is too large.
Load diff
+1947
-19
File diff suppressed because it is too large.
Load diff
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
Reference in new issue
Block a user