diff --git a/scripts/common.py b/scripts/common.py index e3e0d8d1..6a7deb48 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -239,6 +239,50 @@ def list_registered_platforms( return platforms +def _target_overrides(platform_name: str, targets_dir: str) -> dict: + overrides_file = os.path.join(targets_dir, "_overrides.yml") + if not os.path.exists(overrides_file): + return {} + with open(overrides_file) as f: + all_overrides = yaml_load(f) or {} + return all_overrides.get(platform_name, {}).get("targets", {}) + + +def _target_alias_index(targets: dict, overrides: dict) -> dict[str, str]: + """Every name a target answers to, mapped to the name its file uses.""" + alias_index: dict[str, str] = {} + for tname in targets: + alias_index[tname] = tname + for alias in overrides.get(tname, {}).get("aliases", []): + alias_index[alias] = tname + return alias_index + + +def canonical_target_name( + platforms: list[str], target: str, platforms_dir: str = "platforms" +) -> str: + """The name a target is filed under, whichever alias was typed. + + `switch`, `nx` and `nintendo-switch` named three packs of one target, and + --verify-packs looked for a name no build had written. The typed name is + kept when no platform knows it or when platforms file it differently. + """ + targets_dir = os.path.join(platforms_dir, "targets") + found: set[str] = set() + for platform_name in platforms: + target_file = os.path.join(targets_dir, f"{platform_name}.yml") + if not os.path.exists(target_file): + continue + with open(target_file) as f: + targets = (yaml_load(f) or {}).get("targets", {}) + canonical = _target_alias_index( + targets, _target_overrides(platform_name, targets_dir) + ).get(target) + if canonical: + found.add(canonical) + return found.pop() if len(found) == 1 else target + + def load_target_config( platform_name: str, target: str, @@ -260,19 +304,8 @@ def load_target_config( data = yaml_load(f) or {} targets = data.get("targets", {}) - - overrides_file = os.path.join(targets_dir, "_overrides.yml") - overrides = {} - if os.path.exists(overrides_file): - with open(overrides_file) as f: - all_overrides = yaml_load(f) or {} - overrides = all_overrides.get(platform_name, {}).get("targets", {}) - - alias_index: dict[str, str] = {} - for tname in targets: - alias_index[tname] = tname - for alias in overrides.get(tname, {}).get("aliases", []): - alias_index[alias] = tname + overrides = _target_overrides(platform_name, targets_dir) + alias_index = _target_alias_index(targets, overrides) canonical = alias_index.get(target) if canonical is None: diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index f7cc41b3..eb608b36 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -45,6 +45,7 @@ from common import ( group_identical_platforms, list_emulator_profiles, list_platform_system_ids, + canonical_target_name, list_registered_platforms, list_system_ids, load_data_dir_registry, @@ -2766,6 +2767,16 @@ def main(): packresolve.set_offline(bool(args.offline)) _refuse_unapplied_flags(args, parser) + if args.target: + # Named once, by the name the target is filed under: every artifact + # name below derives from it. + args.target = canonical_target_name( + [args.platform] + if args.platform + else list_registered_platforms(args.platforms_dir, include_archived=True), + args.target, + args.platforms_dir, + ) # Quick-exit modes: --verify-packs alone = verify existing packs only # Combined with --all-variants, generation runs first then verify diff --git a/tests/test_target_names.py b/tests/test_target_names.py new file mode 100644 index 00000000..e0804947 --- /dev/null +++ b/tests/test_target_names.py @@ -0,0 +1,58 @@ +"""A hardware target names its artifacts by the name it is filed under. + +`switch`, `nx` and `nintendo-switch` are one RetroArch target. The tag came +from the name as typed, so one target gave three pack and manifest names, +and `--verify-packs --target nx` looked for a pack nobody had built. +""" + +from __future__ import annotations + +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +from common import canonical_target_name # noqa: E402 + + +class AliasesResolveToOneName(unittest.TestCase): + def test_retroarch_switch_aliases(self): + platforms = str(REPO_ROOT / "platforms") + for typed in ("switch", "nx", "nintendo-switch"): + with self.subTest(typed=typed): + self.assertEqual( + canonical_target_name(["retroarch"], typed, platforms), + "nintendo-switch", + ) + + def test_an_unknown_name_is_left_for_the_target_check(self): + self.assertEqual( + canonical_target_name(["retroarch"], "bogus", str(REPO_ROOT / "platforms")), + "bogus", + ) + + +class TheBuilderNamesByTheCanonicalTarget(unittest.TestCase): + def test_an_alias_writes_the_canonical_manifest(self): + if not (REPO_ROOT / "database.json").exists(): + self.skipTest("database.json is not built") + (REPO_ROOT / "tmp").mkdir(exist_ok=True) + with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as out: + proc = subprocess.run( + [sys.executable, "scripts/generate_pack.py", "--platform", "retroarch", + "--target", "nx", "--manifest", "--offline", "--output-dir", out], + cwd=REPO_ROOT, capture_output=True, text=True, timeout=600, check=False, + ) + self.assertEqual(proc.returncode, 0, proc.stderr) + self.assertEqual( + sorted(p.name for p in Path(out).glob("*.json")), + ["retroarch_nintendoswitch.json"], + ) + + +if __name__ == "__main__": + unittest.main()