mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-25 05:09:03 -05:00
feat: place core files per handed system dir
A platform can hand each libretro core a system directory of its own (core_system_dirs). platform_layouts then gives one view per directory, so the gap report, the builder, the manifest, slots and cross_reference place a core's files below each folder it is handed, and a core handed none gets no file. Resolution strips the folder again and searches the collection by the core's own path.
This commit is contained in:
1 parent
ff2137de9d
commit
d0802c5360
6 files changed
+222
-51
No files matched your search
@@ -53,6 +53,56 @@
|
||||
"uniqueItems": true,
|
||||
"description": "Emulators of standalone_cores the platform also launches as a libretro core: both layouts get their files"
|
||||
},
|
||||
"core_system_dirs": {
|
||||
"type": "object",
|
||||
"description": "The libretro system directory the platform hands each core, keyed by that directory relative to the pack root. A platform that declares it gives a core no other directory: the core's files are placed below each directory listed for it, and a core listed nowhere gets none (MinUI hands each launch tag its own Bios/<TAG>/)",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"cores",
|
||||
"source_ref"
|
||||
],
|
||||
"properties": {
|
||||
"cores": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Libretro cores the platform starts with this directory, by the name of their .so"
|
||||
},
|
||||
"devices": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Devices whose build ships a launcher for this directory"
|
||||
},
|
||||
"source_ref": {
|
||||
"type": "string",
|
||||
"description": "file:line of the launcher that names the core, at the revision `source` pins"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"core_repositories": {
|
||||
"type": "object",
|
||||
"description": "Repositories each core of core_system_dirs is built from, read from the platform's build recipes",
|
||||
"additionalProperties": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_directory_ref": {
|
||||
"type": "string",
|
||||
"description": "file:line, at the revision `source` pins, of the code that sets the system directory core_system_dirs lists"
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
|
||||
+72
-2
@@ -1221,6 +1221,12 @@ def resolve_local_file(
|
||||
"""
|
||||
sha1 = file_entry.get("sha1")
|
||||
name = file_entry.get("name", "")
|
||||
# A platform that hands each core a system directory of its own (MinUI's
|
||||
# Bios/<TAG>/) writes that folder in front of the core's path. The tail
|
||||
# the collection indexes is the core's path alone.
|
||||
system_dir = file_entry.get("system_dir")
|
||||
if system_dir and dest_hint.startswith(f"{system_dir}/"):
|
||||
dest_hint = dest_hint[len(system_dir) + 1:]
|
||||
# A name written with its directory is the destination when no path is
|
||||
# given: gam4980/E.BIN resolved by its basename took another model's OS
|
||||
# in verify --emulator and the emulator pack, while the platform pack,
|
||||
@@ -2106,6 +2112,11 @@ def group_identical_platforms(
|
||||
# One archive cannot serve a filesystem that folds case and one
|
||||
# that does not: Lakka keeps bios.col beside BIOS.col.
|
||||
bool(config.get("case_insensitive_fs")),
|
||||
# Where each core's files go: the same cores handed other
|
||||
# folders make another pack.
|
||||
sorted(
|
||||
tag for tag in standalone_set(config) if tag.startswith(CORE_DIR_PREFIX)
|
||||
),
|
||||
]
|
||||
fp = hashlib.sha1(f"{fp}|{emulators}".encode()).hexdigest()
|
||||
if target_cores_cache:
|
||||
@@ -2252,6 +2263,55 @@ DUAL_MODE_PREFIX = "dual:"
|
||||
# Marks, in the same set, a whole directory the platform's launcher hands a
|
||||
# standalone emulator (launchers.handed_directories), as JSON.
|
||||
HANDED_PREFIX = "handed:"
|
||||
# Marks, in the same set, a libretro core and the system directory the
|
||||
# platform hands it (``core_system_dirs``), as JSON.
|
||||
CORE_DIR_PREFIX = "coredir:"
|
||||
|
||||
|
||||
def core_system_dirs(
|
||||
emu_name: str, profile: dict, standalone_cores: Iterable[str]
|
||||
) -> list[str] | None:
|
||||
"""The system directories a platform hands this libretro core, or None.
|
||||
|
||||
None when the platform declares no per-core directory: the core reads
|
||||
the one system directory every core shares. A platform that declares
|
||||
them (MinUI hands each launch tag its own Bios/<TAG>/) gives a core it
|
||||
lists no directory at all, so the list is then empty.
|
||||
"""
|
||||
pairs = [
|
||||
json.loads(tag[len(CORE_DIR_PREFIX):])
|
||||
for tag in standalone_cores if tag.startswith(CORE_DIR_PREFIX)
|
||||
]
|
||||
if not pairs:
|
||||
return None
|
||||
names = {emu_name, *(str(c) for c in profile.get("cores") or [])}
|
||||
return sorted({directory for core, directory in pairs if core in names})
|
||||
|
||||
|
||||
def system_dir_view(profile: dict, directory: str) -> dict:
|
||||
"""The profile as a core reads it from one handed system directory.
|
||||
|
||||
Each file keeps its own path, relative to the system directory, and
|
||||
carries the directory: destinations put it in front, resolution takes
|
||||
it off again (resolve_local_file), so the collection is still searched
|
||||
by the core's own path. A core's archive subfolder moves with it.
|
||||
"""
|
||||
view = {
|
||||
**profile,
|
||||
"files": [
|
||||
{**f, "system_dir": directory} if isinstance(f, dict) else f
|
||||
for f in profile.get("files") or []
|
||||
],
|
||||
}
|
||||
if profile.get("archive_prefix"):
|
||||
view["archive_prefix"] = f"{directory}/{profile['archive_prefix']}"
|
||||
return view
|
||||
|
||||
|
||||
def system_dir_path(entry: dict, path: str) -> str:
|
||||
"""``path`` below the system directory the entry carries, if any."""
|
||||
directory = entry.get("system_dir")
|
||||
return f"{directory}/{path}" if directory else path
|
||||
|
||||
|
||||
def standalone_set(config: dict) -> set[str]:
|
||||
@@ -2263,7 +2323,14 @@ def standalone_set(config: dict) -> set[str]:
|
||||
"""
|
||||
names = {str(c) for c in config.get("standalone_cores") or []}
|
||||
names |= {DUAL_MODE_PREFIX + str(c) for c in config.get("dual_mode_cores") or []}
|
||||
if names:
|
||||
# The system directory each core is handed travels the same way, so
|
||||
# platform_layouts places a core's files below it for every reader.
|
||||
names |= {
|
||||
CORE_DIR_PREFIX + json.dumps([str(core), directory])
|
||||
for directory, entry in (config.get("core_system_dirs") or {}).items()
|
||||
for core in entry.get("cores") or []
|
||||
}
|
||||
if names - {n for n in names if n.startswith(CORE_DIR_PREFIX)}:
|
||||
from launchers import handed_directories # noqa: PLC0415
|
||||
|
||||
names |= {
|
||||
@@ -2328,7 +2395,10 @@ def platform_layouts(
|
||||
(what the build reads from the system directory, where it reads it).
|
||||
"""
|
||||
if not runs_standalone(emu_name, profile, standalone_cores):
|
||||
return [(profile, False)]
|
||||
directories = core_system_dirs(emu_name, profile, standalone_cores)
|
||||
if directories is None:
|
||||
return [(profile, False)]
|
||||
return [(system_dir_view(profile, d), False) for d in directories]
|
||||
standalone = profile
|
||||
root = standalone_root_for(emu_name, profile, standalone_cores)
|
||||
if root:
|
||||
|
||||
@@ -41,6 +41,7 @@ from common import (
|
||||
require_yaml,
|
||||
resolve_local_file,
|
||||
platform_layouts,
|
||||
system_dir_path,
|
||||
standalone_set,
|
||||
select_emulator_profiles,
|
||||
)
|
||||
@@ -652,7 +653,7 @@ def slot_key(f: dict, standalone: bool = False) -> tuple:
|
||||
return (
|
||||
fname,
|
||||
f.get("archive"),
|
||||
where or fname,
|
||||
system_dir_path(f, where or fname),
|
||||
f.get("system"),
|
||||
f.get("variant_group"),
|
||||
f.get("mode", "both"),
|
||||
@@ -727,7 +728,7 @@ def _cross_reference_profile(
|
||||
if is_standalone:
|
||||
effective_path = f.get("standalone_path") or f.get("path") or fname
|
||||
else:
|
||||
effective_path = f.get("path") or fname
|
||||
effective_path = system_dir_path(f, f.get("path") or fname)
|
||||
slot = slot_key(f, is_standalone)
|
||||
seen_key = slot
|
||||
if len(layouts) > 1:
|
||||
|
||||
+81
-42
@@ -15,7 +15,7 @@ from common import load_emulator_profiles
|
||||
from common import resolution_is_hash_exact
|
||||
from common import resolve_local_file
|
||||
from common import resolve_platform_cores
|
||||
from common import platform_layouts
|
||||
from common import core_system_dirs, platform_layouts, system_dir_path
|
||||
from common import standalone_set as platform_standalone_set
|
||||
from common import sanitize_pack_path
|
||||
from common import size_fits
|
||||
@@ -331,7 +331,9 @@ def _extra_dest(u: dict) -> str:
|
||||
the file's name after it ("cafeLibs/" + "snd_user.rpl").
|
||||
"""
|
||||
archive = u.get("archive")
|
||||
raw = archive if archive else (u.get("path") or u["name"])
|
||||
# An archive goes at the root of the core's system directory, which a
|
||||
# platform handing each core its own folder writes in the path.
|
||||
raw = (u.get("path") or archive) if archive else (u.get("path") or u["name"])
|
||||
return f"{raw}{u['name']}" if raw.endswith("/") else raw
|
||||
|
||||
|
||||
@@ -479,6 +481,8 @@ def _agnostic_scan_extras(
|
||||
seed = f.get("path") or ""
|
||||
if standalone:
|
||||
seed = f.get("standalone_path") or seed
|
||||
else:
|
||||
seed = system_dir_path(f, seed or fname)
|
||||
folder = seed.rsplit("/", 1)[0] if "/" in seed else ""
|
||||
dest = f"{folder}/{scan_name}" if folder else scan_name
|
||||
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest
|
||||
@@ -727,6 +731,7 @@ def _archive_prefix_extras(
|
||||
extras_prefix: str,
|
||||
by_name: dict,
|
||||
taken: dict[str, dict] | None = None,
|
||||
standalone_set: set[str] | None = None,
|
||||
) -> list[dict]:
|
||||
"""A second copy of an archive under the subdirectory a core reads.
|
||||
|
||||
@@ -744,45 +749,75 @@ def _archive_prefix_extras(
|
||||
continue
|
||||
if emu_name not in relevant:
|
||||
continue
|
||||
prefix = profile.get("archive_prefix", "")
|
||||
if not prefix:
|
||||
# A platform that hands each core its own system directory moves the
|
||||
# subfolder with it, once per directory.
|
||||
views = [profile]
|
||||
if core_system_dirs(emu_name, profile, standalone_set or set()) is not None:
|
||||
views = [
|
||||
view for view, standalone
|
||||
in platform_layouts(emu_name, profile, standalone_set or set())
|
||||
if not standalone
|
||||
]
|
||||
for view in views:
|
||||
extras.extend(_archive_prefix_copies(
|
||||
emu_name, view, covered_names, seen_dests, extras_prefix,
|
||||
by_name, taken,
|
||||
))
|
||||
return extras
|
||||
|
||||
|
||||
def _archive_prefix_copies(
|
||||
emu_name: str,
|
||||
profile: dict,
|
||||
covered_names: set,
|
||||
seen_dests: set,
|
||||
extras_prefix: str,
|
||||
by_name: dict,
|
||||
taken: dict[str, dict] | None,
|
||||
) -> list[dict]:
|
||||
"""The prefixed copies of one layout's archives."""
|
||||
extras: list[dict] = []
|
||||
prefix = profile.get("archive_prefix", "")
|
||||
if not prefix:
|
||||
return extras
|
||||
profile_archives: dict[str, dict] = {}
|
||||
for f in profile.get("files", []):
|
||||
archive = f.get("archive", "")
|
||||
if archive:
|
||||
profile_archives.setdefault(archive, f)
|
||||
for archive_name, archive_entry in sorted(profile_archives.items()):
|
||||
if archive_name not in covered_names:
|
||||
continue
|
||||
profile_archives: dict[str, dict] = {}
|
||||
for f in profile.get("files", []):
|
||||
archive = f.get("archive", "")
|
||||
if archive:
|
||||
profile_archives.setdefault(archive, f)
|
||||
for archive_name, archive_entry in sorted(profile_archives.items()):
|
||||
if archive_name not in covered_names:
|
||||
continue
|
||||
dest = f"{prefix}/{archive_name}"
|
||||
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest
|
||||
if full_dest in seen_dests and (taken is None or full_dest not in taken):
|
||||
continue
|
||||
if not by_name.get(archive_name):
|
||||
continue
|
||||
extra = {
|
||||
"name": archive_name,
|
||||
"destination": dest,
|
||||
"required": True,
|
||||
"hle_fallback": False,
|
||||
"source_emulator": profile.get("emulator", emu_name),
|
||||
"source_profile": emu_name,
|
||||
# Keyed as the gap report keys an archive: name and path alike.
|
||||
"source_name": archive_name,
|
||||
"source_path": archive_name,
|
||||
"category": archive_entry.get("category", "bios"),
|
||||
"source_system": archive_entry.get("system"),
|
||||
"source_systems": entry_system_ids(archive_entry, profile),
|
||||
"region": archive_entry.get("region"),
|
||||
"variant_group": archive_entry.get("variant_group"),
|
||||
}
|
||||
members = archive_members(archive_name, profile.get("files", []))
|
||||
if members:
|
||||
extra["members"] = members
|
||||
extra["claim_order"] = (3, 0, emu_name)
|
||||
if _take(extra, full_dest, seen_dests, taken):
|
||||
extras.append(extra)
|
||||
dest = f"{prefix}/{archive_name}"
|
||||
full_dest = f"{extras_prefix}/{dest}" if extras_prefix else dest
|
||||
if full_dest in seen_dests and (taken is None or full_dest not in taken):
|
||||
continue
|
||||
if not by_name.get(archive_name):
|
||||
continue
|
||||
extra = {
|
||||
"name": archive_name,
|
||||
"destination": dest,
|
||||
"required": True,
|
||||
"hle_fallback": False,
|
||||
"source_emulator": profile.get("emulator", emu_name),
|
||||
"source_profile": emu_name,
|
||||
# Keyed as the gap report keys an archive: name and path alike.
|
||||
"source_name": archive_name,
|
||||
"source_path": system_dir_path(archive_entry, archive_name),
|
||||
"category": archive_entry.get("category", "bios"),
|
||||
"source_system": archive_entry.get("system"),
|
||||
"source_systems": entry_system_ids(archive_entry, profile),
|
||||
"region": archive_entry.get("region"),
|
||||
"variant_group": archive_entry.get("variant_group"),
|
||||
}
|
||||
members = archive_members(archive_name, profile.get("files", []))
|
||||
if members:
|
||||
extra["members"] = members
|
||||
if archive_entry.get("system_dir"):
|
||||
extra["system_dir"] = archive_entry["system_dir"]
|
||||
extra["claim_order"] = (3, 0, emu_name)
|
||||
if _take(extra, full_dest, seen_dests, taken):
|
||||
extras.append(extra)
|
||||
|
||||
return extras
|
||||
|
||||
@@ -1046,7 +1081,7 @@ def _collect_emulator_extras(
|
||||
if is_standalone:
|
||||
raw = f.get("standalone_path") or f.get("path") or fname
|
||||
else:
|
||||
raw = f.get("path") or fname
|
||||
raw = system_dir_path(f, f.get("path") or fname)
|
||||
dest = f"{raw}{fname}" if raw.endswith("/") else raw
|
||||
# A bare name is a destination too: the core reads it at the root
|
||||
# of the directory core files go to, which the platform may not
|
||||
@@ -1087,6 +1122,8 @@ def _collect_emulator_extras(
|
||||
extra[identity] = f[identity]
|
||||
if f.get("contents"):
|
||||
extra["contents"] = f["contents"]
|
||||
if f.get("system_dir"):
|
||||
extra["system_dir"] = f["system_dir"]
|
||||
extra["claim_order"] = (2, 0, emu_name)
|
||||
if _take(extra, full_dest, seen_dests, taken):
|
||||
extras.append(extra)
|
||||
@@ -1094,7 +1131,7 @@ def _collect_emulator_extras(
|
||||
extras.extend(
|
||||
_archive_prefix_extras(
|
||||
profiles, relevant, covered_names, seen_dests, extras_prefix,
|
||||
by_name, taken,
|
||||
by_name, taken, standalone_set,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1211,6 +1248,8 @@ def _report_extra(u: dict, name: str, dest: str) -> dict:
|
||||
}
|
||||
if u.get("members"):
|
||||
extra["members"] = u["members"]
|
||||
if u.get("system_dir"):
|
||||
extra["system_dir"] = u["system_dir"]
|
||||
# The archive layout the core opens ranks same-named copies by what
|
||||
# they hold. Without it the first copy indexed won.
|
||||
if u.get("contents"):
|
||||
|
||||
+8
-3
@@ -35,6 +35,7 @@ from common import (
|
||||
resolve_platform_cores,
|
||||
platform_layouts,
|
||||
standalone_set as platform_standalone_set,
|
||||
system_dir_path,
|
||||
variant_base_name,
|
||||
)
|
||||
from packresolve import preferred_entries
|
||||
@@ -227,7 +228,7 @@ def profile_claims(
|
||||
dest = (
|
||||
(entry.get("standalone_path") or entry.get("path"))
|
||||
if is_standalone
|
||||
else entry.get("path")
|
||||
else system_dir_path(entry, entry.get("path") or entry.get("name") or "")
|
||||
) or entry.get("name") or ""
|
||||
if not dest:
|
||||
continue
|
||||
@@ -299,8 +300,12 @@ def _archive_claims(
|
||||
members = entry.get("members")
|
||||
if not members:
|
||||
continue
|
||||
system = next(f.get("system") for f in files if f.get("archive") == archive)
|
||||
for dest in (archive, f"{prefix}/{archive}") if prefix else (archive,):
|
||||
member = next(f for f in files if f.get("archive") == archive)
|
||||
system = member.get("system")
|
||||
if member.get("system_dir"):
|
||||
entry = {**entry, "system_dir": member["system_dir"]}
|
||||
root = system_dir_path(entry, archive)
|
||||
for dest in (root, f"{prefix}/{archive}") if prefix else (root,):
|
||||
if placement is None:
|
||||
full = f"{base_dest}/{dest}" if base_dest else dest
|
||||
else:
|
||||
|
||||
+8
-2
@@ -76,6 +76,7 @@ from common import (
|
||||
entry_system_ids,
|
||||
sanitize_pack_path,
|
||||
select_emulator_profiles,
|
||||
system_dir_path,
|
||||
)
|
||||
|
||||
yaml = require_yaml()
|
||||
@@ -576,7 +577,7 @@ def find_undeclared_files(
|
||||
if is_standalone:
|
||||
dest = f.get("standalone_path") or f.get("path") or fname
|
||||
else:
|
||||
dest = f.get("path") or fname
|
||||
dest = system_dir_path(f, f.get("path") or fname)
|
||||
raw_regions = f.get("region") or []
|
||||
region_key = tuple(
|
||||
str(value) for value in (
|
||||
@@ -615,6 +616,7 @@ def find_undeclared_files(
|
||||
if archive:
|
||||
archive_key = (
|
||||
archive,
|
||||
f.get("system_dir"),
|
||||
f.get("system"),
|
||||
f.get("variant_group"),
|
||||
region_key,
|
||||
@@ -634,7 +636,7 @@ def find_undeclared_files(
|
||||
"variant_group": f.get("variant_group"),
|
||||
"name": archive,
|
||||
"archive": archive,
|
||||
"path": archive,
|
||||
"path": system_dir_path(f, archive),
|
||||
"required": False,
|
||||
"hle_fallback": False,
|
||||
"category": f.get("category", "bios"),
|
||||
@@ -647,6 +649,8 @@ def find_undeclared_files(
|
||||
"archive_required_count": 0,
|
||||
"members": archive_members(archive, profile.get("files", [])),
|
||||
}
|
||||
if f.get("system_dir"):
|
||||
archive_entries[archive_key]["system_dir"] = f["system_dir"]
|
||||
entry = archive_entries[archive_key]
|
||||
entry["archive_file_count"] += 1
|
||||
if f.get("required", False):
|
||||
@@ -738,6 +742,8 @@ def find_undeclared_files(
|
||||
}
|
||||
if f.get("contents"):
|
||||
entry["contents"] = f["contents"]
|
||||
if f.get("system_dir"):
|
||||
entry["system_dir"] = f["system_dir"]
|
||||
members = with_archive_members(f, profile.get("files", [])).get("members")
|
||||
if members:
|
||||
entry["members"] = members
|
||||
|
||||
Reference in new issue
Block a user