diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 7773d612..b5f7a1a6 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -310,6 +310,43 @@ def _pack_member_groups( return groups +def _narrowings( + source: str, + regions: list[str] | None, + target_name: str | None, + one_per_slot: bool, + required_only: bool, +) -> list[tuple[str, str]]: + """Every dimension that narrows a pack, as (filename tag, plain label). + + The pack name and the pack README are both built from this list, so a new + way of narrowing a pack cannot reach users nameless or unannounced. Order + is the filename order and must not change: it is what existing archives + are called. + """ + applied: list[tuple[str, str]] = [] + if source == "platform": + applied.append(("_Platform", "only what the platform declares")) + elif source == "truth": + applied.append(("_Truth", "only what emulator profiles require")) + if regions: + pretty = ", ".join( + " ".join(w.title() for w in slug.split("-")) for slug in regions + ) + applied.append( + (f"_{region_mod.region_tag(regions)}", f"region priority {pretty}") + ) + if target_name: + applied.append( + (f"_{_target_tag(target_name)}", f"cores available on {target_name}") + ) + if one_per_slot: + applied.append(("_OnePerSlot", "one BIOS per system and region")) + if required_only: + applied.append(("_Required", "required files only")) + return applied + + def _target_tag(target_name: str) -> str: """Filename tag for a hardware target. @@ -935,8 +972,11 @@ def _build_readme( fallback_systems: list[str] | None = None, one_per_slot: bool = False, undecidable_slots: list[str] | None = None, + narrowings: list[tuple[str, str]] | None = None, + system_filter: list[str] | None = None, ) -> str: """Build a personalized step-by-step README for each platform pack.""" + narrowings = narrowings or [] sep = "=" * 50 header = ( f"{sep}\n" @@ -1175,15 +1215,28 @@ def _build_readme( slot_info = "" if one_per_slot: slot_info = ( - "PACK TYPE: One File Per Slot\n\n" " Where a core declares which BIOS it prefers, only that one was\n" " kept for each system and region. Systems whose cores declare no\n" - " order keep all of theirs.\n\n" + " order keep all of theirs.\n" ) if undecidable_slots: slot_info += ( - f" {len(undecidable_slots)} slot(s) had no declared order.\n\n" + f" {len(undecidable_slots)} slot(s) had no declared order.\n" ) + slot_info += "\n" + + narrowed = "" + labels = [label for _tag, label in narrowings] + if system_filter: + labels.append(f"systems {', '.join(system_filter)}") + if labels: + listed = "".join(f" {label}\n" for label in labels) + narrowed = ( + "PACK TYPE: Narrowed\n\n" + " This pack holds fewer files than the full one:\n" + f"{listed}" + "\n The unfiltered pack is the one to use when in doubt.\n\n" + ) credits = "" if contributors: @@ -1194,7 +1247,8 @@ def _build_readme( credits += "\n" return ( - header + source_info + region_info + slot_info + guide + credits + footer + header + narrowed + source_info + region_info + slot_info + + guide + credits + footer ) @@ -1254,11 +1308,10 @@ 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 "" - source_tag = {"platform": "_Platform", "truth": "_Truth"}.get(source, "") - region_tag_str = f"_{region_mod.region_tag(regions)}" if regions else "" - target_tag = f"_{_target_tag(target_name)}" if target_name else "" - slot_tag = "_OnePerSlot" if one_per_slot else "" + narrowings = _narrowings( + source, regions, target_name, one_per_slot, required_only + ) + narrow_tags = "".join(tag for tag, _label in narrowings) sys_tag = "" if system_filter: @@ -1273,7 +1326,7 @@ def generate_pack( 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}{source_tag}{region_tag_str}{target_tag}{slot_tag}{req_tag}_BIOS_Pack{sys_tag}.zip" + zip_name = f"{platform_display.replace(' ', '_')}{version_tag}{narrow_tags}_BIOS_Pack{sys_tag}.zip" zip_path = os.path.join(output_dir, zip_name) os.makedirs(output_dir, exist_ok=True) @@ -1903,6 +1956,8 @@ def generate_pack( fallback_systems=region_fallbacks, one_per_slot=one_per_slot, undecidable_slots=slot_undecidable, + narrowings=narrowings, + system_filter=system_filter, ) _write_generated_member(zf, "README.txt", readme_text) @@ -2545,11 +2600,14 @@ def generate_split_packs( 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 "" - rgn_tag = f"_{region_mod.region_tag(regions)}" if regions else "" - tgt_tag = f"_{_target_tag(target_name)}" if target_name else "" + narrow_tags = "".join( + tag + for tag, _label in _narrowings( + source, regions, target_name, one_per_slot, required_only + ) + ) safe_group = group_name.replace(" ", "_") - new_name = f"{platform_display.replace(' ', '_')}{ver_tag}{source_tag}{rgn_tag}{tgt_tag}{req_tag}_{safe_group}_BIOS_Pack.zip" + new_name = f"{platform_display.replace(' ', '_')}{ver_tag}{narrow_tags}_{safe_group}_BIOS_Pack.zip" new_path = os.path.join(split_dir, new_name) if new_path != zip_path: os.rename(zip_path, new_path) diff --git a/tests/test_e2e.py b/tests/test_e2e.py index ab435822..2935f0c5 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -5685,6 +5685,52 @@ struct BurnDriver BurnDrvneogeo = { ) self.assertTrue(checked.issubset(self._names_in(out))) + def test_every_narrowing_names_itself_and_says_so(self): + """A dimension that removes files must show in the pack name and in the + README. Both are built from _narrowings, so this holds for any new one.""" + from generate_pack import _build_readme, _narrowings + + dimensions = [ + {"source": "platform"}, + {"source": "truth"}, + {"regions": ["north-america"]}, + {"target_name": "switch"}, + {"one_per_slot": True}, + {"required_only": True}, + ] + for kwargs in dimensions: + applied = _narrowings( + kwargs.get("source", "full"), + kwargs.get("regions"), + kwargs.get("target_name"), + kwargs.get("one_per_slot", False), + kwargs.get("required_only", False), + ) + self.assertEqual(len(applied), 1, kwargs) + tag, label = applied[0] + self.assertTrue(tag.startswith("_") and len(tag) > 1, kwargs) + self.assertTrue(label.strip(), kwargs) + readme = _build_readme("p", "P", "", 1, 1, narrowings=applied) + self.assertIn("PACK TYPE: Narrowed", readme) + self.assertIn(label, readme) + + def test_a_full_pack_is_not_announced_as_narrowed(self): + from generate_pack import _build_readme, _narrowings + + self.assertEqual(_narrowings("full", None, None, False, False), []) + self.assertNotIn( + "PACK TYPE: Narrowed", _build_readme("p", "P", "", 1, 1) + ) + + def test_system_filter_is_announced_too(self): + from generate_pack import _build_readme + + readme = _build_readme( + "p", "P", "", 1, 1, system_filter=["sony-playstation"] + ) + self.assertIn("PACK TYPE: Narrowed", readme) + self.assertIn("sony-playstation", readme) + def test_extras_group_under_their_system_not_a_shared_bucket(self): """find_undeclared_files reports the display name, the profiles are keyed by slug; a key-only lookup put nearly every core extra in one diff --git a/wiki/advanced-usage.md b/wiki/advanced-usage.md index 4f4aa95d..f7b873c3 100644 --- a/wiki/advanced-usage.md +++ b/wiki/advanced-usage.md @@ -157,6 +157,14 @@ overwrites the full one, and the pack README says the pack was narrowed. selects by hash. `pipeline.py` never passes it, so the released packs stay complete. +Every dimension that removes files appears in the output filename and in the +pack README, because both are built from the same list (`_narrowings` in +`generate_pack.py`). A pack narrowed three ways is called +`Recalbox_10.0.8_NorthAmerica_OnePerSlot_Required_BIOS_Pack.zip` and opens with +a `PACK TYPE: Narrowed` block listing the three. Adding a new way to narrow a +pack means adding it to that list, so it cannot reach users nameless or +unannounced. + Region and target both appear in the output filename (`RetroArch_v1.22.2_NorthAmerica_Switch_BIOS_Pack.zip`, `install/retroarch_northamerica_switch.json`), so a filtered build can never