From 43cb7a9884e3d0893318209b759f70fbd4804586 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:36:51 +0100 Subject: [PATCH] feat: allow --platform + --system combination --- scripts/common.py | 11 ++++++ scripts/generate_pack.py | 78 +++++++++++++++++++++++++++++++-------- tests/test_e2e.py | 79 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 15 deletions(-) diff --git a/scripts/common.py b/scripts/common.py index b5dbd3cb..229bb1b8 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -1068,3 +1068,14 @@ def list_system_ids(emulators_dir: str) -> None: for sys_id in sorted(system_emus): count = len(system_emus[sys_id]) print(f" {sys_id:35s} ({count} emulator{'s' if count > 1 else ''})") + + +def list_platform_system_ids(platform_name: str, platforms_dir: str) -> None: + """Print system IDs from a platform's YAML config.""" + config = load_platform_config(platform_name, platforms_dir) + systems = config.get("systems", {}) + for sys_id in sorted(systems): + file_count = len(systems[sys_id].get("files", [])) + mfr = systems[sys_id].get("manufacturer", "") + mfr_display = f" [{mfr.split('|')[0]}]" if mfr else "" + print(f" {sys_id:35s} ({file_count} file{'s' if file_count != 1 else ''}){mfr_display}") diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 5a0e0079..2acd2c90 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -27,7 +27,8 @@ sys.path.insert(0, os.path.dirname(__file__)) from common import ( _build_validation_index, build_zip_contents_index, check_file_validation, check_inside_zip, compute_hashes, fetch_large_file, filter_files_by_mode, - group_identical_platforms, list_emulator_profiles, list_registered_platforms, + group_identical_platforms, list_emulator_profiles, list_platform_system_ids, + list_registered_platforms, filter_systems_by_target, list_system_ids, load_database, load_data_dir_registry, load_emulator_profiles, load_platform_config, md5_composite, resolve_local_file, @@ -232,6 +233,7 @@ def generate_pack( emu_profiles: dict | None = None, target_cores: set[str] | None = None, required_only: bool = False, + system_filter: list[str] | None = None, ) -> str | None: """Generate a ZIP pack for a platform. @@ -248,7 +250,25 @@ def generate_pack( version = config.get("version", config.get("dat_version", "")) version_tag = f"_{version.replace(' ', '')}" if version else "" req_tag = "_Required" if required_only else "" - zip_name = f"{platform_display.replace(' ', '_')}{version_tag}{req_tag}_BIOS_Pack.zip" + + sys_tag = "" + if system_filter: + display_parts = [] + for sid in system_filter: + s = sid.lower().replace("_", "-") + for prefix in ("microsoft-", "nintendo-", "sony-", "sega-", "snk-", + "panasonic-", "nec-", "epoch-", "mattel-", "fairchild-", + "hartung-", "tiger-", "magnavox-", "philips-", "bandai-", + "casio-", "coleco-", "commodore-", "sharp-", "sinclair-", + "atari-"): + if s.startswith(prefix): + s = s[len(prefix):] + break + parts = s.split("-") + display_parts.append("_".join(p.title() for p in parts if p)) + sys_tag = "_" + "_".join(display_parts) + + zip_name = f"{platform_display.replace(' ', '_')}{version_tag}{req_tag}_BIOS_Pack{sys_tag}.zip" zip_path = os.path.join(output_dir, zip_name) os.makedirs(output_dir, exist_ok=True) @@ -281,6 +301,18 @@ def generate_pack( platform_cores=plat_cores, ) + if system_filter: + from common import _norm_system_id + norm_filter = {_norm_system_id(s) for s in system_filter} + filtered = {sid: sys_data for sid, sys_data in pack_systems.items() + if sid in system_filter or _norm_system_id(sid) in norm_filter} + if not filtered: + available = sorted(pack_systems.keys())[:10] + print(f" WARNING: no systems matched filter {system_filter} " + f"(available: {', '.join(available)})") + return None + pack_systems = filtered + with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for sys_id, system in sorted(pack_systems.items()): for file_entry in system.get("files", []): @@ -899,7 +931,10 @@ def main(): list_emulator_profiles(args.emulators_dir) return if args.list_systems: - list_system_ids(args.emulators_dir) + if args.platform: + list_platform_system_ids(args.platform, args.platforms_dir) + else: + list_system_ids(args.emulators_dir) return if args.list_targets: if not args.platform: @@ -914,18 +949,26 @@ def main(): print(f" {t['name']:30s} {t['architecture']:10s} {t['core_count']:>4d} cores{aliases}") return - # Mutual exclusion - modes = sum(1 for x in (args.platform, args.all, args.emulator, args.system) if x) - if modes == 0: + # Mode validation + has_platform = bool(args.platform) + has_all = args.all + has_emulator = bool(args.emulator) + has_system = bool(args.system) + + # --platform/--all and --system can combine (system filters within platform) + # --emulator is exclusive with everything else + if has_emulator and (has_platform or has_all or has_system): + parser.error("--emulator is mutually exclusive with --platform, --all, and --system") + if has_platform and has_all: + parser.error("--platform and --all are mutually exclusive") + if not (has_platform or has_all or has_emulator or has_system): parser.error("Specify --platform, --all, --emulator, or --system") - if modes > 1: - parser.error("--platform, --all, --emulator, and --system are mutually exclusive") - if args.standalone and not (args.emulator or args.system): - parser.error("--standalone requires --emulator or --system") - if args.target and not (args.platform or args.all): + if args.standalone and not (has_emulator or (has_system and not has_platform and not has_all)): + parser.error("--standalone requires --emulator or --system (without --platform)") + if args.target and not (has_platform or has_all): parser.error("--target requires --platform or --all") - if args.target and (args.emulator or args.system): - parser.error("--target is incompatible with --emulator and --system") + if args.target and has_emulator: + parser.error("--target is incompatible with --emulator") db = load_database(args.db) zip_contents = build_zip_contents_index(db) @@ -941,8 +984,8 @@ def main(): sys.exit(1) return - # System mode - if args.system: + # System mode (standalone, without platform context) + if has_system and not has_platform and not has_all: system_ids = [s.strip() for s in args.system.split(",") if s.strip()] result = generate_system_pack( system_ids, args.emulators_dir, db, args.bios_dir, args.output_dir, @@ -952,6 +995,10 @@ def main(): sys.exit(1) return + system_filter = None + if args.system: + system_filter = [s.strip() for s in args.system.split(",") if s.strip()] + # Platform mode (existing) if args.all: platforms = list_registered_platforms( @@ -1016,6 +1063,7 @@ def main(): zip_contents=zip_contents, data_registry=data_registry, emu_profiles=emu_profiles, target_cores=tc, required_only=args.required_only, + system_filter=system_filter, ) if zip_path and variants: rep_cfg = load_platform_config(representative, args.platforms_dir) diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 97eb57b6..37bd6da7 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -1602,5 +1602,84 @@ class TestE2E(unittest.TestCase): self.assertTrue(any("present_req.bin" in n for n in names)) + def test_132_platform_system_filter(self): + """--platform + --system filters systems within a platform pack.""" + from generate_pack import generate_pack + output_dir = os.path.join(self.root, "pack_sysfilter") + os.makedirs(output_dir, exist_ok=True) + config = { + "platform": "SysFilterTest", + "verification_mode": "existence", + "base_destination": "system", + "systems": { + "system-a": { + "files": [ + {"name": "present_req.bin", "destination": "present_req.bin"}, + ], + }, + "system-b": { + "files": [ + {"name": "present_opt.bin", "destination": "present_opt.bin"}, + ], + }, + }, + } + with open(os.path.join(self.platforms_dir, "test_sysfilter.yml"), "w") as fh: + yaml.dump(config, fh) + zip_path = generate_pack( + "test_sysfilter", self.platforms_dir, self.db, self.bios_dir, output_dir, + system_filter=["system-a"], + ) + self.assertIsNotNone(zip_path) + with zipfile.ZipFile(zip_path) as zf: + names = zf.namelist() + self.assertTrue(any("present_req.bin" in n for n in names)) + self.assertFalse(any("present_opt.bin" in n for n in names)) + + def test_133_platform_system_filter_normalized(self): + """_norm_system_id normalization matches with manufacturer prefix.""" + from common import _norm_system_id + self.assertEqual( + _norm_system_id("sony-playstation"), + _norm_system_id("playstation"), + ) + + def test_134_list_systems_platform_context(self): + """list_platform_system_ids lists systems from a platform YAML.""" + from common import list_platform_system_ids + import io + config = { + "platform": "ListSysTest", + "verification_mode": "existence", + "systems": { + "alpha-sys": { + "files": [ + {"name": "a.bin", "destination": "a.bin"}, + ], + }, + "beta-sys": { + "files": [ + {"name": "b1.bin", "destination": "b1.bin"}, + {"name": "b2.bin", "destination": "b2.bin"}, + ], + }, + }, + } + with open(os.path.join(self.platforms_dir, "test_listsys.yml"), "w") as fh: + yaml.dump(config, fh) + captured = io.StringIO() + old_stdout = sys.stdout + sys.stdout = captured + try: + list_platform_system_ids("test_listsys", self.platforms_dir) + finally: + sys.stdout = old_stdout + output = captured.getvalue() + self.assertIn("alpha-sys", output) + self.assertIn("beta-sys", output) + self.assertIn("1 file", output) + self.assertIn("2 files", output) + + if __name__ == "__main__": unittest.main()