diff --git a/scripts/common.py b/scripts/common.py index 53376652..40e5bad1 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -275,19 +275,24 @@ def load_target_config( raise ValueError(msg) cores = set(str(c) for c in targets[canonical].get("cores", [])) + return apply_target_overrides(cores, overrides, canonical) - default_ovr = overrides.get("_default", {}) - ovr = overrides.get(canonical, {}) - for c in default_ovr.get("add_cores", []): - cores.add(str(c)) - for c in default_ovr.get("remove_cores", []): - cores.discard(str(c)) - for c in ovr.get("add_cores", []): - cores.add(str(c)) - for c in ovr.get("remove_cores", []): - cores.discard(str(c)) - return cores +def apply_target_overrides( + cores: set[str], overrides: dict, target: str +) -> set[str]: + """Cores of one target once the platform's overrides are applied. + + ``_default`` applies to every target of the platform, then the target's + own entry: a name the scraper reads as a core on every board (a machine + variant, a launcher) is written once instead of once per board. + """ + result = set(cores) + for scope in ("_default", target): + entry = overrides.get(scope) or {} + result |= {str(c) for c in entry.get("add_cores") or []} + result -= {str(c) for c in entry.get("remove_cores") or []} + return result def list_available_targets( diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index c61717ca..9530c2a9 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -29,6 +29,7 @@ from pathlib import Path sys.path.insert(0, os.path.dirname(__file__)) from common import ( + apply_target_overrides, artifact_lock, ArtifactLockBusy, build_target_cores_cache, @@ -1896,12 +1897,14 @@ def generate_target_manifests(targets_dir: str, output_dir: str) -> None: # file instead: the user asked for a filter and got the full pack. overrides_path = targets_path / "_overrides.yml" alias_map: dict[str, list[str]] = {} + platform_overrides: dict = {} if overrides_path.is_file(): with open(overrides_path) as f: all_overrides = yaml_load(f) or {} - for tname, ovr in ( + platform_overrides = ( all_overrides.get(yml_file.stem, {}).get("targets", {}) or {} - ).items(): + ) + for tname, ovr in platform_overrides.items(): names = [a for a in (ovr or {}).get("aliases", []) if isinstance(a, str)] if names: alias_map[tname] = names @@ -1925,7 +1928,12 @@ def generate_target_manifests(targets_dir: str, output_dir: str) -> None: f"{yml_file}: target {target_name!r} cores must be unique " "non-empty strings" ) - result[target_name] = cores + # The same add/remove lists the pack builder applies: a list + # published without them makes the installer skip files of a + # core the pack for that target ships. + result[target_name] = sorted( + apply_target_overrides(set(cores), platform_overrides, target_name) + ) for target_name, names in alias_map.items(): if target_name not in result: diff --git a/tests/test_check_freshness.py b/tests/test_check_freshness.py index 05e7c331..e017cf74 100644 --- a/tests/test_check_freshness.py +++ b/tests/test_check_freshness.py @@ -166,6 +166,28 @@ class CoreResolutionTests(unittest.TestCase): {"ghost": ["android", "linux"]}, ) + def test_a_default_override_drops_a_name_on_every_target(self): + index = cf.profile_name_index(self.PROFILES) + targets = { + "targets": { + "x86_64": {"cores": ["A500", "ghost"]}, + "rpi": {"cores": ["A500", "na"]}, + } + } + overrides = { + "plat": { + "targets": { + "_default": {"remove_cores": ["A500"]}, + "rpi": {"remove_cores": ["na"]}, + } + } + } + removed = cf._removed_cores(overrides, "plat") + self.assertEqual( + cf.unresolved_target_cores(targets, index, removed), + {"ghost": ["x86_64"]}, + ) + def test_coreinfo_gaps_fold_case_and_flag_standalone_profiles(self): names = ["FreeIntvTSOverlay", "eka2l1", "wqxemu", "mednafen_psx"] unprofiled, standalone = cf.coreinfo_gaps(names, self.PROFILES) diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 0e469a2a..6edd9a72 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -1747,6 +1747,33 @@ class TestE2E(unittest.TestCase): self.assertIn("core_a", cores) self.assertIn("core_b", cores) + def test_target_default_override_reaches_every_target(self): + """`_default` drops a name on every target, then the target adds its own.""" + self._write_target_fixtures() + from common import load_target_config + + targets_dir = os.path.join(self.platforms_dir, "targets") + overrides = { + "testplatform": { + "targets": { + "_default": {"remove_cores": ["core_a"], "add_cores": ["core_z"]}, + "target-full": {"aliases": ["full"], "add_cores": ["core_a"]}, + }, + }, + } + with open(os.path.join(targets_dir, "_overrides.yml"), "w") as f: + yaml.dump(overrides, f) + self.assertEqual( + load_target_config("testplatform", "target-minimal", self.platforms_dir), + {"core_z"}, + ) + self.assertEqual( + load_target_config("testplatform", "full", self.platforms_dir), + {"core_a", "core_b", "core_c", "core_z"}, + ) + with self.assertRaises(ValueError): + load_target_config("testplatform", "_default", self.platforms_dir) + def test_target_single_target_noop(self): self._write_target_fixtures() from common import load_target_config diff --git a/tests/test_install.py b/tests/test_install.py index 51cfaf4f..2722d366 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -821,6 +821,33 @@ class TargetFlagIsAppliedOrRefused(unittest.TestCase): f"{alias} disagrees with what the builder resolves", ) + def test_every_published_target_is_the_list_the_builder_resolves(self): + """add_cores and remove_cores reach the installer, not only the builder. + + The published lists were the scraped ones, untouched by the + overrides: Batocera x86_64 lacked citron and xenia, which the pack + for that target ships, and carried the Amiga machine variants and + the launchers the builder drops. + """ + import sys as _sys + + _sys.path.insert(0, str(REPO_ROOT / "scripts")) + from common import load_target_config + + manifests = sorted((REPO_ROOT / "install" / "targets").glob("*.json")) + if not manifests: + self.skipTest("no target manifest generated yet") + for path in manifests: + targets = json.loads(path.read_text()) + for target, cores in targets.items(): + if cores is None: + continue + with self.subTest(platform=path.stem, target=target): + resolved = load_target_config( + path.stem, target, str(REPO_ROOT / "platforms") + ) + self.assertEqual(cores, sorted(resolved)) + class TestBaseUrlScheme(unittest.TestCase): """The bootstraps insist on HTTPS; the installer must not be laxer."""