mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-10 13:33:24 -05:00
fix: one selection and one name per system
This commit is contained in:
1 parent
b9cf7c7415
commit
6e6ae77ccd
4 files changed
+173
-35
No files matched your search
@@ -994,6 +994,27 @@ class ProfileSelectionError(ValueError):
|
|||||||
"""A named profile cannot answer for itself."""
|
"""A named profile cannot answer for itself."""
|
||||||
|
|
||||||
|
|
||||||
|
def profiles_for_systems(
|
||||||
|
profiles: dict, system_ids: list[str], standalone: bool = False
|
||||||
|
) -> list[str]:
|
||||||
|
"""Profile keys that serve any of the systems, spelled either way.
|
||||||
|
|
||||||
|
Compared on the normalized id: stella2014 writes atari_2600 where every
|
||||||
|
other profile writes atari-2600, and an exact match built two different
|
||||||
|
packs under one file name, the second replacing the first.
|
||||||
|
"""
|
||||||
|
wanted = {_norm_system_id(sid) for sid in system_ids}
|
||||||
|
matching = []
|
||||||
|
for name, profile in sorted(profiles.items()):
|
||||||
|
if profile.get("type") in ("launcher", "alias", "test"):
|
||||||
|
continue
|
||||||
|
if standalone and "standalone" not in profile.get("type", "libretro"):
|
||||||
|
continue
|
||||||
|
if wanted & {_norm_system_id(sid) for sid in profile.get("systems", [])}:
|
||||||
|
matching.append(name)
|
||||||
|
return matching
|
||||||
|
|
||||||
|
|
||||||
def select_emulator_profiles(
|
def select_emulator_profiles(
|
||||||
profile_names: list[str],
|
profile_names: list[str],
|
||||||
all_profiles: dict,
|
all_profiles: dict,
|
||||||
@@ -1768,6 +1789,8 @@ from ziptools import ( # noqa: E402,F401
|
|||||||
)
|
)
|
||||||
from artifacts import ( # noqa: E402,F401
|
from artifacts import ( # noqa: E402,F401
|
||||||
write_if_changed,
|
write_if_changed,
|
||||||
|
write_text_atomic,
|
||||||
|
copy_file_atomic,
|
||||||
ArtifactLockBusy,
|
ArtifactLockBusy,
|
||||||
artifact_lock,
|
artifact_lock,
|
||||||
hold_artifact_lock,
|
hold_artifact_lock,
|
||||||
|
|||||||
+44
-20
@@ -31,6 +31,8 @@ sys.path.insert(0, os.path.dirname(__file__))
|
|||||||
from common import resolve_platform_cores as _platform_cores
|
from common import resolve_platform_cores as _platform_cores
|
||||||
from common import (
|
from common import (
|
||||||
apply_target_overrides,
|
apply_target_overrides,
|
||||||
|
profiles_for_systems,
|
||||||
|
write_text_atomic,
|
||||||
artifact_lock,
|
artifact_lock,
|
||||||
ArtifactLockBusy,
|
ArtifactLockBusy,
|
||||||
build_target_cores_cache,
|
build_target_cores_cache,
|
||||||
@@ -1559,16 +1561,7 @@ def generate_system_pack(
|
|||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Generate a ZIP pack for all emulators supporting given system IDs."""
|
"""Generate a ZIP pack for all emulators supporting given system IDs."""
|
||||||
profiles = load_emulator_profiles(emulators_dir)
|
profiles = load_emulator_profiles(emulators_dir)
|
||||||
matching = []
|
matching = profiles_for_systems(profiles, system_ids, standalone)
|
||||||
for name, profile in sorted(profiles.items()):
|
|
||||||
if profile.get("type") in ("launcher", "alias", "test"):
|
|
||||||
continue
|
|
||||||
emu_systems = set(profile.get("systems", []))
|
|
||||||
if emu_systems & set(system_ids):
|
|
||||||
ptype = profile.get("type", "libretro")
|
|
||||||
if standalone and "standalone" not in ptype:
|
|
||||||
continue
|
|
||||||
matching.append(name)
|
|
||||||
|
|
||||||
if not matching:
|
if not matching:
|
||||||
all_systems: set[str] = set()
|
all_systems: set[str] = set()
|
||||||
@@ -1590,10 +1583,9 @@ def generate_system_pack(
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Use system-based ZIP name
|
# Named like the platform's --system tag: one spelling of a system is
|
||||||
sys_display = "_".join(
|
# one file name, and one selection.
|
||||||
_name_part("_".join(w.title() for w in sid.split("-"))) for sid in system_ids
|
sys_display = "_".join(_system_display_name(sid) for sid in system_ids)
|
||||||
)
|
|
||||||
# Built in a scratch directory: under its emulator name in output_dir it
|
# Built in a scratch directory: under its emulator name in output_dir it
|
||||||
# overwrote, then carried away, an --emulator pack already there.
|
# overwrote, then carried away, an --emulator pack already there.
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
@@ -1829,7 +1821,16 @@ def generate_md5_pack(
|
|||||||
if emulator_name and emulators_dir:
|
if emulator_name and emulators_dir:
|
||||||
profiles = load_emulator_profiles(emulators_dir, skip_aliases=False)
|
profiles = load_emulator_profiles(emulators_dir, skip_aliases=False)
|
||||||
if emulator_name in profiles:
|
if emulator_name in profiles:
|
||||||
profile = profiles[emulator_name]
|
# The same gate as --emulator: a core with no standalone build
|
||||||
|
# has no standalone layout, and a pack named for one would be
|
||||||
|
# the libretro pack under a name that promises otherwise.
|
||||||
|
try:
|
||||||
|
(_name, profile), = select_emulator_profiles(
|
||||||
|
[emulator_name], profiles, standalone
|
||||||
|
)
|
||||||
|
except ProfileSelectionError as exc:
|
||||||
|
print(f"Error: {exc}", file=sys.stderr)
|
||||||
|
return None
|
||||||
emu_display = profile.get("emulator", emulator_name)
|
emu_display = profile.get("emulator", emulator_name)
|
||||||
emu_pack_structure = profile.get("pack_structure")
|
emu_pack_structure = profile.get("pack_structure")
|
||||||
for fe in profile.get("files", []):
|
for fe in profile.get("files", []):
|
||||||
@@ -2002,9 +2003,7 @@ def generate_target_manifests(targets_dir: str, output_dir: str) -> None:
|
|||||||
result[alias] = result[target_name]
|
result[alias] = result[target_name]
|
||||||
|
|
||||||
out_path = Path(output_dir) / f"{yml_file.stem}.json"
|
out_path = Path(output_dir) / f"{yml_file.stem}.json"
|
||||||
with open(out_path, "w") as f:
|
write_text_atomic(out_path, json.dumps(result, indent=2, sort_keys=True) + "\n")
|
||||||
json.dump(result, f, indent=2, sort_keys=True)
|
|
||||||
f.write("\n")
|
|
||||||
count += 1
|
count += 1
|
||||||
print(f" {yml_file.stem}: {len(result)} targets")
|
print(f" {yml_file.stem}: {len(result)} targets")
|
||||||
print(f"Generated {count} target manifest(s) in {output_dir}")
|
print(f"Generated {count} target manifest(s) in {output_dir}")
|
||||||
@@ -2106,8 +2105,7 @@ def _write_manifest_if_changed(path: str, manifest: dict) -> None:
|
|||||||
new_cmp = {k: v for k, v in manifest.items() if k != "generated"}
|
new_cmp = {k: v for k, v in manifest.items() if k != "generated"}
|
||||||
if old_cmp == new_cmp:
|
if old_cmp == new_cmp:
|
||||||
return # no content change, keep existing timestamp
|
return # no content change, keep existing timestamp
|
||||||
with open(path, "w") as f:
|
write_text_atomic(path, new_json)
|
||||||
f.write(new_json)
|
|
||||||
|
|
||||||
|
|
||||||
def _run_manifest_mode(
|
def _run_manifest_mode(
|
||||||
@@ -2300,6 +2298,11 @@ def _pack_label(source: str, required_only: bool) -> str:
|
|||||||
return f"[source={source}, required]" if required_only else f"[source={source}]"
|
return f"[source={source}, required]" if required_only else f"[source={source}]"
|
||||||
|
|
||||||
|
|
||||||
|
def _produced(split: bool, zip_path, zip_paths) -> bool:
|
||||||
|
"""Whether the build wrote what the run asked for."""
|
||||||
|
return bool(zip_paths if split else zip_path)
|
||||||
|
|
||||||
|
|
||||||
def _run_platform_packs(
|
def _run_platform_packs(
|
||||||
args,
|
args,
|
||||||
groups,
|
groups,
|
||||||
@@ -2402,6 +2405,12 @@ def _run_platform_packs(
|
|||||||
except (FileNotFoundError, OSError, yaml.YAMLError) as e:
|
except (FileNotFoundError, OSError, yaml.YAMLError) as e:
|
||||||
print(f" ERROR: {e}")
|
print(f" ERROR: {e}")
|
||||||
failed.append(representative)
|
failed.append(representative)
|
||||||
|
else:
|
||||||
|
# A builder that declined (no system left under the target)
|
||||||
|
# produced nothing: the run must say so, not verify whatever
|
||||||
|
# the output directory already held and exit 0.
|
||||||
|
if not _produced(args.split, zip_path, zip_paths):
|
||||||
|
failed.append(representative)
|
||||||
|
|
||||||
print("\nVerifying packs and generating manifests...")
|
print("\nVerifying packs and generating manifests...")
|
||||||
skip_conf = bool(system_filter or args.split)
|
skip_conf = bool(system_filter or args.split)
|
||||||
@@ -3081,6 +3090,18 @@ def _manifest_region_drops(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _record_unplaceable(unplaceable: list[dict], record_omission) -> None:
|
||||||
|
"""Held by the collection, nowhere to put it on this platform.
|
||||||
|
|
||||||
|
Said in the manifest, where RomM's DOSBox Pure ROMs were simply absent.
|
||||||
|
"""
|
||||||
|
for u in unplaceable:
|
||||||
|
record_omission(
|
||||||
|
u.get("path") or u.get("name", ""), u, u.get("system") or "",
|
||||||
|
"no_platform_slug", [u.get("emulator", "")],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_manifest(
|
def generate_manifest(
|
||||||
platform_name: str,
|
platform_name: str,
|
||||||
platforms_dir: str,
|
platforms_dir: str,
|
||||||
@@ -3322,6 +3343,7 @@ def generate_manifest(
|
|||||||
seen_lower.add(dedup_key.lower())
|
seen_lower.add(dedup_key.lower())
|
||||||
|
|
||||||
# Phase 2: core complement (emulator extras)
|
# Phase 2: core complement (emulator extras)
|
||||||
|
unplaceable: list[dict] = []
|
||||||
if source != "platform":
|
if source != "platform":
|
||||||
core_files = _collect_emulator_extras(
|
core_files = _collect_emulator_extras(
|
||||||
config,
|
config,
|
||||||
@@ -3332,9 +3354,11 @@ def generate_manifest(
|
|||||||
emu_profiles,
|
emu_profiles,
|
||||||
target_cores=target_cores,
|
target_cores=target_cores,
|
||||||
include_all=(source == "truth"),
|
include_all=(source == "truth"),
|
||||||
|
unplaceable=unplaceable,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
core_files = []
|
core_files = []
|
||||||
|
_record_unplaceable(unplaceable, record_omission)
|
||||||
total_size += _manifest_core_entries(
|
total_size += _manifest_core_entries(
|
||||||
core_files, config, db, bios_dir, base_dest, repo_root,
|
core_files, config, db, bios_dir, base_dest, repo_root,
|
||||||
zip_contents, offline, region_drops, case_insensitive,
|
zip_contents, offline, region_drops, case_insensitive,
|
||||||
|
|||||||
+31
-15
@@ -34,6 +34,7 @@ import slots
|
|||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(__file__))
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
from common import (
|
from common import (
|
||||||
|
profiles_for_systems,
|
||||||
PROFILE_IDENTITY_FIELDS,
|
PROFILE_IDENTITY_FIELDS,
|
||||||
list_available_targets,
|
list_available_targets,
|
||||||
list_platform_system_ids,
|
list_platform_system_ids,
|
||||||
@@ -716,6 +717,17 @@ def find_exclusion_notes(
|
|||||||
# Platform verification
|
# Platform verification
|
||||||
|
|
||||||
|
|
||||||
|
def _mark_unplaced(config: dict, undeclared: list[dict], profiles: dict) -> None:
|
||||||
|
"""What the builder cannot place is not in the pack, held or not."""
|
||||||
|
from packextras import unplaceable_extras
|
||||||
|
|
||||||
|
unplaced = unplaceable_extras(config, undeclared, profiles)
|
||||||
|
for u in undeclared:
|
||||||
|
if (u.get("emulator", ""), u.get("name", ""), u.get("path") or "") in unplaced:
|
||||||
|
u["in_pack"] = False
|
||||||
|
u["omitted"] = "no platform slug for its system"
|
||||||
|
|
||||||
|
|
||||||
def _twin_index(
|
def _twin_index(
|
||||||
verify_systems: dict, db: dict, base_dest: str, zip_contents: dict,
|
verify_systems: dict, db: dict, base_dest: str, zip_contents: dict,
|
||||||
data_dir_registry: dict | None,
|
data_dir_registry: dict | None,
|
||||||
@@ -1020,6 +1032,7 @@ def verify_platform(
|
|||||||
)
|
)
|
||||||
not in region_drops
|
not in region_drops
|
||||||
]
|
]
|
||||||
|
_mark_unplaced(config, undeclared, profiles)
|
||||||
exclusions = find_exclusion_notes(
|
exclusions = find_exclusion_notes(
|
||||||
config, emulators_dir, emu_profiles, target_cores=target_cores
|
config, emulators_dir, emu_profiles, target_cores=target_cores
|
||||||
)
|
)
|
||||||
@@ -1191,17 +1204,27 @@ def _print_undeclared_entry(u: dict, prefix: str, verbose: bool) -> None:
|
|||||||
print(f" [{'+'.join(checks)}]")
|
print(f" [{'+'.join(checks)}]")
|
||||||
|
|
||||||
|
|
||||||
|
def _split_undeclared(undeclared: list[dict]) -> tuple[list[dict], list[dict], list[dict]]:
|
||||||
|
"""Game data, firmware the pack places, firmware it holds but cannot place.
|
||||||
|
|
||||||
|
Everything that is not game data is firmware the core loads, archives
|
||||||
|
included: a bios_zip sat in neither list, so a required one that was
|
||||||
|
missing was never printed.
|
||||||
|
"""
|
||||||
|
game_data = [u for u in undeclared if u.get("category", "bios") == "game_data"]
|
||||||
|
firmware = [u for u in undeclared if u.get("category", "bios") != "game_data"]
|
||||||
|
unplaced = [u for u in firmware if u["in_repo"] and not u.get("in_pack", True)]
|
||||||
|
placed = [u for u in firmware if u.get("in_pack", True)]
|
||||||
|
return game_data, placed, unplaced
|
||||||
|
|
||||||
|
|
||||||
def _print_undeclared_section(result: dict, verbose: bool) -> None:
|
def _print_undeclared_section(result: dict, verbose: bool) -> None:
|
||||||
"""Print cross-reference section for undeclared files used by cores."""
|
"""Print cross-reference section for undeclared files used by cores."""
|
||||||
undeclared = result.get("undeclared_files", [])
|
undeclared = result.get("undeclared_files", [])
|
||||||
if not undeclared:
|
if not undeclared:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Everything that is not game data is firmware the core loads, archives
|
game_data, bios_files, unplaced = _split_undeclared(undeclared)
|
||||||
# included: a bios_zip sat in neither list, so a required one that was
|
|
||||||
# missing was never printed.
|
|
||||||
game_data = [u for u in undeclared if u.get("category", "bios") == "game_data"]
|
|
||||||
bios_files = [u for u in undeclared if u.get("category", "bios") != "game_data"]
|
|
||||||
|
|
||||||
req_not_in_repo = [
|
req_not_in_repo = [
|
||||||
u
|
u
|
||||||
@@ -1224,6 +1247,8 @@ def _print_undeclared_section(result: dict, verbose: bool) -> None:
|
|||||||
print(
|
print(
|
||||||
f" Core files: {core_in_pack} in pack, {core_missing_req} required missing, {core_missing_opt} optional missing"
|
f" Core files: {core_in_pack} in pack, {core_missing_req} required missing, {core_missing_opt} optional missing"
|
||||||
)
|
)
|
||||||
|
if unplaced:
|
||||||
|
print(f" Core files held but not packed: {len(unplaced)} ({unplaced[0]['omitted']})")
|
||||||
|
|
||||||
for u in req_not_in_repo:
|
for u in req_not_in_repo:
|
||||||
_print_undeclared_entry(u, "MISSING (required)", verbose)
|
_print_undeclared_entry(u, "MISSING (required)", verbose)
|
||||||
@@ -1637,16 +1662,7 @@ def verify_system(
|
|||||||
) -> dict:
|
) -> dict:
|
||||||
"""Verify files for all emulators supporting given system IDs."""
|
"""Verify files for all emulators supporting given system IDs."""
|
||||||
profiles = load_emulator_profiles(emulators_dir)
|
profiles = load_emulator_profiles(emulators_dir)
|
||||||
matching = []
|
matching = profiles_for_systems(profiles, system_ids, standalone)
|
||||||
for name, profile in sorted(profiles.items()):
|
|
||||||
if profile.get("type") in ("launcher", "alias", "test"):
|
|
||||||
continue
|
|
||||||
emu_systems = set(profile.get("systems", []))
|
|
||||||
if emu_systems & set(system_ids):
|
|
||||||
ptype = profile.get("type", "libretro")
|
|
||||||
if standalone and "standalone" not in ptype:
|
|
||||||
continue # skip non-standalone in standalone mode
|
|
||||||
matching.append(name)
|
|
||||||
|
|
||||||
if not matching:
|
if not matching:
|
||||||
all_systems: set[str] = set()
|
all_systems: set[str] = set()
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""One system, two spellings, one pack.
|
||||||
|
|
||||||
|
stella2014 and stella2023 write atari_2600 where every other profile writes
|
||||||
|
atari-2600. --system matched the spelling exactly and named the ZIP from
|
||||||
|
it, so the two spellings built two different packs under one file name,
|
||||||
|
the second replacing the first; --from-md5 --emulator X --standalone
|
||||||
|
accepted a libretro-only core and named a standalone pack that was the
|
||||||
|
libretro pack.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||||
|
|
||||||
|
import common # noqa: E402
|
||||||
|
import generate_pack as builder # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class SpellingsAgree(unittest.TestCase):
|
||||||
|
def test_both_spellings_select_the_same_profiles(self):
|
||||||
|
profiles = common.load_emulator_profiles(str(REPO_ROOT / "emulators"))
|
||||||
|
dashed = common.profiles_for_systems(profiles, ["atari-2600"])
|
||||||
|
underscored = common.profiles_for_systems(profiles, ["atari_2600"])
|
||||||
|
self.assertEqual(dashed, underscored)
|
||||||
|
self.assertIn("stella2014", dashed)
|
||||||
|
self.assertIn("gopher2600", dashed)
|
||||||
|
|
||||||
|
def test_the_pack_name_normalizes_the_spelling(self):
|
||||||
|
self.assertEqual(
|
||||||
|
builder._system_display_name("atari_2600"), builder._system_display_name("atari-2600")
|
||||||
|
)
|
||||||
|
self.assertEqual(builder._system_display_name("msxturboR"), builder._system_display_name("msxturbor"))
|
||||||
|
|
||||||
|
|
||||||
|
class StandaloneNeedsAStandaloneBuild(unittest.TestCase):
|
||||||
|
def test_a_custom_pack_refuses_it_for_a_libretro_core(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
Path(tmp, "emulators").mkdir()
|
||||||
|
Path(tmp, "emulators", "handy.yml").write_text(
|
||||||
|
"emulator: Handy\ntype: libretro\nsystems: [atari-lynx]\nfiles:\n - name: lynxboot.img\n"
|
||||||
|
)
|
||||||
|
common._emulator_profiles_cache.clear()
|
||||||
|
self.addCleanup(common._emulator_profiles_cache.clear)
|
||||||
|
db = {"files": {}, "indexes": {"by_md5": {}, "by_crc32": {}, "by_name": {}}}
|
||||||
|
result = builder.generate_md5_pack(
|
||||||
|
[("md5", "d8f1206299c48946e6ec5ef96d014eaa")], db, tmp, tmp,
|
||||||
|
emulator_name="handy", emulators_dir=str(Path(tmp, "emulators")), standalone=True,
|
||||||
|
)
|
||||||
|
self.assertIsNone(result)
|
||||||
|
self.assertEqual([p for p in os.listdir(tmp) if p.endswith(".zip")], [])
|
||||||
|
|
||||||
|
|
||||||
|
class ADeclinedBuildIsAFailure(unittest.TestCase):
|
||||||
|
def test_a_target_that_removes_every_requested_system_exits_nonzero(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
proc = subprocess.run(
|
||||||
|
[sys.executable, "scripts/generate_pack.py", "--platform", "retroarch",
|
||||||
|
"--system", "sony-playstation-2", "--target", "switch", "--offline",
|
||||||
|
"--output-dir", tmp],
|
||||||
|
capture_output=True, text=True, cwd=str(REPO_ROOT), timeout=600, check=False,
|
||||||
|
)
|
||||||
|
self.assertNotEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||||
|
self.assertEqual([p for p in os.listdir(tmp) if p.endswith(".zip")], [])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user