fix: name and announce every narrowed pack

This commit is contained in:
Abdessamad Derraz committed 2026-08-11 17:54:28 +02:00
1 parent bdf6bf6da0
commit 36a284b93b
4 files changed
+138 -26

No files matched your search

+44 -24
View File
@@ -316,6 +316,7 @@ def _narrowings(
target_name: str | None, target_name: str | None,
one_per_slot: bool, one_per_slot: bool,
required_only: bool, required_only: bool,
standalone: bool = False,
) -> list[tuple[str, str]]: ) -> list[tuple[str, str]]:
"""Every dimension that narrows a pack, as (filename tag, plain label). """Every dimension that narrows a pack, as (filename tag, plain label).
@@ -342,6 +343,8 @@ def _narrowings(
) )
if one_per_slot: if one_per_slot:
applied.append(("_OnePerSlot", "one BIOS per system and region")) applied.append(("_OnePerSlot", "one BIOS per system and region"))
if standalone:
applied.append(("_Standalone", "standalone mode files only"))
if required_only: if required_only:
applied.append(("_Required", "required files only")) applied.append(("_Required", "required files only"))
return applied return applied
@@ -2168,8 +2171,13 @@ def generate_emulator_pack(
# ZIP naming # ZIP naming
display_names = [p.get("emulator", n).replace(" ", "") for n, p in selected] display_names = [p.get("emulator", n).replace(" ", "") for n, p in selected]
region_tag_str = f"_{region_mod.region_tag(regions)}" if regions else "" narrow_tags = "".join(
zip_name = "_".join(display_names) + f"{region_tag_str}_BIOS_Pack.zip" tag
for tag, _label in _narrowings(
"full", regions, None, False, required_only, standalone
)
)
zip_name = "_".join(display_names) + f"{narrow_tags}_BIOS_Pack.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)
@@ -2532,8 +2540,17 @@ def generate_split_packs(
"""Generate split packs (one ZIP per system or manufacturer).""" """Generate split packs (one ZIP per system or manufacturer)."""
config = load_platform_config(platform_name, platforms_dir) config = load_platform_config(platform_name, platforms_dir)
platform_display = config.get("platform", platform_name) platform_display = config.get("platform", platform_name)
source_tag = {"platform": "_Platform", "truth": "_Truth"}.get(source, "") # The split directory carries the same tags as the packs inside it, or two
split_dir = os.path.join(output_dir, f"{platform_display.replace(' ', '_')}{source_tag}_Split") # differently narrowed runs write into one another.
split_tags = "".join(
tag
for tag, _label in _narrowings(
source, regions, target_name, one_per_slot, required_only
)
)
split_dir = os.path.join(
output_dir, f"{platform_display.replace(' ', '_')}{split_tags}_Split"
)
os.makedirs(split_dir, exist_ok=True) os.makedirs(split_dir, exist_ok=True)
systems = config.get("systems", {}) systems = config.get("systems", {})
@@ -2929,19 +2946,19 @@ def _run_manifest_mode(
target_name=args.target, target_name=args.target,
offline=args.offline, offline=args.offline,
) )
source_suffix = {"platform": "_platform", "truth": "_truth"}.get(source, "") narrow_suffix = "".join(
req_suffix = "_required" if required_only else "" tag.lower()
rgn = getattr(args, "regions", None) for tag, _label in _narrowings(
region_suffix = ( source,
f"_{region_mod.region_tag(rgn).lower()}" if rgn else "" getattr(args, "regions", None),
) args.target,
target_suffix = ( False,
f"_{_target_tag(args.target).lower()}" if args.target else "" required_only,
)
) )
out_path = os.path.join( out_path = os.path.join(
args.output_dir, args.output_dir,
f"{representative}{source_suffix}{region_suffix}" f"{representative}{narrow_suffix}.json",
f"{target_suffix}{req_suffix}.json",
) )
_write_manifest_if_changed(out_path, manifest) _write_manifest_if_changed(out_path, manifest)
print( print(
@@ -2954,8 +2971,7 @@ def _run_manifest_mode(
if alias_plat != representative: if alias_plat != representative:
alias_path = os.path.join( alias_path = os.path.join(
args.output_dir, args.output_dir,
f"{alias_plat}{source_suffix}{region_suffix}" f"{alias_plat}{narrow_suffix}.json",
f"{target_suffix}{req_suffix}.json",
) )
alias_manifest = dict(manifest) alias_manifest = dict(manifest)
alias_manifest["platform"] = alias_plat alias_manifest["platform"] = alias_plat
@@ -3141,17 +3157,19 @@ def _run_platform_packs(
load_platform_config(p, args.platforms_dir).get("platform", p) load_platform_config(p, args.platforms_dir).get("platform", p)
for p in group_platforms for p in group_platforms
] ]
source_tag = {"platform": "_Platform", "truth": "_Truth"}.get(source, "") narrow_tags = "".join(
region_values = getattr(args, "regions", None) tag
rgn_tag = ( for tag, _label in _narrowings(
f"_{region_mod.region_tag(region_values)}" source,
if region_values getattr(args, "regions", None),
else "" args.target,
args.one_per_slot,
required_only,
)
) )
req_tag = "_Required" if required_only else ""
combined = ( combined = (
"_".join(n.replace(" ", "") for n in all_names) "_".join(n.replace(" ", "") for n in all_names)
+ f"{ver_tag}{source_tag}{rgn_tag}{req_tag}_BIOS_Pack.zip" + f"{ver_tag}{narrow_tags}_BIOS_Pack.zip"
) )
new_path = os.path.join(os.path.dirname(zip_path), combined) new_path = os.path.join(os.path.dirname(zip_path), combined)
if new_path != zip_path: if new_path != zip_path:
@@ -3301,6 +3319,8 @@ def main():
parser.error(str(exc)) parser.error(str(exc))
if args.manifest_targets: if args.manifest_targets:
parser.error("--region is incompatible with --manifest-targets") parser.error("--region is incompatible with --manifest-targets")
if args.one_per_slot and args.manifest_targets:
parser.error("--one-per-slot is incompatible with --manifest-targets")
# Quick-exit modes: --verify-packs alone = verify existing packs only # Quick-exit modes: --verify-packs alone = verify existing packs only
# Combined with --all-variants, generation runs first then verify # Combined with --all-variants, generation runs first then verify
+60
View File
@@ -5714,6 +5714,66 @@ struct BurnDriver BurnDrvneogeo = {
self.assertIn("PACK TYPE: Narrowed", readme) self.assertIn("PACK TYPE: Narrowed", readme)
self.assertIn(label, readme) self.assertIn(label, readme)
def test_no_mode_swallows_a_narrowing_flag(self):
"""Every mode either honours a narrowing flag or refuses it.
Silently ignoring one is how --manifest-targets accepted --region and
wrote seven unfiltered manifests, and how --manifest accepted
--one-per-slot. A mode that cannot apply a flag must say so.
"""
import subprocess
repo = os.path.join(os.path.dirname(__file__), "..")
matrix = [
(["--manifest-targets"], ["--region", "us"], "refuse"),
(["--manifest-targets"], ["--one-per-slot"], "refuse"),
(["--platform", "retroarch", "--manifest"], ["--one-per-slot"], "refuse"),
(["--emulator", "duckstation"], ["--one-per-slot"], "refuse"),
(["--system", "sony-playstation"], ["--one-per-slot"], "refuse"),
(["--platform", "retroarch", "--from-md5", "d8f1"], ["--region", "us"],
"refuse"),
(["--platform", "retroarch", "--from-md5", "d8f1"], ["--one-per-slot"],
"refuse"),
]
for mode, flag, expected in matrix:
with self.subTest(mode=mode, flag=flag):
result = subprocess.run(
[sys.executable, "scripts/generate_pack.py", *mode, *flag,
"--output-dir", os.path.join(self.root, "modecheck")],
capture_output=True, text=True, cwd=repo, timeout=120,
)
combined = result.stdout + result.stderr
self.assertNotEqual(
result.returncode, 0,
f"{' '.join(mode + flag)} was accepted and ignored:\n{combined}",
)
self.assertIn("error:", combined)
def test_standalone_mode_names_itself(self):
"""--emulator --standalone ships a different file set; without a tag it
overwrote the pack built without it."""
from generate_pack import _narrowings
applied = _narrowings("full", None, None, False, False, standalone=True)
self.assertEqual([tag for tag, _l in applied], ["_Standalone"])
def test_every_name_builder_uses_the_same_list(self):
"""Four places name a pack or a manifest. None may assemble tags by
hand: that is how --one-per-slot reached --split unnamed."""
import inspect
import generate_pack
source = inspect.getsource(generate_pack)
for hand_rolled in (
'req_tag = "_Required"',
'source_tag = {"platform": "_Platform"',
'req_suffix = "_required"',
'slot_tag = "_OnePerSlot"',
):
self.assertNotIn(hand_rolled, source, hand_rolled)
self.assertGreaterEqual(source.count("_narrowings("), 5)
def test_a_full_pack_is_not_announced_as_narrowed(self): def test_a_full_pack_is_not_announced_as_narrowed(self):
from generate_pack import _build_readme, _narrowings from generate_pack import _build_readme, _narrowings
+31
View File
@@ -419,5 +419,36 @@ class TestSchemaMatchesModule(unittest.TestCase):
self.assertEqual(set(node["items"]["enum"]), set(region.REGIONS)) self.assertEqual(set(node["items"]["enum"]), set(region.REGIONS))
class TestVerifyModesHonourRegion(unittest.TestCase):
"""verify.py must apply --region in every mode, or refuse it.
It silently ignored the flag in --emulator and --system mode: the report
was identical with and without, so a filtered pack could not be checked.
"""
def _run(self, *args: str) -> str:
import subprocess
repo = os.path.join(os.path.dirname(__file__), "..")
return subprocess.run(
[sys.executable, "scripts/verify.py", *args],
capture_output=True, text=True, cwd=repo, timeout=900,
).stdout
def test_emulator_mode_narrows(self):
plain = self._run("--emulator", "duckstation")
filtered = self._run("--emulator", "duckstation", "--region", "us")
if "duckstation" not in plain:
self.skipTest("duckstation profile not present")
self.assertNotEqual(plain, filtered)
def test_system_mode_narrows(self):
plain = self._run("--system", "sony-playstation")
filtered = self._run("--system", "sony-playstation", "--region", "us")
if not plain.strip():
self.skipTest("system not present")
self.assertNotEqual(plain, filtered)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+3 -2
View File
@@ -158,8 +158,9 @@ 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 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 pack README, because the five places that name an artefact (the pack, the
`generate_pack.py`). A pack narrowed three ways is called `--split` rename, the `_Split` directory, the grouped-alias rename and the
manifest) all build from one 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 `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 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 pack means adding it to that list, so it cannot reach users nameless or