feat: name every narrowing in the pack filename

This commit is contained in:
Abdessamad Derraz committed 2026-08-11 16:56:20 +02:00
1 parent 5b0e6836de
commit 4bd277a7be
3 files changed
+126 -14

No files matched your search

+72 -14
View File
@@ -310,6 +310,43 @@ def _pack_member_groups(
return 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: def _target_tag(target_name: str) -> str:
"""Filename tag for a hardware target. """Filename tag for a hardware target.
@@ -935,8 +972,11 @@ def _build_readme(
fallback_systems: list[str] | None = None, fallback_systems: list[str] | None = None,
one_per_slot: bool = False, one_per_slot: bool = False,
undecidable_slots: list[str] | None = None, undecidable_slots: list[str] | None = None,
narrowings: list[tuple[str, str]] | None = None,
system_filter: list[str] | None = None,
) -> str: ) -> str:
"""Build a personalized step-by-step README for each platform pack.""" """Build a personalized step-by-step README for each platform pack."""
narrowings = narrowings or []
sep = "=" * 50 sep = "=" * 50
header = ( header = (
f"{sep}\n" f"{sep}\n"
@@ -1175,15 +1215,28 @@ def _build_readme(
slot_info = "" slot_info = ""
if one_per_slot: if one_per_slot:
slot_info = ( slot_info = (
"PACK TYPE: One File Per Slot\n\n"
" Where a core declares which BIOS it prefers, only that one was\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" " 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: if undecidable_slots:
slot_info += ( 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 = "" credits = ""
if contributors: if contributors:
@@ -1194,7 +1247,8 @@ def _build_readme(
credits += "\n" credits += "\n"
return ( 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 = config.get("version", config.get("dat_version", ""))
version_tag = f"_{version.replace(' ', '')}" if version else "" version_tag = f"_{version.replace(' ', '')}" if version else ""
req_tag = "_Required" if required_only else "" narrowings = _narrowings(
source_tag = {"platform": "_Platform", "truth": "_Truth"}.get(source, "") source, regions, target_name, one_per_slot, required_only
region_tag_str = f"_{region_mod.region_tag(regions)}" if regions else "" )
target_tag = f"_{_target_tag(target_name)}" if target_name else "" narrow_tags = "".join(tag for tag, _label in narrowings)
slot_tag = "_OnePerSlot" if one_per_slot else ""
sys_tag = "" sys_tag = ""
if system_filter: if system_filter:
@@ -1273,7 +1326,7 @@ def generate_pack(
display_parts.append("_".join(p.title() for p in parts if p)) display_parts.append("_".join(p.title() for p in parts if p))
sys_tag = "_" + "_".join(display_parts) 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) zip_path = os.path.join(output_dir, zip_name)
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
@@ -1903,6 +1956,8 @@ def generate_pack(
fallback_systems=region_fallbacks, fallback_systems=region_fallbacks,
one_per_slot=one_per_slot, one_per_slot=one_per_slot,
undecidable_slots=slot_undecidable, undecidable_slots=slot_undecidable,
narrowings=narrowings,
system_filter=system_filter,
) )
_write_generated_member(zf, "README.txt", readme_text) _write_generated_member(zf, "README.txt", readme_text)
@@ -2545,11 +2600,14 @@ def generate_split_packs(
if zip_path: if zip_path:
version = config.get("version", config.get("dat_version", "")) version = config.get("version", config.get("dat_version", ""))
ver_tag = f"_{version.replace(' ', '')}" if version else "" ver_tag = f"_{version.replace(' ', '')}" if version else ""
req_tag = "_Required" if required_only else "" narrow_tags = "".join(
rgn_tag = f"_{region_mod.region_tag(regions)}" if regions else "" tag
tgt_tag = f"_{_target_tag(target_name)}" if target_name else "" for tag, _label in _narrowings(
source, regions, target_name, one_per_slot, required_only
)
)
safe_group = group_name.replace(" ", "_") 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) new_path = os.path.join(split_dir, new_name)
if new_path != zip_path: if new_path != zip_path:
os.rename(zip_path, new_path) os.rename(zip_path, new_path)
+46
View File
@@ -5685,6 +5685,52 @@ struct BurnDriver BurnDrvneogeo = {
) )
self.assertTrue(checked.issubset(self._names_in(out))) 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): def test_extras_group_under_their_system_not_a_shared_bucket(self):
"""find_undeclared_files reports the display name, the profiles are """find_undeclared_files reports the display name, the profiles are
keyed by slug; a key-only lookup put nearly every core extra in one keyed by slug; a key-only lookup put nearly every core extra in one
+8
View File
@@ -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 selects by hash. `pipeline.py` never passes it, so the released packs stay
complete. 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 Region and target both appear in the output filename
(`RetroArch_v1.22.2_NorthAmerica_Switch_BIOS_Pack.zip`, (`RetroArch_v1.22.2_NorthAmerica_Switch_BIOS_Pack.zip`,
`install/retroarch_northamerica_switch.json`), so a filtered build can never `install/retroarch_northamerica_switch.json`), so a filtered build can never