mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
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:
1 parent
389ebfa132
commit
05f3f5c702
10 files changed
+383
-41
No files matched your search
+129
-29
@@ -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:
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user