mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-06 01:53:56 -06:00
feat: carry the board list in the pack
This commit is contained in:
1 parent
8d86b7b4f3
commit
ddafc2ff42
7 files changed
+238
-93
No files matched your search
+1
-1
@@ -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 = "37d1cbaddd22995b6390fcb95ed3d44d899f935bc93d288fe989d4dfae92775e"
|
||||
$defaultInstallSha256 = "d32691fb483d73a2845911957c2ce0fb18c0e7990605b3b466ef936ef8cb882e"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+70
-13
@@ -4124,13 +4124,52 @@ def fetch_manifest(plat: str) -> dict:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _pack_alone(plat: str) -> bool:
|
||||
"""A pack read without a file list for plat: one built before packs
|
||||
carried their install list records the systems of its files only."""
|
||||
return (LOCAL.pack is not None and LOCAL.manifest is None
|
||||
and (LOCAL.pack.install_list or {}).get("platform") != plat)
|
||||
|
||||
|
||||
def local_targets(plat: str) -> "dict | None":
|
||||
"""The cores per board this run has without the network: the list the
|
||||
pack carries, else targets/<platform>.json beside --manifest, where the
|
||||
repository keeps it (install/targets/). None when there is neither."""
|
||||
if LOCAL.pack is not None and LOCAL.pack.targets is not None \
|
||||
and plat in LOCAL.pack.platforms():
|
||||
return LOCAL.pack.targets
|
||||
if LOCAL.manifest is None:
|
||||
return None
|
||||
beside = LOCAL.manifest.parent / "targets" / f"{plat}.json"
|
||||
raw = _read_capped(beside, MAX_TARGETS_BYTES)
|
||||
if raw is None:
|
||||
return None
|
||||
try:
|
||||
return _validate_targets(json.loads(raw.decode("utf-8")))
|
||||
except (UnicodeDecodeError, ValueError) as exc:
|
||||
print(f" Error: the target list {beside} is not valid: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def fetch_targets(plat: str) -> "dict | None":
|
||||
"""Download the target core list.
|
||||
"""The target core list: the one this run has at hand, else downloaded.
|
||||
|
||||
Empty on 404, where the platform publishes no targets. None when the list
|
||||
could not be read: a timeout says nothing about the platform, and
|
||||
answering "no targets" for it sent users to the full pack.
|
||||
answering "no targets" for it sent users to the full pack. A run without
|
||||
the network and with no list at hand is refused, code 2, before any
|
||||
file is written.
|
||||
"""
|
||||
held = local_targets(plat)
|
||||
if held is not None:
|
||||
return held
|
||||
if LOCAL.offline:
|
||||
source = (f"{LOCAL.pack.name} carries none" if LOCAL.pack is not None
|
||||
else f"{offline_named()} makes no request")
|
||||
print(f"Error: --target reads the cores of each {display_name(plat)} board from a "
|
||||
f"list, and {source}. Put install/targets/{plat}.json beside the file list "
|
||||
"named with --manifest, or run with the network.", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
url = TARGETS_URL.format(platform=plat)
|
||||
try:
|
||||
return _validate_targets(_fetch_json(url, MAX_TARGETS_BYTES, "target list"))
|
||||
@@ -9089,7 +9128,7 @@ def _older_list_advice(plat: str = "") -> str:
|
||||
the pack's: the one-line installer does not change it, and a current
|
||||
pack was said to come from an older release.
|
||||
"""
|
||||
if LOCAL.pack is not None and LOCAL.manifest is None:
|
||||
if _pack_alone(plat):
|
||||
return (f" The list in {LOCAL.pack.name} does not record it: add --manifest FILE, "
|
||||
f"a copy of install/{plat or '<platform>'}.json, or run without the option.")
|
||||
if RELEASE_REF != DEFAULT_RELEASE_REF:
|
||||
@@ -9167,7 +9206,7 @@ def _requested_narrowing(
|
||||
"""The systems, cores and regions named on the command line, checked."""
|
||||
if not (args.system or args.core or args.region):
|
||||
return None
|
||||
pack_alone = LOCAL.pack is not None and LOCAL.manifest is None
|
||||
pack_alone = _pack_alone(plat)
|
||||
recorded = recorded_axes(files, omitted)
|
||||
if pack_alone:
|
||||
# A pack records the systems of its files, not their cores or
|
||||
@@ -10862,6 +10901,11 @@ class PackSource:
|
||||
# The version of the platform's list the pack was built from, "" for
|
||||
# a pack that does not record it.
|
||||
self.list_version = ""
|
||||
# The platform's install list and its cores per board, which a pack
|
||||
# built since carries: with them a pack alone narrows to a board as
|
||||
# the network list does. None for a pack that carries none.
|
||||
self.install_list: dict | None = None
|
||||
self.targets: dict | None = None
|
||||
try:
|
||||
for path in self.files:
|
||||
archive = zipfile.ZipFile(path)
|
||||
@@ -10963,6 +11007,15 @@ class PackSource:
|
||||
version = data.get("list_version")
|
||||
if isinstance(version, str) and _VERSION_TEXT.fullmatch(version):
|
||||
self.list_version = version
|
||||
if isinstance(data.get("install_list"), dict):
|
||||
self.install_list = data["install_list"]
|
||||
if data.get("targets") is not None:
|
||||
try:
|
||||
self.targets = _validate_targets(data["targets"])
|
||||
except ValueError:
|
||||
# Read as a pack that carries none: --target is then refused
|
||||
# with the reason, never applied from a broken list.
|
||||
self.targets = None
|
||||
for key in ("system_titles", "system_names"):
|
||||
held = data.get(key)
|
||||
if isinstance(held, dict):
|
||||
@@ -10999,9 +11052,11 @@ class PackSource:
|
||||
if f"_{PLATFORM_NAMES[plat].replace(' ', '_')}_" in stem]
|
||||
|
||||
def manifest(self, plat: str) -> dict:
|
||||
"""A file list for plat built from the pack's own: the same files at
|
||||
the same places, as extracting the pack would leave them, with the
|
||||
systems it records."""
|
||||
"""A file list for plat: the install list the pack carries, else one
|
||||
built from the pack's own, the same files at the same places, as
|
||||
extracting the pack would leave them, with the systems it records."""
|
||||
if self.install_list is not None and self.install_list.get("platform") == plat:
|
||||
return self.install_list
|
||||
return {
|
||||
"manifest_version": 2,
|
||||
"platform": plat,
|
||||
@@ -13238,13 +13293,14 @@ def main() -> None:
|
||||
# The runs the full-screen interface starts read it from here, and
|
||||
# the words name the option given.
|
||||
os.environ[OFFLINE_VARIABLE] = OFFLINE_FLAG_VALUE
|
||||
if offline_requested() and (args.target or args.list_targets):
|
||||
if (offline_requested() and (args.target or args.list_targets)
|
||||
and not (args.manifest or args.from_pack)):
|
||||
# No list can be at hand: a pack carries one, and so can the folder
|
||||
# of a --manifest (targets/<platform>.json).
|
||||
parser.error("--target and --list-targets read a core list from the network, "
|
||||
f"which {offline_named()} does not use")
|
||||
f"which {offline_named()} does not use: name a pack with --from-pack, "
|
||||
"or a file list with --manifest and targets/<platform>.json beside it")
|
||||
install_bounded_opener()
|
||||
if args.from_pack and args.target:
|
||||
parser.error("--target reads a core list from the network, which --from-pack "
|
||||
"does not use")
|
||||
if getattr(args, "from") and (
|
||||
args.list_platforms or args.list_targets or args.list_systems
|
||||
or args.list_cores or args.list_emulators
|
||||
@@ -14947,7 +15003,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
# narrows to it: a Batocera RPi4 took 8.6 GiB where its board needs
|
||||
# 5.5. Only on the device itself, where the board file exists.
|
||||
target = args.target
|
||||
if not target and not args.every_board and args.dest is None and not LOCAL.offline:
|
||||
if (not target and not args.every_board and args.dest is None
|
||||
and (not LOCAL.offline or local_targets(plat_name) is not None)):
|
||||
board = detect_board(plat_name)
|
||||
if board:
|
||||
known = fetch_targets(plat_name) or {}
|
||||
|
||||
+1
-1
@@ -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="37d1cbaddd22995b6390fcb95ed3d44d899f935bc93d288fe989d4dfae92775e"
|
||||
DEFAULT_INSTALL_SHA256="d32691fb483d73a2845911957c2ce0fb18c0e7990605b3b466ef936ef8cb882e"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{
|
||||
"dest": "roms/ports/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1088455,
|
||||
"sha256": "37d1cbaddd22995b6390fcb95ed3d44d899f935bc93d288fe989d4dfae92775e",
|
||||
"size": 1091372,
|
||||
"sha256": "d32691fb483d73a2845911957c2ce0fb18c0e7990605b3b466ef936ef8cb882e",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -71,8 +71,8 @@
|
||||
"muos": {
|
||||
"archive": {
|
||||
"dest": "ARCHIVE/RetroBIOS.muxapp",
|
||||
"size": 1142082,
|
||||
"sha256": "cce8bbfd699e23eb524244c01141d0a3316a3c3b769c54d3367e924b8d428ed2"
|
||||
"size": 1144999,
|
||||
"sha256": "eaa8cf2c295c6d57b349ef7b28fcb48cb47a7a13c251b13ff66dedbc5764bfd6"
|
||||
},
|
||||
"members": [
|
||||
{
|
||||
@@ -92,8 +92,8 @@
|
||||
{
|
||||
"name": "RetroBIOS/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1088455,
|
||||
"sha256": "37d1cbaddd22995b6390fcb95ed3d44d899f935bc93d288fe989d4dfae92775e",
|
||||
"size": 1091372,
|
||||
"sha256": "d32691fb483d73a2845911957c2ce0fb18c0e7990605b3b466ef936ef8cb882e",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,8 @@
|
||||
{
|
||||
"dest": ".config/modules/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1088455,
|
||||
"sha256": "37d1cbaddd22995b6390fcb95ed3d44d899f935bc93d288fe989d4dfae92775e",
|
||||
"size": 1091372,
|
||||
"sha256": "d32691fb483d73a2845911957c2ce0fb18c0e7990605b3b466ef936ef8cb882e",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
|
||||
+80
-64
@@ -2273,67 +2273,7 @@ def generate_target_manifests(targets_dir: str, output_dir: str) -> None:
|
||||
for yml_file in sorted(targets_path.glob("*.yml")):
|
||||
if yml_file.name.startswith("_"):
|
||||
continue
|
||||
with open(yml_file) as f:
|
||||
data = yaml_load(f) or {}
|
||||
targets = data.get("targets", {})
|
||||
if not isinstance(targets, dict):
|
||||
raise ValueError(f"{yml_file}: targets must be a mapping")
|
||||
|
||||
# The pack builder accepts the aliases declared in _overrides.yml, so
|
||||
# --target switch works there. Emitting only canonical names left the
|
||||
# installer rejecting the same word and, worse, carrying on with every
|
||||
# file instead: the user asked for a filter and got the full pack.
|
||||
overrides_path = targets_path / "_overrides.yml"
|
||||
alias_map: dict[str, list[str]] = {}
|
||||
platform_overrides: dict = {}
|
||||
if overrides_path.is_file():
|
||||
with open(overrides_path) as f:
|
||||
all_overrides = yaml_load(f) or {}
|
||||
platform_overrides = (
|
||||
all_overrides.get(yml_file.stem, {}).get("targets", {}) or {}
|
||||
)
|
||||
for tname, ovr in platform_overrides.items():
|
||||
names = [a for a in (ovr or {}).get("aliases", []) if isinstance(a, str)]
|
||||
if names:
|
||||
alias_map[tname] = names
|
||||
|
||||
result: dict[str, list[str] | None] = {}
|
||||
for target_name, target_info in targets.items():
|
||||
if not isinstance(target_name, str) or not target_name:
|
||||
raise ValueError(f"{yml_file}: target names must be non-empty strings")
|
||||
if not isinstance(target_info, dict):
|
||||
raise ValueError(f"{yml_file}: target {target_name!r} must be a mapping")
|
||||
cores = target_info.get("cores")
|
||||
if cores is None:
|
||||
result[target_name] = None
|
||||
continue
|
||||
if (
|
||||
not isinstance(cores, list)
|
||||
or any(not isinstance(core, str) or not core for core in cores)
|
||||
or len(cores) != len(set(cores))
|
||||
):
|
||||
raise ValueError(
|
||||
f"{yml_file}: target {target_name!r} cores must be unique "
|
||||
"non-empty strings"
|
||||
)
|
||||
# The builder's own reading, overrides included: a list published
|
||||
# another way makes the installer serve what the pack does not.
|
||||
# An empty list is a board with no core, not one with every core.
|
||||
resolved = load_target_config(
|
||||
yml_file.stem, target_name, str(targets_path.parent)
|
||||
)
|
||||
result[target_name] = None if resolved is None else sorted(resolved)
|
||||
|
||||
for target_name, names in alias_map.items():
|
||||
if target_name not in result:
|
||||
continue
|
||||
for alias in names:
|
||||
if alias in result:
|
||||
raise ValueError(
|
||||
f"{yml_file}: alias {alias!r} collides with a target name"
|
||||
)
|
||||
result[alias] = result[target_name]
|
||||
|
||||
result = target_list(yml_file)
|
||||
out_path = Path(output_dir) / f"{yml_file.stem}.json"
|
||||
write_text_atomic(out_path, json.dumps(result, indent=2, sort_keys=True) + "\n")
|
||||
count += 1
|
||||
@@ -2341,6 +2281,73 @@ def generate_target_manifests(targets_dir: str, output_dir: str) -> None:
|
||||
print(f"Generated {count} target manifest(s) in {output_dir}")
|
||||
|
||||
|
||||
def target_list(yml_file: Path) -> dict[str, list[str] | None]:
|
||||
"""A platform's cores per hardware target, as install/targets/ publishes
|
||||
them and a pack carries them: aliases included, overrides applied."""
|
||||
targets_path = yml_file.parent
|
||||
with open(yml_file) as f:
|
||||
data = yaml_load(f) or {}
|
||||
targets = data.get("targets", {})
|
||||
if not isinstance(targets, dict):
|
||||
raise ValueError(f"{yml_file}: targets must be a mapping")
|
||||
|
||||
# The pack builder accepts the aliases declared in _overrides.yml, so
|
||||
# --target switch works there. Emitting only canonical names left the
|
||||
# installer rejecting the same word and, worse, carrying on with every
|
||||
# file instead: the user asked for a filter and got the full pack.
|
||||
overrides_path = targets_path / "_overrides.yml"
|
||||
alias_map: dict[str, list[str]] = {}
|
||||
platform_overrides: dict = {}
|
||||
if overrides_path.is_file():
|
||||
with open(overrides_path) as f:
|
||||
all_overrides = yaml_load(f) or {}
|
||||
platform_overrides = (
|
||||
all_overrides.get(yml_file.stem, {}).get("targets", {}) or {}
|
||||
)
|
||||
for tname, ovr in platform_overrides.items():
|
||||
names = [a for a in (ovr or {}).get("aliases", []) if isinstance(a, str)]
|
||||
if names:
|
||||
alias_map[tname] = names
|
||||
|
||||
result: dict[str, list[str] | None] = {}
|
||||
for target_name, target_info in targets.items():
|
||||
if not isinstance(target_name, str) or not target_name:
|
||||
raise ValueError(f"{yml_file}: target names must be non-empty strings")
|
||||
if not isinstance(target_info, dict):
|
||||
raise ValueError(f"{yml_file}: target {target_name!r} must be a mapping")
|
||||
cores = target_info.get("cores")
|
||||
if cores is None:
|
||||
result[target_name] = None
|
||||
continue
|
||||
if (
|
||||
not isinstance(cores, list)
|
||||
or any(not isinstance(core, str) or not core for core in cores)
|
||||
or len(cores) != len(set(cores))
|
||||
):
|
||||
raise ValueError(
|
||||
f"{yml_file}: target {target_name!r} cores must be unique "
|
||||
"non-empty strings"
|
||||
)
|
||||
# The builder's own reading, overrides included: a list published
|
||||
# another way makes the installer serve what the pack does not.
|
||||
# An empty list is a board with no core, not one with every core.
|
||||
resolved = load_target_config(
|
||||
yml_file.stem, target_name, str(targets_path.parent)
|
||||
)
|
||||
result[target_name] = None if resolved is None else sorted(resolved)
|
||||
|
||||
for target_name, names in alias_map.items():
|
||||
if target_name not in result:
|
||||
continue
|
||||
for alias in names:
|
||||
if alias in result:
|
||||
raise ValueError(
|
||||
f"{yml_file}: alias {alias!r} collides with a target name"
|
||||
)
|
||||
result[alias] = result[target_name]
|
||||
return result
|
||||
|
||||
|
||||
def _resolve_systems(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
|
||||
"""Turn --system into the platform's system ids, before any work.
|
||||
|
||||
@@ -2930,6 +2937,7 @@ def _run_platform_packs(
|
||||
# The install list of the same choices names each file's
|
||||
# systems: the pack records them, so a pack alone (a
|
||||
# handheld without Wi-Fi) can be narrowed by system.
|
||||
board_list = Path(args.platforms_dir) / "targets" / f"{representative}.yml"
|
||||
systems_for[os.path.basename(zip_path)] = pack_systems(generate_manifest(
|
||||
representative, args.platforms_dir, db, args.bios_dir,
|
||||
registry_path, emulators_dir=args.emulators_dir,
|
||||
@@ -2939,7 +2947,7 @@ def _run_platform_packs(
|
||||
offline=args.offline, required_only=required_only,
|
||||
data_registry=data_registry,
|
||||
no_game_data=getattr(args, "no_game_data", False),
|
||||
))
|
||||
), target_list(board_list) if board_list.is_file() else None)
|
||||
|
||||
print("\nVerifying packs and generating manifests...")
|
||||
skip_conf = bool(system_filter or args.split)
|
||||
@@ -4770,7 +4778,7 @@ def _manifest_core_index(
|
||||
|
||||
|
||||
|
||||
def pack_systems(install_manifest: dict) -> dict:
|
||||
def pack_systems(install_manifest: dict, targets: dict | None = None) -> dict:
|
||||
"""What a pack's manifest.json records of its systems, from the install
|
||||
list built with the same choices: each file's systems, by its path in
|
||||
the pack, and the names a person reads for them.
|
||||
@@ -4778,6 +4786,11 @@ def pack_systems(install_manifest: dict) -> dict:
|
||||
A pack recorded none: the installer refused --system with a pack alone
|
||||
("the file list comes from an older release") and offered no Choose
|
||||
systems, the route of a handheld without Wi-Fi.
|
||||
|
||||
The install list itself and the platform's cores per board (targets, as
|
||||
install/targets/ publishes them) ride along: with the pack alone, a
|
||||
board was never applied and --target was refused, and a Batocera RPi4
|
||||
card took 8.9 GB where its board reads 5.8.
|
||||
"""
|
||||
files = install_manifest.get("files", [])
|
||||
entries = [*files, *install_manifest.get("omitted_files", [])]
|
||||
@@ -4803,6 +4816,9 @@ def pack_systems(install_manifest: dict) -> dict:
|
||||
# the one the machine runs when it has the pack alone.
|
||||
if install_manifest.get("version"):
|
||||
systems["list_version"] = install_manifest["version"]
|
||||
systems["install_list"] = install_manifest
|
||||
if targets is not None:
|
||||
systems["targets"] = targets
|
||||
return systems
|
||||
|
||||
|
||||
@@ -4818,7 +4834,7 @@ def annotate_pack_systems(manifest: dict, systems: dict | None) -> None:
|
||||
for key in ("system_titles", "system_names"):
|
||||
if systems.get(key):
|
||||
manifest[key] = systems[key]
|
||||
for key in ("list_records", "list_version"):
|
||||
for key in ("list_records", "list_version", "install_list", "targets"):
|
||||
if key in systems:
|
||||
manifest[key] = systems[key]
|
||||
|
||||
|
||||
@@ -386,6 +386,75 @@ class SplitParts(Scratch):
|
||||
self.assertFalse(self.dest.exists())
|
||||
|
||||
|
||||
def _carrying(pack: Path, extra: dict) -> None:
|
||||
"""Add what a pack built since carries to its manifest.json."""
|
||||
with zipfile.ZipFile(pack) as archive:
|
||||
members = {info.filename: archive.read(info) for info in archive.infolist()}
|
||||
listed = {**json.loads(members["manifest.json"]), **extra}
|
||||
members["manifest.json"] = json.dumps(listed).encode()
|
||||
with zipfile.ZipFile(pack, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||
for name, data in members.items():
|
||||
archive.writestr(name, data)
|
||||
|
||||
|
||||
# A GBA board and a PlayStation board: PAYLOAD's files split between them.
|
||||
BOARDS = {"gba-board": ["mgba"], "psx-board": ["pcsx_rearmed"]}
|
||||
|
||||
|
||||
class ABoardWithoutTheNetwork(Scratch):
|
||||
"""A pack alone, or --offline, never applied a board: --target was
|
||||
refused, and a board found on the device was ignored. A Batocera RPi4
|
||||
card took 8.9 GB from its pack where its board reads 5.8."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.downloads = self.root / "downloads"
|
||||
self.downloads.mkdir()
|
||||
self.pack = _pack(self.downloads, "RetroArch_v1_BIOS_Pack.zip", PAYLOAD)
|
||||
|
||||
def installed(self) -> set[str]:
|
||||
return {p.relative_to(self.dest).as_posix() for p in self.dest.rglob("*")
|
||||
if p.is_file()}
|
||||
|
||||
def test_the_builder_writes_both_into_the_pack_list(self):
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
import generate_pack # noqa: PLC0415
|
||||
|
||||
install_list = manifest()
|
||||
board_file = REPO_ROOT / "platforms" / "targets" / "batocera.yml"
|
||||
boards = generate_pack.target_list(board_file)
|
||||
self.assertEqual(boards, json.loads(
|
||||
(REPO_ROOT / "install" / "targets" / "batocera.json").read_text()))
|
||||
pack_list = {"files": []}
|
||||
generate_pack.annotate_pack_systems(
|
||||
pack_list, generate_pack.pack_systems(install_list, boards))
|
||||
self.assertEqual(pack_list["install_list"], install_list)
|
||||
self.assertEqual(pack_list["targets"], boards)
|
||||
|
||||
def test_the_pack_applies_its_board_list(self):
|
||||
_carrying(self.pack, {"install_list": manifest(), "targets": BOARDS})
|
||||
proc = self.run_installer("--from-pack", str(self.pack), "--target", "gba-board",
|
||||
"--no-input")
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertEqual(self.installed(), {"gba_bios.bin"})
|
||||
|
||||
def test_a_list_beside_the_manifest_applies_offline(self):
|
||||
(self.root / "targets").mkdir()
|
||||
(self.root / "targets" / "retroarch.json").write_text(json.dumps(BOARDS))
|
||||
self.complete()
|
||||
proc = self.run_installer("--offline", "--check", "--manifest", str(self.list),
|
||||
"--target", "psx-board", "--no-input")
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
self.assertIn("for target psx-board", proc.stdout)
|
||||
|
||||
def test_a_pack_without_a_list_says_why(self):
|
||||
proc = self.run_installer("--from-pack", str(self.pack), "--target", "gba-board",
|
||||
"--no-input")
|
||||
self.assertEqual(proc.returncode, 2)
|
||||
self.assertIn(f"{self.pack.name} carries none", proc.stderr)
|
||||
self.assertFalse(self.dest.exists())
|
||||
|
||||
|
||||
class FromPackInstallsOffline(Scratch):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
+9
-6
@@ -124,9 +124,12 @@ The platform is read from the pack's name when `--platform` is not given. The
|
||||
file list is the one the pack carries (`manifest.json`), with each file's
|
||||
SHA-1 and size, and every file is checked against it before it takes its
|
||||
place. A data tree (Dolphin's `Sys/`, PPSSPP's assets) comes from the pack
|
||||
too, not from its source archive. `--target` reads a core list from the
|
||||
network and is refused with `--from-pack`. A board found on the device is not
|
||||
applied either: every board's files are installed. At a terminal the run
|
||||
too, not from its source archive. A pack built since 2026-11 also carries
|
||||
its platform's install list and the cores of each board (the
|
||||
`install/targets/<platform>.json` list): `--target` applies with the pack
|
||||
alone, and so does a board found on the device. An older pack carries
|
||||
neither, and `--target` is then refused before anything is written. At a
|
||||
terminal the run
|
||||
finds a card mounted here, asks the folder when nothing is found, and names
|
||||
the BIOS folder on its menu before anything is copied.
|
||||
|
||||
@@ -953,7 +956,7 @@ what the options name, everything by default.
|
||||
|--------|--------|
|
||||
| <span id="option-platform"></span>`--platform NAME` | Install for this platform instead of the detected one. A system `--list-platforms` names under its own files (`muos`, `arkos`) stands for the file list it gets, and the run says which. ES-DE, LaunchBox and Android, which keep no BIOS folder, are answered with the run that fills each emulator's folder. Unknown names are refused with the closest ones |
|
||||
| <span id="option-dest"></span>`--dest PATH` | Destination directory, overriding detection. Without `--platform`, the file list is the platform detected here: at a terminal the installer asks, Enter taking the one found, and otherwise it uses the one found, RetroArch's when nothing is found, and refuses when several are. The run prints whose list it is. A folder that does not exist is named with the nearest one that does, then created, except for a system that runs from its SD card, which refuses it. When more than its last level is missing (a mistyped path), a run at a terminal asks first, and the answer defaults to no |
|
||||
| <span id="option-target"></span>`--target NAME` | Keep only the files the cores of that hardware target need, and the systems they emulate. An unknown target is refused rather than ignored, since carrying on would install everything. On Batocera, Recalbox, ROCKNIX and Lakka the board is read from the system (`batocera.arch`, `recalbox.arch`, `HW_DEVICE`, `LIBREELEC_DEVICE` or `LIBREELEC_PROJECT`) and applied when no `--target` is given |
|
||||
| <span id="option-target"></span>`--target NAME` | Keep only the files the cores of that hardware target need, and the systems they emulate. An unknown target is refused rather than ignored, since carrying on would install everything. On Batocera, Recalbox, ROCKNIX and Lakka the board is read from the system (`batocera.arch`, `recalbox.arch`, `HW_DEVICE`, `LIBREELEC_DEVICE` or `LIBREELEC_PROJECT`) and applied when no `--target` is given, from a pack alone too when it carries the core list per board |
|
||||
| <span id="option-every-board"></span>`--every-board` | Install the files of every board instead of the one the system names, for a card that moves between devices |
|
||||
| <span id="option-installed-cores"></span>`--installed-cores` | Keep only what the cores already installed read (RetroArch, Lakka, RetroPie). See [Installed cores](#installed-cores) |
|
||||
| <span id="option-cores-dir"></span>`--cores-dir PATH` | The folder RetroArch loads its cores from, for `--installed-cores` when it is not found. Implies `--installed-cores` |
|
||||
@@ -978,7 +981,7 @@ what the options name, everything by default.
|
||||
| <span id="option-events"></span>`--events` | Write the run as JSON events on standard output, one object per line, and the usual text on standard error. Implies `--no-input`. See [Machine-readable events](#machine-readable-events) |
|
||||
| <span id="option-from"></span>`--from FOLDER` | Copy the BIOS files a folder holds (an old SD card, a backup, a ZIP) instead of downloading them, each one recognised by its hash and copied, never moved. Repeat for several folders. See [Copy from an old card](#copy-from-an-old-card) |
|
||||
| <span id="option-from-pack"></span>`--from-pack PATH` | Install from a pack already downloaded: its ZIP, one of its parts (the others are found in the same folder), or the folder holding them. Makes no network request and checks every file's hash. See [Without a network](#without-a-network-offline-install) |
|
||||
| <span id="option-offline"></span>`--offline` | Make no network request. `--check`, `--from`, `--from-pack` and `--manifest` work without one, the file list stops the run at once when none of them gives it, and a file the run would download fails at once. `RETROBIOS_INSTALL_OFFLINE=1` does the same. Refused with `--target` and `--list-targets`, which read a core list from the network |
|
||||
| <span id="option-offline"></span>`--offline` | Make no network request. `--check`, `--from`, `--from-pack` and `--manifest` work without one, the file list stops the run at once when none of them gives it, and a file the run would download fails at once. `RETROBIOS_INSTALL_OFFLINE=1` does the same. `--target` and `--list-targets` read the core list per board a pack carries, or `targets/<platform>.json` beside `--manifest`, and are refused when neither gives one |
|
||||
| <span id="option-manifest"></span>`--manifest FILE` | Read the file list from FILE, a copy of `install/<platform>.json`, instead of downloading it. See [Without a network](#without-a-network-offline-install) |
|
||||
| <span id="option-restore"></span>`--restore FOLDER` | Put back the files a run replaced and kept in FOLDER, a dated folder inside `retrobios-backup`. Takes no other option but `--dry-run` and `--no-input`. See [Files you already had](#files-you-already-had) |
|
||||
| <span id="option-tui"></span>`--tui` | Open the full-screen interface, or stop with the reason it cannot open here (code 2) instead of falling back to the questions. Wins over `RETROBIOS_PLAIN` and `ACCESSIBLE`. See [The full-screen interface](#the-full-screen-interface) |
|
||||
@@ -1722,7 +1725,7 @@ fields, the installer refuses to narrow rather than install everything.
|
||||
|------|---------|
|
||||
| `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 `--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` or `--from-pack`, `--target` or `--list-targets` with `--offline` (or `RETROBIOS_INSTALL_OFFLINE` set), which read a core list from the network, `--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 |
|
||||
| `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`), `--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 |
|
||||
| `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`) |
|
||||
|
||||
|
||||
Reference in new issue
Block a user