fix: record each platform's own list version

install/<platform>.json wrote the constant "1.0" as the version of the
list it was built from. The manifest now carries the version the platform
YAML records, and generate_pack refuses a YAML without one.

inherits: no longer passes a parent's version to a child: Lakka was
published as RetroArch v1.22.2 in its manifest, pack name and site page.
lakka.yml records v6.1, the Lakka-LibreELEC tag its target list is read
at. The MiSTer scraper refuses a BiosDB without a timestamp instead of
writing an empty version.

A pack's manifest.json records list_version, so an install from the pack
alone knows it too. Manifests regenerated twice and compared: only
version and generated changed.
This commit is contained in:
Abdessamad Derraz committed 2026-10-27 19:10:39 +01:00
1 parent 1f6c018006
commit 25c356669b
37 files changed
+197 -52

No files matched your search

+53
View File
@@ -2080,6 +2080,59 @@ def _git_paths(*args: str, stdin: list[str] | None = None) -> set[str] | None:
return {p for p in result.stdout.split("\0") if p}
class ManifestVersionIsThePlatformsOwn(unittest.TestCase):
"""Every install manifest named its list "1.0", and an inheriting YAML
took its parent's version: Lakka was published as RetroArch v1.22.2.
The installer compares the list's version with the one a machine runs,
so each manifest states the version its own platform YAML records."""
def setUp(self):
from scripts.common import _platform_config_cache
_platform_config_cache.clear()
def test_committed_manifests_state_their_platform_version(self):
from scripts.common import load_platform_config
manifests = sorted((ROOT / "install").glob("*.json"))
self.assertTrue(manifests)
for path in manifests:
with self.subTest(manifest=path.name):
data = json.loads(path.read_text())
config = load_platform_config(data["platform"], str(ROOT / "platforms"))
self.assertTrue(config.get("version"))
self.assertEqual(data["version"], str(config["version"]))
def test_no_child_shows_its_parents_version(self):
from scripts.common import load_platform_config
platforms = ROOT / "platforms"
children = {}
for path in sorted(platforms.glob("*.yml")):
raw = yaml.safe_load(path.read_text()) or {}
if isinstance(raw, dict) and raw.get("inherits"):
children[path.stem] = raw
self.assertIn("lakka", children)
for name, raw in children.items():
with self.subTest(platform=name):
parent = yaml.safe_load(
(platforms / f"{raw['inherits']}.yml").read_text())
version = load_platform_config(name, str(platforms)).get("version")
self.assertEqual(version, raw.get("version"))
self.assertNotEqual(version, parent.get("version"))
def test_an_unversioned_child_has_no_version(self):
from scripts.common import load_platform_config
with tempfile.TemporaryDirectory(dir=TMP_ROOT) as tmp:
Path(tmp, "parent.yml").write_text("platform: P\nversion: '2.0'\nsystems: {}\n")
Path(tmp, "child.yml").write_text("inherits: parent\nplatform: C\n")
self.assertNotIn("version", load_platform_config("child", tmp))
def test_lakka_names_the_release_its_cores_are_read_at(self):
platforms = ROOT / "platforms"
lakka = yaml.safe_load((platforms / "lakka.yml").read_text())
targets = yaml.safe_load((platforms / "targets" / "lakka.yml").read_text())
tag = re.search(r"/blob/([^/]+)/", targets["source"]).group(1)
self.assertEqual(lakka["version"], tag)
class EveryManifestEntryIsFetchable(unittest.TestCase):
"""An install manifest may not list a file the installer cannot get.
+7 -6
View File
@@ -58,6 +58,7 @@ class ACoreFileFillsAFreeDeclaration(unittest.TestCase):
self.emulators.mkdir()
(self.platforms / "fakeplat.yml").write_text(yaml.safe_dump({
"platform": "FakePlat",
"version": "2.1",
"verification_mode": "existence",
"base_destination": "system",
"cores": ["fakecore"],
@@ -118,7 +119,7 @@ class AFileDeclaredUnderAnotherName(unittest.TestCase):
(root / "platforms").mkdir()
(root / "emulators").mkdir()
(root / "platforms" / "fakeplat.yml").write_text(yaml.safe_dump({
"platform": "FakePlat", "verification_mode": "existence",
"platform": "FakePlat", "version": "2.1", "verification_mode": "existence",
"base_destination": "system", "cores": ["gs"],
"systems": {"apple-iigs": {"files": [
{"name": "ROM1", "destination": "ROM1", "md5": md5},
@@ -156,7 +157,7 @@ class AMissingDataDirectoryFailsTheRun(unittest.TestCase):
(root / "emulators").mkdir()
(root / "bios").mkdir()
(root / "platforms" / "fakeplat.yml").write_text(yaml.safe_dump({
"platform": "FakePlat", "verification_mode": "existence",
"platform": "FakePlat", "version": "2.1", "verification_mode": "existence",
"base_destination": "system", "cores": [],
"systems": {"console-a": {"files": [], "data_directories": [
{"ref": "assets", "destination": "assets"},
@@ -197,7 +198,7 @@ class AFileNobodyHoldsIsAnOmission(unittest.TestCase):
for folder in ("platforms", "emulators", "bios"):
(root / folder).mkdir()
(root / "platforms" / "fakeplat.yml").write_text(yaml.safe_dump({
"platform": "FakePlat", "verification_mode": "existence",
"platform": "FakePlat", "version": "2.1", "verification_mode": "existence",
"base_destination": "system", "cores": ["devil"],
"systems": {"diablo": {"files": []}},
}))
@@ -238,7 +239,7 @@ class AMemberNameIsNotALooseFile(unittest.TestCase):
(root / "platforms").mkdir()
(root / "emulators").mkdir()
(root / "platforms" / "fakeplat.yml").write_text(yaml.safe_dump({
"platform": "FakePlat", "verification_mode": "existence",
"platform": "FakePlat", "version": "2.1", "verification_mode": "existence",
"base_destination": "system", "cores": ["naomi2core"],
"systems": {"sega-naomi2": {"files": []}},
}))
@@ -283,7 +284,7 @@ class TwoDeclarationsOfOnePath(unittest.TestCase):
for folder in ("platforms", "emulators"):
(root / folder).mkdir()
(root / "platforms" / "fakeplat.yml").write_text(yaml.safe_dump({
"platform": "FakePlat", "verification_mode": "existence",
"platform": "FakePlat", "version": "2.1", "verification_mode": "existence",
"base_destination": "system", "cores": [],
"systems": {
"a-system": {"files": [{"name": first, "destination": "shared.bin"}]},
@@ -335,7 +336,7 @@ class ANamelessWhitelistEntry(unittest.TestCase):
(root / folder).mkdir()
values = [md5.upper(), f"{'0' * 32},{md5}", "f" * 32]
(root / "platforms" / "fakeplat.yml").write_text(yaml.safe_dump({
"platform": "FakePlat", "verification_mode": "md5",
"platform": "FakePlat", "version": "2.1", "verification_mode": "md5",
"base_destination": "bios", "cores": [],
"systems": {"sony-playstation": {"files": [
{"name": f"sony-playstation:{v}", "destination": "", "md5": v}
+1 -1
View File
@@ -76,7 +76,7 @@ class _Collection(unittest.TestCase):
}}
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
self.config = {
"platform": "Exist", "verification_mode": "existence",
"platform": "Exist", "version": "2.1", "verification_mode": "existence",
"cores": ["sized", "unsized"],
"systems": {"mac": {"files": []}, "iigs": {"files": []}},
}
+1
View File
@@ -56,6 +56,7 @@ files:
PLATFORM = {
"platform": "Demo",
"version": "2.1",
"verification_mode": "existence",
"base_destination": "",
"cores": ["demo"],
+47
View File
@@ -224,6 +224,7 @@ class TestE2E(unittest.TestCase):
def _create_existence_platform(self):
config = {
"platform": "TestExistence",
"version": "9.1",
"verification_mode": "existence",
"base_destination": "system",
"systems": {
@@ -3676,6 +3677,51 @@ class TestE2E(unittest.TestCase):
self.assertIsInstance(f["size"], int)
self.assertGreater(len(f["sha1"]), 0)
def test_91_manifest_version_is_the_platform_list_version(self):
"""The manifest names the version of the list it was built from.
It was the constant "1.0" for every platform, so the installer could
not tell which Batocera or Recalbox release a list described. Two
platforms of different versions show the value comes from each YAML.
A YAML without one, an inheriting child included, is refused rather
than written with a stand-in or its parent's version.
"""
from common import _platform_config_cache
from generate_pack import generate_manifest
registry_path = os.path.join(self.platforms_dir, "_test_registry_version.yml")
with open(registry_path, "w") as fh:
yaml.dump({"platforms": {}}, fh)
with open(os.path.join(self.platforms_dir, "test_md5.yml")) as fh:
config = yaml.safe_load(fh)
config["version"] = "41"
with open(os.path.join(self.platforms_dir, "versioned_md5.yml"), "w") as fh:
yaml.dump(config, fh)
del config["version"]
with open(os.path.join(self.platforms_dir, "unversioned.yml"), "w") as fh:
yaml.dump(config, fh)
_platform_config_cache.clear()
def version_of(name: str) -> str:
return generate_manifest(
name, self.platforms_dir, self.db, self.bios_dir, registry_path,
emulators_dir=self.emulators_dir, offline=True,
)["version"]
self.assertEqual(version_of("test_existence"), "9.1")
self.assertEqual(version_of("versioned_md5"), "41")
for name in ("unversioned", "test_inherited"):
with self.subTest(platform=name), \
self.assertRaisesRegex(ValueError, "records no version"):
version_of(name)
with open(os.path.join(self.platforms_dir, "test_inherited.yml")) as fh:
child = yaml.safe_load(fh)
child["version"] = "2"
with open(os.path.join(self.platforms_dir, "test_inherited.yml"), "w") as fh:
yaml.dump(child, fh)
_platform_config_cache.clear()
self.assertEqual(version_of("test_inherited"), "2")
def test_91b_manifest_hash_points_at_a_fetchable_file(self):
"""A declared hash no local file carries must not blank repo_path.
@@ -5726,6 +5772,7 @@ struct BurnDriver BurnDrvneogeo = {
self.db = self._build_db()
config = {
"platform": "RegionPlat",
"version": "3.0",
"verification_mode": "existence",
"systems": {
"region-console": {
+1
View File
@@ -47,6 +47,7 @@ class FrontendDigestDecides(unittest.TestCase):
)
(self.platforms / "digestplat.yml").write_text(yaml.safe_dump({
"platform": "DigestPlat",
"version": "2.1",
"verification_mode": "md5",
"base_destination": "bios",
"systems": {"psx": {"files": [{
+2 -2
View File
@@ -1021,7 +1021,7 @@ class AHeldFileTheInstallerCannotFetch(unittest.TestCase):
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
Path("platforms").mkdir()
Path("platforms/p.yml").write_text(json.dumps({
"platform": "P", "verification_mode": "existence", "cores": ["core"],
"platform": "P", "version": "2.1", "verification_mode": "existence", "cores": ["core"],
"systems": {"sys": {"files": []}},
}))
Path("platforms/_registry.yml").write_text(
@@ -1096,7 +1096,7 @@ class AStandaloneLayoutBelowTheBiosFolder(unittest.TestCase):
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
Path("platforms").mkdir()
Path("platforms/p.yml").write_text(json.dumps({
"platform": "P", "verification_mode": "existence",
"platform": "P", "version": "2.1", "verification_mode": "existence",
"base_destination": "bios", "cores": ["dsa", "other"],
"standalone_cores": ["dsa"], "systems": {"sys": {"files": []}},
}))
+1
View File
@@ -193,6 +193,7 @@ class PackPlacesCoreFilesUnderTheTag(unittest.TestCase):
platform = {
"platform": "MinUI Test",
"version": "2025-11-27",
"base_destination": "",
"verification_mode": "existence",
"cores": ["gambatte", "arcadecore", "unlisted"],
+1 -1
View File
@@ -92,7 +92,7 @@ class _Fixture(unittest.TestCase):
for key, profile in PROFILES.items():
(self.emulators / f"{key}.yml").write_text(yaml.dump(profile))
platform = {
"platform": "Handheld", "verification_mode": "existence",
"platform": "Handheld", "version": "2.1", "verification_mode": "existence",
"cores": ["emu", "doomcore"],
"systems": {
"console": {"files": [{"name": "firmware.rom", "destination": "firmware.rom"}]},
+2 -1
View File
@@ -52,6 +52,7 @@ files:
PLATFORM = {
"platform": "Demo",
"version": "2.1",
"verification_mode": "existence",
"base_destination": "",
"cores": ["demo"],
@@ -494,7 +495,7 @@ class RefusedFilesAreNotPresent(unittest.TestCase):
}
db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
config = {
"platform": "Hashy", "verification_mode": "md5", "base_destination": "bios",
"platform": "Hashy", "version": "2.1", "verification_mode": "md5", "base_destination": "bios",
"systems": {"sys": {"files": [
{"name": "good.bin", "destination": "good.bin",
"md5": hashlib.md5(b"good.bin").hexdigest()},
+1
View File
@@ -51,6 +51,7 @@ class RequiredTwins(unittest.TestCase):
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
platform = {
"platform": "Twins",
"version": "2.1",
"verification_mode": "md5",
"base_destination": "bios",
"cores": [],
+2 -2
View File
@@ -243,7 +243,7 @@ class _Fixture(unittest.TestCase):
"required": False}]}
for name in ("testra", "testnosave"):
(self.platforms / f"{name}.yml").write_text(yaml.safe_dump({
"platform": name, "verification_mode": "existence",
"platform": name, "version": "2.1", "verification_mode": "existence",
"base_destination": "system", "cores": ["corex"],
"systems": {"test-console": system},
}))
@@ -399,7 +399,7 @@ class AFolderPerSystem(_Fixture):
"required": False}]}
for name in ("testbato", "testrb"):
(self.platforms / f"{name}.yml").write_text(yaml.safe_dump({
"platform": name, "verification_mode": "existence",
"platform": name, "version": "2.1", "verification_mode": "existence",
"base_destination": "bios", "cores": ["corex", "corey"],
"systems": {"test-console": system},
# Two systems run CoreX, none runs CoreY.
+4 -3
View File
@@ -58,6 +58,7 @@ class SlugPlatform(unittest.TestCase):
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
self.config = {
"platform": "Slugs",
"version": "2.1",
"verification_mode": "md5",
"base_destination": "bios",
"cores": ["dosbox"],
@@ -139,7 +140,7 @@ class RegionKeysForEveryClaimant(unittest.TestCase):
)
common._emulator_profiles_cache.clear()
profiles = common.load_emulator_profiles("emulators")
config = {"platform": "P", "verification_mode": "existence", "cores": ["one", "two"],
config = {"platform": "P", "version": "2.1", "verification_mode": "existence", "cores": ["one", "two"],
"systems": {"sys": {"files": []}}}
_groups, extra_dests = platform_region_groups(
config, config["systems"], "emulators", db, "", profiles
@@ -178,7 +179,7 @@ class FlatFirmwareFolder(unittest.TestCase):
}
db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
config = {
"platform": "Flat", "verification_mode": "md5",
"platform": "Flat", "version": "2.1", "verification_mode": "md5",
"base_destination": "bios", "cores": ["psx"],
"systems": {
"psx": {"files": [{"name": "scph5501.bin",
@@ -300,7 +301,7 @@ class OnePathCountsOnce(unittest.TestCase):
common._emulator_profiles_cache.clear()
self.addCleanup(common._emulator_profiles_cache.clear)
profiles = common.load_emulator_profiles("emulators")
config = {"platform": "P", "verification_mode": "existence", "cores": ["one", "two"],
config = {"platform": "P", "version": "2.1", "verification_mode": "existence", "cores": ["one", "two"],
"systems": {"amiga": {"files": []}}}
report = verify_platform(config, db, "emulators", profiles)
claims = [u for u in report["undeclared_files"] if u["name"] == "rom.key"]