diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index fb9b5dbe..791d0940 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -891,6 +891,67 @@ def list_platforms(platforms_dir: str) -> list[str]: return list_registered_platforms(platforms_dir, include_archived=True) +def _system_display_name(system_id: str) -> str: + """Convert system ID to display name for ZIP naming.""" + s = system_id.lower().replace("_", "-") + for prefix in MANUFACTURER_PREFIXES: + if s.startswith(prefix): + s = s[len(prefix):] + break + parts = s.split("-") + return "_".join(p.title() for p in parts if p) + + +def generate_split_packs( + platform_name: str, + platforms_dir: str, + db: dict, + bios_dir: str, + output_dir: str, + group_by: str = "system", + emulators_dir: str = "emulators", + zip_contents: dict | None = None, + data_registry: dict | None = None, + emu_profiles: dict | None = None, + target_cores: set[str] | None = None, + required_only: bool = False, +) -> list[str]: + """Generate split packs (one ZIP per system or manufacturer).""" + config = load_platform_config(platform_name, platforms_dir) + platform_display = config.get("platform", platform_name) + split_dir = os.path.join(output_dir, f"{platform_display.replace(' ', '_')}_Split") + os.makedirs(split_dir, exist_ok=True) + + systems = config.get("systems", {}) + + if group_by == "manufacturer": + groups = _group_systems_by_manufacturer(systems, db, bios_dir) + else: + groups = {_system_display_name(sid): [sid] for sid in systems} + + results = [] + for group_name, group_system_ids in sorted(groups.items()): + zip_path = generate_pack( + platform_name, platforms_dir, db, bios_dir, split_dir, + emulators_dir=emulators_dir, zip_contents=zip_contents, + data_registry=data_registry, emu_profiles=emu_profiles, + target_cores=target_cores, required_only=required_only, + system_filter=group_system_ids, + ) + if zip_path: + version = config.get("version", config.get("dat_version", "")) + ver_tag = f"_{version.replace(' ', '')}" if version else "" + req_tag = "_Required" if required_only else "" + new_name = f"{platform_display.replace(' ', '_')}{ver_tag}{req_tag}_{group_name}_BIOS_Pack.zip" + new_path = os.path.join(split_dir, new_name) + if new_path != zip_path: + os.rename(zip_path, new_path) + zip_path = new_path + results.append(zip_path) + + return results + + def main(): parser = argparse.ArgumentParser(description="Generate platform BIOS ZIP packs") parser.add_argument("--platform", "-p", help="Platform name (e.g., retroarch)") @@ -915,6 +976,11 @@ def main(): parser.add_argument("--list", action="store_true", help="List available platforms") parser.add_argument("--required-only", action="store_true", help="Only include required files, skip optional") + parser.add_argument("--split", action="store_true", + help="Generate one ZIP per system/manufacturer") + parser.add_argument("--group-by", choices=["system", "manufacturer"], + default="system", + help="Grouping for --split (default: system)") parser.add_argument("--target", "-t", help="Hardware target (e.g., switch, rpi4)") parser.add_argument("--list-targets", action="store_true", help="List available targets for the platform") args = parser.parse_args() @@ -962,6 +1028,12 @@ def main(): parser.error("Specify --platform, --all, --emulator, or --system") 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.split and not (has_platform or has_all): + parser.error("--split requires --platform or --all") + if args.split and has_emulator: + parser.error("--split is incompatible with --emulator") + if args.group_by != "system" and not args.split: + parser.error("--group-by requires --split") if args.target and not (has_platform or has_all): parser.error("--target requires --platform or --all") if args.target and has_emulator: @@ -1054,15 +1126,25 @@ def main(): try: tc = target_cores_cache.get(representative) if args.target else None - zip_path = generate_pack( - representative, args.platforms_dir, db, args.bios_dir, args.output_dir, - include_extras=args.include_extras, emulators_dir=args.emulators_dir, - 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: + if args.split: + zip_paths = generate_split_packs( + representative, args.platforms_dir, db, args.bios_dir, + args.output_dir, group_by=args.group_by, + emulators_dir=args.emulators_dir, zip_contents=zip_contents, + data_registry=data_registry, emu_profiles=emu_profiles, + target_cores=tc, required_only=args.required_only, + ) + print(f" Split into {len(zip_paths)} packs") + else: + zip_path = generate_pack( + representative, args.platforms_dir, db, args.bios_dir, args.output_dir, + include_extras=args.include_extras, emulators_dir=args.emulators_dir, + 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 not args.split and zip_path and variants: rep_cfg = load_platform_config(representative, args.platforms_dir) ver = rep_cfg.get("version", rep_cfg.get("dat_version", "")) ver_tag = f"_{ver.replace(' ', '')}" if ver else "" diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 37bd6da7..2410af7f 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -1681,5 +1681,90 @@ class TestE2E(unittest.TestCase): self.assertIn("2 files", output) + def test_135_split_by_system(self): + """--split generates one ZIP per system in a subdirectory.""" + import tempfile + with tempfile.TemporaryDirectory() as tmpdir: + plat_dir = os.path.join(tmpdir, "platforms") + os.makedirs(plat_dir) + bios_dir = os.path.join(tmpdir, "bios", "Test") + os.makedirs(os.path.join(bios_dir, "SysA")) + os.makedirs(os.path.join(bios_dir, "SysB")) + emu_dir = os.path.join(tmpdir, "emulators") + os.makedirs(emu_dir) + out_dir = os.path.join(tmpdir, "dist") + + file_a = os.path.join(bios_dir, "SysA", "bios_a.bin") + file_b = os.path.join(bios_dir, "SysB", "bios_b.bin") + with open(file_a, "wb") as f: + f.write(b"system_a") + with open(file_b, "wb") as f: + f.write(b"system_b") + + from common import compute_hashes + ha = compute_hashes(file_a) + hb = compute_hashes(file_b) + + db = { + "files": { + ha["sha1"]: {"name": "bios_a.bin", "md5": ha["md5"], + "sha1": ha["sha1"], "sha256": ha["sha256"], + "path": file_a, + "paths": [file_a]}, + hb["sha1"]: {"name": "bios_b.bin", "md5": hb["md5"], + "sha1": hb["sha1"], "sha256": hb["sha256"], + "path": file_b, + "paths": [file_b]}, + }, + "indexes": { + "by_md5": {ha["md5"]: ha["sha1"], hb["md5"]: hb["sha1"]}, + "by_name": {"bios_a.bin": [ha["sha1"]], "bios_b.bin": [hb["sha1"]]}, + "by_crc32": {}, "by_path_suffix": {}, + }, + } + + registry = {"platforms": {"splitplat": {"status": "active"}}} + with open(os.path.join(plat_dir, "_registry.yml"), "w") as f: + yaml.dump(registry, f) + plat_cfg = { + "platform": "SplitTest", + "verification_mode": "existence", + "systems": { + "test-system-a": {"files": [{"name": "bios_a.bin", "sha1": ha["sha1"]}]}, + "test-system-b": {"files": [{"name": "bios_b.bin", "sha1": hb["sha1"]}]}, + }, + } + with open(os.path.join(plat_dir, "splitplat.yml"), "w") as f: + yaml.dump(plat_cfg, f) + + from generate_pack import generate_split_packs + from common import build_zip_contents_index, load_emulator_profiles + zip_contents = build_zip_contents_index(db) + emu_profiles = load_emulator_profiles(emu_dir) + + zip_paths = generate_split_packs( + "splitplat", plat_dir, db, os.path.join(tmpdir, "bios"), out_dir, + emulators_dir=emu_dir, zip_contents=zip_contents, + emu_profiles=emu_profiles, group_by="system", + ) + self.assertEqual(len(zip_paths), 2) + + # Check subdirectory exists + split_dir = os.path.join(out_dir, "SplitTest_Split") + self.assertTrue(os.path.isdir(split_dir)) + + # Verify each ZIP contains only its system's files + for zp in zip_paths: + with zipfile.ZipFile(zp) as zf: + names = zf.namelist() + basename = os.path.basename(zp) + if "System_A" in basename: + self.assertIn("bios_a.bin", names) + self.assertNotIn("bios_b.bin", names) + elif "System_B" in basename: + self.assertIn("bios_b.bin", names) + self.assertNotIn("bios_a.bin", names) + + if __name__ == "__main__": unittest.main()