feat: honour the boards a folder offers a core on

ROCKNIX writes some add_emu_core calls under a case on DEVICE, and the
rom folders file keeps that branch as the offer's `when` (uae4arm for
amiga, emulators/package.mk:188). A board's core list names such a core
when another folder offers it there, so --target placed its files in a
folder the board does not offer it for.

offer_boards() reads `when` and board_defaults against the platform's
targets, aliases included. A board build (generate_pack and generate
--manifest with --target, verify --target) carries the board in the
config (TARGET_KEY), and content_folder_tags leaves out the pairs the
board is not offered. A manifest for every board lists in
offered_boards each profile placed in such a folder, and install.py
--target drops its files there on the other boards.

Manifests: no file moves. The cores ROCKNIX offers under a device case
read no file beside the game today, so no manifest carries
offered_boards yet.
This commit is contained in:
Abdessamad Derraz committed 2026-11-11 13:46:57 +01:00
1 parent 389ebfa132
commit 05f3f5c702
10 files changed
+383 -41

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
# the site's build rewrites this line there (scripts/sitebootstrap.py).
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
$defaultInstallSha256 = "67036bbe4db401de213d9a8ab44161b8b64bfde91834b438fff47802b5a461aa"
$defaultInstallSha256 = "8c8ead297c502f40f02f4407bdbb3edcb2af31b68f0a3fca06ed963c6cbc3cb0"
# The pin above refuses any changed byte: this cap only stops a body that
# cannot be the installer (a sign-in portal's page) before it is hashed.
$maximumInstallerBytes = 8MB
+45 -2
View File
@@ -4277,6 +4277,21 @@ def _validate_manifest(data: object, plat: str) -> dict:
_check_tags(mapping, key, field)
if mapping[key] is None:
raise ValueError(f"invalid {field} for {key}")
offered = data.get("offered_boards")
if offered is not None:
if not isinstance(offered, dict) or len(offered) > MAX_MANIFEST_FILES:
raise ValueError("invalid offered_boards")
for key, places in offered.items():
if not isinstance(key, str) or not 0 < len(key) <= 256:
raise ValueError("invalid offered_boards key")
if not isinstance(places, list) or not all(
isinstance(p, dict) and isinstance(p.get("folder"), str)
and p.get("root") in ("roms", "install")
and isinstance(p.get("boards"), list)
and all(isinstance(b, str) for b in p["boards"])
for p in places
):
raise ValueError(f"invalid offered_boards for {key}")
titles = data.get("system_titles")
if titles is not None:
if not isinstance(titles, dict) or len(titles) > MAX_MANIFEST_FILES:
@@ -4883,6 +4898,33 @@ def designated_cores(
return chosen
def offered_here(
manifest: dict, entry: dict, cores: set[str], target: str | None
) -> set[str]:
"""The cores a board reads this file for.
A roms folder can offer a core on some boards only (ROCKNIX writes
uae4arm for roms/amiga under the devices it builds it for): on another
board, that core reads nothing there, even when the board runs it for
another folder. `offered_boards` gives those boards.
"""
offered = manifest.get("offered_boards") or {}
if target is None or not offered:
return cores
tree = {"roms": "roms", None: "install"}.get(entry.get("root"))
if tree is None:
return cores
dest = str(entry.get("dest", ""))
return {
core for core in cores
if not any(
place["root"] == tree and dest.startswith(place["folder"] + "/")
and target not in place["boards"]
for place in offered.get(core) or []
)
}
def narrow_to_cores(
manifest: dict,
names: list[str],
@@ -4919,7 +4961,8 @@ def narrow_to_cores(
result.append(_for_cores(entry, cores) or _settle_slot(entry, cores))
continue
if entry.get("cores") is not None:
copy = _for_cores(entry, cores)
here = offered_here(manifest, entry, cores, target)
copy = _for_cores(entry, here)
if copy is not None:
result.append(copy)
continue
@@ -4927,7 +4970,7 @@ def narrow_to_cores(
# wants the slot and has nothing held is a file left out.
claim = next(
(c for c in entry.get("absent_claims", [])
if cores.intersection(c.get("cores") or [])),
if here.intersection(c.get("cores") or [])),
None,
)
if claim is not None:
+1 -1
View File
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
# curl or wget stop on a cut transfer before the hash is read: the site's
# build rewrites this line there (scripts/sitebootstrap.py).
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
DEFAULT_INSTALL_SHA256="67036bbe4db401de213d9a8ab44161b8b64bfde91834b438fff47802b5a461aa"
DEFAULT_INSTALL_SHA256="8c8ead297c502f40f02f4407bdbb3edcb2af31b68f0a3fca06ed963c6cbc3cb0"
# The pin above refuses any changed byte: this cap only stops a body that
# cannot be the installer (a sign-in portal's page) before it is hashed.
MAX_INSTALLER_BYTES=8388608
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 1220974,
"sha256": "67036bbe4db401de213d9a8ab44161b8b64bfde91834b438fff47802b5a461aa",
"size": 1222786,
"sha256": "8c8ead297c502f40f02f4407bdbb3edcb2af31b68f0a3fca06ed963c6cbc3cb0",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 1274599,
"sha256": "8ea3d79c9bbcec747555c3970edef0692db5a00ada0ff45892aaf68dcfc38eb6"
"size": 1276411,
"sha256": "8008566a12fc1d18a19d9510c1884919d60ede9eb227bef25973082ac1b204e3"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 1220974,
"sha256": "67036bbe4db401de213d9a8ab44161b8b64bfde91834b438fff47802b5a461aa",
"size": 1222786,
"sha256": "8c8ead297c502f40f02f4407bdbb3edcb2af31b68f0a3fca06ed963c6cbc3cb0",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 1220974,
"sha256": "67036bbe4db401de213d9a8ab44161b8b64bfde91834b438fff47802b5a461aa",
"size": 1222786,
"sha256": "8c8ead297c502f40f02f4407bdbb3edcb2af31b68f0a3fca06ed963c6cbc3cb0",
"executable": false
},
{
+21
View File
@@ -142,6 +142,27 @@
"uniqueItems": true
}
},
"offered_boards": {
"description": "Each core a roms folder of the platform offers on some boards only, with the folder (below the roms root, or the install tree) and the boards and board aliases it is offered on. install.py --target keeps the core's files in that folder on those boards only.",
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["root", "folder", "boards"],
"properties": {
"root": {"enum": ["roms", "install"]},
"folder": {"type": "string", "minLength": 1},
"boards": {
"type": "array",
"items": {"type": "string", "minLength": 1},
"uniqueItems": true
}
}
}
}
},
"system_cores": {
"description": "Each platform system with the cores whose presence keeps it. A system absent here is kept whatever cores are available.",
"type": "object",
+129 -29
View File
@@ -3412,6 +3412,107 @@ def _core_key(name: str) -> str:
return str(name).lower().replace("-", "_")
# Set on a platform config built for one board (generate_pack, verify
# --target): a core a roms folder offers on other boards only is not placed
# there.
TARGET_KEY = "_target"
def _branch_holds(branch: str, board: str) -> bool:
"""Whether one `VARIABLE PATTERN` branch of a device case admits a board.
ROCKNIX files a board under the DEVICE value its target scraper reads,
lowercased. A branch on any other variable (PREFER_GLES) is no board
condition, and admits every board.
"""
variable, _, pattern = branch.strip().partition(" ")
if variable != "DEVICE" or not pattern.strip():
return True
import fnmatch # noqa: PLC0415
return any(
fnmatch.fnmatchcase(board.upper(), p.strip().upper()) for p in pattern.split("|")
)
def when_admits(when: str, board: str) -> bool:
"""Whether an offer's `when` holds on a board: alternatives joined by
"; ", each the branches it is nested under joined by ", "."""
return any(
all(_branch_holds(branch, board) for branch in alternative.split(", "))
for alternative in when.split("; ")
)
def offer_boards(config: dict) -> dict[tuple[str, str], frozenset[str] | None]:
"""The boards on which each roms folder offers each core, by
(core key, folder), None where every board does.
ROCKNIX writes a core under a `case "${DEVICE}"` branch for the devices
it builds (`when`), and a default for a named device in board_defaults
(DEVICE=RK3326). Boards are the platform's targets, aliases included.
"""
platform = str(config.get("_key") or "")
targets = list_available_targets(
platform, str(Path(__file__).resolve().parent.parent / "platforms")
) if platform else []
names = {t["name"]: [t["name"], *t.get("aliases", [])] for t in targets}
def admitted(test: Callable[[str], bool]) -> set[str]:
return {n for board, spelled in names.items() if test(board) for n in spelled}
every = admitted(lambda _board: True)
found: dict[tuple[str, str], set[str]] = {}
for row in config.get("_rom_folders") or []:
folder = str(row.get("folder") or "")
offers = row.get("offers") or []
offered = {(o.get("emulator"), _core_key(o.get("core") or "")) for o in offers}
choices: list[tuple[dict, set[str]]] = []
if row.get("core") and (row.get("emulator"), _core_key(row["core"])) not in offered:
choices.append((row, every))
for key, spec in (row.get("board_defaults") or {}).items():
branch = str(key).replace("=", " ", 1)
choices.append((spec, admitted(lambda b, branch=branch: _branch_holds(branch, b))))
for offer in offers:
when = str(offer.get("when") or "")
choices.append((offer, admitted(lambda b, when=when: when_admits(when, b))
if when else every))
for choice, boards in choices:
if choice.get("core") and choice.get("emulator") in LIBRETRO_RUNNERS:
found.setdefault((_core_key(choice["core"]), folder), set()).update(boards)
return {
pair: None if not names or boards >= every else frozenset(boards)
for pair, boards in found.items()
}
def content_folder_boards(config: dict) -> dict[tuple[str, str], frozenset[str] | None]:
"""Each (core key, roms folder) a platform launches a libretro core for,
with the boards it does so on, None for every board."""
pairs: set[tuple[str, str]] = set()
for core, systems in (config.get("libretro_systems") or {}).items():
pairs |= {(_core_key(core), str(s)) for s in systems or []}
for system in (config.get("systems") or {}).values():
if isinstance(system, dict) and system.get("native_id"):
pairs |= {(_core_key(c), str(system["native_id"])) for c in system.get("cores") or []}
for record in config.get("launchers") or []:
if record.get("kind") == "package" and record.get("build") == "libretro":
package = str(record.get("emulator", ""))
if package.startswith("lr-mess"):
# Built with SUBTARGET=mess (lr-mess2016.sh:31): the MESS
# drivers alone, none of the arcade sets of the core it
# shares a repository with.
continue
core = _core_key(package.removeprefix("lr-"))
pairs |= {(core, str(s)) for s in record.get("systems") or []}
# Pairs from the platform's own lists hold on every board, an offer on
# the boards its frontend writes it for.
return {
**{pair: on for pair, on in offer_boards(config).items() if pair not in pairs},
**{pair: None for pair in pairs},
}
def content_folder_tags(config: dict) -> set[str]:
"""The roms folders a platform launches each libretro core for, and the
ROM search paths its commands hand emulators and cores, as tags.
@@ -3435,34 +3536,22 @@ def content_folder_tags(config: dict) -> set[str]:
return tags
rows = config.get("_rom_folders") or []
folders = {str(r.get("folder")) for r in rows if r.get("folder")}
pairs: set[tuple[str, str]] = set()
for core, systems in (config.get("libretro_systems") or {}).items():
pairs |= {(_core_key(core), str(s)) for s in systems or []}
for row in rows:
choices = [row, *(row.get("board_defaults") or {}).values(), *(row.get("offers") or [])]
for choice in choices:
if choice.get("core") and choice.get("emulator") in LIBRETRO_RUNNERS:
pairs.add((_core_key(choice["core"]), str(row["folder"])))
for system in (config.get("systems") or {}).values():
if isinstance(system, dict) and system.get("native_id"):
pairs |= {(_core_key(c), str(system["native_id"])) for c in system.get("cores") or []}
for record in config.get("launchers") or []:
if record.get("kind") == "package" and record.get("build") == "libretro":
package = str(record.get("emulator", ""))
if package.startswith("lr-mess"):
# Built with SUBTARGET=mess (lr-mess2016.sh:31): the MESS
# drivers alone, none of the arcade sets of the core it
# shares a repository with.
continue
core = _core_key(package.removeprefix("lr-"))
pairs |= {(core, str(s)) for s in record.get("systems") or []}
boards = content_folder_boards(config)
target = config.get(TARGET_KEY)
pairs = {pair for pair, on in boards.items() if not target or on is None or target in on}
from launchers import _roms_place # noqa: PLC0415
def place(folder: str) -> list:
return list(_roms_place(config, folder) or ())
# Without a board, a core a folder offers on some boards only carries
# them, for a manifest the installer narrows to one.
tags |= {
CONTENT_FOLDER_PREFIX + json.dumps([c, f, place(f)]) for c, f in pairs if f in folders
CONTENT_FOLDER_PREFIX + json.dumps([
c, f, place(f),
None if target or boards.get((c, f)) is None else sorted(boards[(c, f)]),
])
for c, f in pairs if f in folders
}
for row in rows:
for search in row.get("searched_by") or []:
@@ -3601,12 +3690,19 @@ def content_layout(
return {**entry, **where, "path": f"{folder}/{rel}", "layout_path": rel}
if not standalone:
folders = sorted({
tuple(where) for core, _folder, where in (
json.loads(t[len(CONTENT_FOLDER_PREFIX):])
for t in tags if t.startswith(CONTENT_FOLDER_PREFIX)
) if core in names and where
})
# A place keeps the boards its core is offered on there, every board
# when one folder offers it everywhere.
on_boards: dict[tuple, set[str] | None] = {}
for core, _folder, where, on in (
json.loads(t[len(CONTENT_FOLDER_PREFIX):])
for t in tags if t.startswith(CONTENT_FOLDER_PREFIX)
):
if core not in names or not where:
continue
place = tuple(where)
known = on_boards.get(place, set())
on_boards[place] = None if on is None or known is None else known | set(on)
folders = sorted(on_boards)
# The folders a passed -rompath replaces the build's own search
# path with stand for these: libretro MAME hands MAME
# <system>/mame/bios unless the content's command line names
@@ -3633,7 +3729,11 @@ def content_layout(
out.append({**plain, "unplaced": OUTSIDE_BIOS_FOLDER})
continue
rel = str(f.get("path") or f.get("name") or "")
out.extend(roms_entry(plain, tree, folder, rel) for tree, folder in folders)
out.extend(
{**roms_entry(plain, tree, folder, rel),
**({"boards": sorted(on)} if (on := on_boards[(tree, folder)]) is not None else {})}
for tree, folder in folders
)
return {**view, "files": out} if changed else view
pools = {str(v).strip("/") for v in (view.get("standalone_pools") or {}).values()}
if not searches or "" not in pools:
+71
View File
@@ -39,6 +39,10 @@ sys.path.insert(0, os.path.dirname(__file__))
from alternatives import core_file_spares, settle_sets, slot_set_keys
from common import resolve_platform_cores as _platform_cores
from common import (
ROMS_ROOT,
TARGET_KEY,
_core_key,
content_folder_boards,
OUTER_ROOTS,
outer_layout,
installed_data_refs,
@@ -895,6 +899,8 @@ def generate_pack(
Returns the path to the generated ZIP, or None on failure.
"""
config = load_platform_config(platform_name, platforms_dir)
if target_name:
config = {**config, TARGET_KEY: target_name}
if zip_contents is None:
zip_contents = {}
if emu_profiles is None:
@@ -4861,6 +4867,8 @@ def generate_manifest(
config = load_platform_config(platform_name, platforms_dir)
if fold_case is not None:
config = {**config, "case_insensitive_fs": fold_case}
if target_name:
config = {**config, TARGET_KEY: target_name}
if zip_contents is None:
zip_contents = {}
if emu_profiles is None:
@@ -5334,6 +5342,10 @@ def generate_manifest(
**({"downloaded_cores": downloaded} if (downloaded := _downloaded_cores(
config, emu_profiles, core_names, platforms_dir,
)) else {}),
**({"offered_boards": offered} if (offered := _offered_boards(
config, emu_profiles, core_names,
[*manifest_files, *omitted_by_destination.values()],
)) else {}),
"system_cores": system_cores,
# The names the platform itself gives its systems (Batocera's psx),
# which --system accepts beside the ids.
@@ -5488,6 +5500,65 @@ def _manifest_data_directories(
return trees
def _offered_boards(
config: dict,
profiles: dict[str, dict],
core_names: dict[str, list[str]],
entries: list[dict],
) -> dict[str, list[dict]]:
"""The roms folders each profile is offered for on some boards only,
each with those boards, aliases included, where the manifest places a
file of that profile.
ROCKNIX writes uae4arm for roms/amiga under the devices it builds it for
(emulators/package.mk:188), and a board's core list names uae4arm when
another folder offers it there. The pack builder reads the board from
the config (TARGET_KEY) and `install.py --target` reads its answer here.
A manifest built for one board needs none.
"""
if config.get(TARGET_KEY):
return {}
pairs = content_folder_boards(config)
if all(on is None for on in pairs.values()):
return {}
from launchers import _roms_place # noqa: PLC0415
by_key: dict[str, set[str]] = {}
for name, claimants in upstream_profile_index(profiles).items():
by_key.setdefault(_core_key(name), set()).update(claimants)
free = {
(profile, folder) for (core, folder), on in pairs.items() if on is None
for profile in by_key.get(core, ())
}
found: dict[tuple[str, str, str], set[str]] = {}
for (core, folder), on in pairs.items():
where = _roms_place(config, folder) if on is not None else None
if where is None:
continue
for profile in by_key.get(core, ()):
if profile in core_names and (profile, folder) not in free:
found.setdefault((profile, where[0], where[1]), set()).update(on)
# An entry below the roms root names it, one in the install tree none.
trees = {ROMS_ROOT: ROMS_ROOT, None: "install"}
placed = {
(profile, trees[entry.get("root")], str(entry.get("dest", "")).rpartition("/")[0])
for entry in entries if entry.get("root") in trees
for copy in [entry, *entry.get("alternatives", [])]
for profile in copy.get("cores") or []
}
result: dict[str, list[dict]] = {}
for (profile, tree, folder), boards in sorted(found.items()):
if not any(
p == profile and t == tree and (d == folder or d.startswith(f"{folder}/"))
for p, t, d in placed
):
continue
result.setdefault(profile, []).append(
{"root": tree, "folder": folder, "boards": sorted(boards)}
)
return result
def _downloaded_cores(
config: dict,
profiles: dict[str, dict],
+7
View File
@@ -46,6 +46,8 @@ from alternatives import (
slot_set_keys,
)
from common import (
TARGET_KEY,
canonical_target_name,
OUTER_ROOTS,
_foreign_tree,
_owner_tokens,
@@ -3302,6 +3304,11 @@ def _main(held: contextlib.ExitStack) -> None:
for group_platforms, representative in groups:
config = load_platform_config(representative, args.platforms_dir)
tc = target_cores_cache.get(representative) if args.target else None
if tc is not None:
# A core a roms folder offers on other boards only is not placed
# there, as generate_pack builds the board's pack.
config = {**config, TARGET_KEY: canonical_target_name(
[representative], args.target, args.platforms_dir)}
result = verify_platform(
config,
db,
+46
View File
@@ -145,6 +145,52 @@ class ACoreOnlyTheDownloaderBrings(unittest.TestCase):
self.assertEqual([f["dest"] for f in files], ["kick31.rom", "roms/a4091.rom"])
class CoreOfferedOnSomeBoards(unittest.TestCase):
"""ROCKNIX writes uae4arm for roms/amiga under the devices it builds it
for (emulators/package.mk:188), and s922x runs uae4arm for amigacd32:
the core list alone would keep its roms/amiga files there."""
MANIFEST = {
"core_names": {"uae4arm": [], "puae": []},
"offered_boards": {
"uae4arm": [{"root": "roms", "folder": "amiga", "boards": ["h700", "rk3326"]}],
},
"files": [
{"dest": "amiga/kick.rom", "cores": ["uae4arm"], "root": "roms"},
{"dest": "amigacd32/kick.rom", "cores": ["uae4arm"], "root": "roms"},
{"dest": "amiga/shared.rom", "cores": ["uae4arm", "puae"], "root": "roms"},
],
"omitted_files": [],
}
def dests(self, target):
files, _ = install.narrow_to_cores(self.MANIFEST, ["uae4arm"], target=target)
return [f["dest"] for f in files]
def test_a_board_it_is_offered_on_keeps_the_folder(self):
self.assertEqual(self.dests("rk3326"),
["amiga/kick.rom", "amigacd32/kick.rom", "amiga/shared.rom"])
def test_another_board_keeps_only_the_folders_offering_it(self):
self.assertEqual(self.dests("s922x"), ["amigacd32/kick.rom"])
def test_a_core_offered_everywhere_keeps_a_shared_file(self):
files, _ = install.narrow_to_cores(self.MANIFEST, ["uae4arm", "puae"], target="s922x")
self.assertEqual([f["dest"] for f in files], ["amigacd32/kick.rom", "amiga/shared.rom"])
def test_installed_cores_are_no_board(self):
files, _ = install.narrow_to_cores(self.MANIFEST, ["uae4arm"])
self.assertEqual(len(files), 3)
def test_the_manifest_check_refuses_a_malformed_map(self):
base = {"manifest_version": 2, "platform": "rocknix", "files": [], "omitted_files": []}
install._validate_manifest(
{**base, "offered_boards": self.MANIFEST["offered_boards"]}, "rocknix")
with self.assertRaisesRegex(ValueError, "offered_boards"):
install._validate_manifest(
{**base, "offered_boards": {"uae4arm": [{"folder": "amiga"}]}}, "rocknix")
class ACoreTheListDoesNotKnow(unittest.TestCase):
"""A core newer than the list (parallel_dc, issue 87) loses nothing its .info declares."""
+54
View File
@@ -13,6 +13,7 @@ import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
import hashlib
import io
import json
import shutil
import sys
import tempfile
@@ -1208,6 +1209,59 @@ class EveryCoreAFolderOffers(unittest.TestCase):
self.assertIn("arcade", folders, platform)
class CoreOfferedOnSomeBoards(unittest.TestCase):
"""ROCKNIX writes some add_emu_core calls under a `case "${DEVICE}"`
branch (emulators/package.mk:188 offers uae4arm for amiga on the devices
it builds it for). A board's pack places such a core in that folder only
when the branch names the board, and a manifest for every board says
where it does."""
ROW = {
"folder": "amiga", "core": "puae", "emulator": "retroarch",
"offers": [
{"emulator": "retroarch", "core": "puae"},
{"emulator": "retroarch", "core": "uae4arm", "when": "DEVICE H700|RK3326"},
{"emulator": "retroarch", "core": "puae2021",
"when": 'if [ "${PREFER_GLES}" = "yes" ]; then'},
],
}
def config(self, **extra) -> dict:
base = load_platform_config("rocknix", str(REPO_ROOT / "platforms"))
return {**base, "_rom_folders": [self.ROW], **extra}
def test_a_branch_names_the_boards(self) -> None:
from common import when_admits
self.assertTrue(when_admits("DEVICE H700|RK3326", "rk3326"))
self.assertFalse(when_admits("DEVICE H700|RK3326", "s922x"))
self.assertTrue(when_admits("DEVICE AMD64; DEVICE RK3588*", "rk3588"))
self.assertTrue(when_admits('if [ "${PREFER_GLES}" = "yes" ]; then', "h700"))
def test_an_offer_carries_the_boards_its_branch_names(self) -> None:
from common import offer_boards
boards = offer_boards(self.config())
self.assertEqual(boards[("uae4arm", "amiga")], frozenset({"h700", "rk3326"}))
self.assertIsNone(boards[("puae", "amiga")])
self.assertIsNone(boards[("puae2021", "amiga")])
def test_a_board_pack_places_the_core_where_the_board_offers_it(self) -> None:
from common import CONTENT_FOLDER_PREFIX, TARGET_KEY, content_folder_tags
def placed(config: dict) -> dict:
rows = [json.loads(t[len(CONTENT_FOLDER_PREFIX):]) for t in content_folder_tags(config)
if t.startswith(CONTENT_FOLDER_PREFIX)]
return {core: on for core, _folder, _where, on in rows}
every = placed(self.config())
self.assertEqual(every["uae4arm"], ["h700", "rk3326"])
self.assertIsNone(every["puae"])
self.assertIn("uae4arm", placed(self.config(**{TARGET_KEY: "rk3326"})))
self.assertNotIn("uae4arm", placed(self.config(**{TARGET_KEY: "s922x"})))
self.assertIn("puae", placed(self.config(**{TARGET_KEY: "s922x"})))
def platform_manifest(platform: str) -> dict:
from common import load_database