mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-17 00:43:29 -05:00
fix: keep a parent config intact under inheritance
This commit is contained in:
1 parent
de08f71c52
commit
1d7b6462e9
2 files changed
+126
-46
No files matched your search
+60
-46
@@ -96,6 +96,42 @@ _shared_yml_cache: dict[str, dict] = {}
|
||||
_platform_config_cache: dict[tuple[str, str], dict] = {}
|
||||
|
||||
|
||||
def _inherited_yaml(platform_name: str, platforms_dir: str) -> tuple[dict, list[str]]:
|
||||
"""The platform's YAML merged over its parents', and the chain of names.
|
||||
|
||||
Built from fresh copies: merging over the parent's cached config shared
|
||||
its systems, so a child's overrides rewrote the parent for the rest of
|
||||
the run.
|
||||
"""
|
||||
if yaml is None:
|
||||
raise ImportError("PyYAML required: pip install pyyaml")
|
||||
|
||||
config_file = os.path.join(platforms_dir, f"{platform_name}.yml")
|
||||
if not os.path.exists(config_file):
|
||||
raise FileNotFoundError(f"Platform config not found: {config_file}")
|
||||
|
||||
with open(config_file) as f:
|
||||
config = yaml_load(f) or {}
|
||||
if "inherits" not in config:
|
||||
return config, [platform_name]
|
||||
|
||||
merged, chain = _inherited_yaml(config["inherits"], platforms_dir)
|
||||
merged.update(
|
||||
{k: v for k, v in config.items() if k not in ("inherits", "overrides")}
|
||||
)
|
||||
if "overrides" in config and "systems" in config["overrides"]:
|
||||
merged.setdefault("systems", {})
|
||||
for sys_id, override in config["overrides"]["systems"].items():
|
||||
if sys_id in merged["systems"]:
|
||||
merged["systems"][sys_id] = {
|
||||
**merged["systems"][sys_id],
|
||||
**override,
|
||||
}
|
||||
else:
|
||||
merged["systems"][sys_id] = override
|
||||
return merged, [platform_name, *chain]
|
||||
|
||||
|
||||
def load_platform_config(platform_name: str, platforms_dir: str = "platforms") -> dict:
|
||||
"""Load a platform config with inheritance and shared group resolution.
|
||||
|
||||
@@ -106,34 +142,7 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") -
|
||||
if cache_key in _platform_config_cache:
|
||||
return _platform_config_cache[cache_key]
|
||||
|
||||
if yaml is None:
|
||||
raise ImportError("PyYAML required: pip install pyyaml")
|
||||
|
||||
config_file = os.path.join(platforms_dir, f"{platform_name}.yml")
|
||||
if not os.path.exists(config_file):
|
||||
raise FileNotFoundError(f"Platform config not found: {config_file}")
|
||||
|
||||
with open(config_file) as f:
|
||||
config = yaml_load(f) or {}
|
||||
|
||||
# Resolve inheritance
|
||||
if "inherits" in config:
|
||||
parent = load_platform_config(config["inherits"], platforms_dir)
|
||||
merged = {**parent}
|
||||
merged.update(
|
||||
{k: v for k, v in config.items() if k not in ("inherits", "overrides")}
|
||||
)
|
||||
if "overrides" in config and "systems" in config["overrides"]:
|
||||
merged.setdefault("systems", {})
|
||||
for sys_id, override in config["overrides"]["systems"].items():
|
||||
if sys_id in merged["systems"]:
|
||||
merged["systems"][sys_id] = {
|
||||
**merged["systems"][sys_id],
|
||||
**override,
|
||||
}
|
||||
else:
|
||||
merged["systems"][sys_id] = override
|
||||
config = merged
|
||||
config, chain = _inherited_yaml(platform_name, platforms_dir)
|
||||
|
||||
# Resolve shared group includes
|
||||
shared_path = os.path.join(platforms_dir, "_shared.yml")
|
||||
@@ -176,26 +185,31 @@ def load_platform_config(platform_name: str, platforms_dir: str = "platforms") -
|
||||
with open(registry_path) as f:
|
||||
_shared_yml_cache[reg_real] = yaml_load(f) or {}
|
||||
reg = _shared_yml_cache[reg_real]
|
||||
reg_entry = reg.get("platforms", {}).get(platform_name, {})
|
||||
# The platform's own entry first, then its parents' for what it does
|
||||
# not say: RetroPie's status, scraper and install are its own, its
|
||||
# verification mode is RetroArch's.
|
||||
for depth, name in enumerate(chain):
|
||||
reg_entry = reg.get("platforms", {}).get(name, {})
|
||||
|
||||
# Merge cores (union for lists, override for all_libretro)
|
||||
reg_cores = reg_entry.get("cores")
|
||||
if reg_cores is not None:
|
||||
cfg_cores = config.get("cores")
|
||||
if reg_cores == "all_libretro":
|
||||
config["cores"] = "all_libretro"
|
||||
elif isinstance(reg_cores, list) and isinstance(cfg_cores, list):
|
||||
merged_set = {str(c) for c in cfg_cores} | {str(c) for c in reg_cores}
|
||||
config["cores"] = sorted(merged_set)
|
||||
elif isinstance(reg_cores, list) and cfg_cores is None:
|
||||
config["cores"] = reg_cores
|
||||
# Merge cores (union for lists, override for all_libretro); a
|
||||
# parent's only where the platform names none.
|
||||
reg_cores = reg_entry.get("cores")
|
||||
if reg_cores is not None and (depth == 0 or "cores" not in config):
|
||||
cfg_cores = config.get("cores")
|
||||
if reg_cores == "all_libretro":
|
||||
config["cores"] = "all_libretro"
|
||||
elif isinstance(reg_cores, list) and isinstance(cfg_cores, list):
|
||||
merged_set = {str(c) for c in cfg_cores} | {str(c) for c in reg_cores}
|
||||
config["cores"] = sorted(merged_set)
|
||||
elif isinstance(reg_cores, list) and cfg_cores is None:
|
||||
config["cores"] = reg_cores
|
||||
|
||||
# Merge all registry fields absent from config (except cores,
|
||||
# handled above with union logic). No hardcoded list: any field
|
||||
# added to the registry is automatically available in the config.
|
||||
for key, val in reg_entry.items():
|
||||
if key != "cores" and key not in config:
|
||||
config[key] = val
|
||||
# Merge all registry fields absent from config (except cores,
|
||||
# handled above with union logic). No hardcoded list: any field
|
||||
# added to the registry is automatically available in the config.
|
||||
for key, val in reg_entry.items():
|
||||
if key != "cores" and key not in config:
|
||||
config[key] = val
|
||||
|
||||
# Recalbox hashes a .zip by its members, so a plain md5 never answers one
|
||||
# of its archive declarations: the resolver reads the mark per entry.
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""A platform that inherits another one.
|
||||
|
||||
The child was merged over the parent's cached config: its overrides rewrote
|
||||
the parent's systems for the rest of the run, and the parent's registry
|
||||
fields hid the child's own, so RetroPie loaded as active with RetroArch's
|
||||
scraper and install block.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
import common # noqa: E402
|
||||
|
||||
|
||||
class AChildLeavesItsParentAlone(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.dir = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
|
||||
self.addCleanup(shutil.rmtree, self.dir, True)
|
||||
self.addCleanup(common._platform_config_cache.clear)
|
||||
self.addCleanup(common._shared_yml_cache.clear)
|
||||
files = {
|
||||
"parent.yml": {"platform": "Parent", "verification_mode": "existence",
|
||||
"systems": {"sys-a": {"files": [{"name": "a.bin"}]}}},
|
||||
"child.yml": {"platform": "Child", "inherits": "parent", "overrides": {
|
||||
"systems": {"sys-a": {"files": [{"name": "child.bin"}]},
|
||||
"sys-new": {"files": [{"name": "new.bin"}]}}}},
|
||||
"_registry.yml": {"platforms": {
|
||||
"parent": {"status": "active", "scraper": "parent", "hash_type": "md5",
|
||||
"cores": "all_libretro"},
|
||||
"child": {"status": "archived", "scraper": "child"},
|
||||
}},
|
||||
}
|
||||
for name, data in files.items():
|
||||
(self.dir / name).write_text(yaml.safe_dump(data))
|
||||
|
||||
def load(self, name):
|
||||
return common.load_platform_config(name, str(self.dir))
|
||||
|
||||
def test_the_parent_keeps_its_systems(self):
|
||||
self.load("parent")
|
||||
self.load("child")
|
||||
parent = self.load("parent")
|
||||
self.assertEqual(list(parent["systems"]), ["sys-a"])
|
||||
self.assertEqual(parent["systems"]["sys-a"]["files"], [{"name": "a.bin"}])
|
||||
|
||||
def test_the_child_reads_its_own_registry_entry_first(self):
|
||||
self.load("parent")
|
||||
child = self.load("child")
|
||||
self.assertEqual((child["status"], child["scraper"]), ("archived", "child"))
|
||||
# What it does not say comes from its parent's entry.
|
||||
self.assertEqual((child["hash_type"], child["cores"]), ("md5", "all_libretro"))
|
||||
self.assertEqual(child["systems"]["sys-a"]["files"], [{"name": "child.bin"}])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user