mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-06 01:53:56 -06:00
feat: carry the data trees a platform's cores read
A platform pack carried only the trees its own systems declared, so for_platforms reached RetroArch, Lakka and RetroPie alone and every other platform running blueMSX, FBNeo or Dolphin got no tree. The pack, the install list and the pipeline's tree members now add the trees of the libretro cores the platform runs, below the system directory and build folder the core reads, unless the platform installs or never opens them. A platform entry the manifest resolved into a carried tree becomes that tree's member, which the installer can fetch: AmberELEC and EmuELEC no longer hold 192 blueMSX machine configs back. stonesoup reads its tree beside the content and pcee2 compiles its resources in, so neither tree reaches a BIOS pack.
This commit is contained in:
1 parent
736dadd7e9
commit
54a3876a8b
11 files changed
+428
-70
No files matched your search
+2
-1
@@ -23,7 +23,8 @@ resources_directory: "pcsx2/resources/"
|
||||
data_directories:
|
||||
- ref: pcsx2-resources
|
||||
destination: "pcsx2/resources"
|
||||
source_ref: "pcee2-libretro/Libretro.cpp:543-571, pcsx2/Pcsx2Config.cpp:2417-2429"
|
||||
bundled: true
|
||||
source_ref: "pcee2-libretro/Libretro.cpp:543-571, pcsx2/Pcsx2Config.cpp:2417-2429, cmake/EmbedResources.cmake:19-31, 46-62"
|
||||
|
||||
notes: |
|
||||
libretro port of current PCSX2, merged up to upstream ab0b401f (v2.9.138,
|
||||
|
||||
@@ -42,7 +42,8 @@ notes: |
|
||||
data_directories:
|
||||
- ref: stonesoup
|
||||
installed_on: [recalbox]
|
||||
source_ref: "crawl-ref/source/files.cc:375-458"
|
||||
load_from: content_dir
|
||||
source_ref: "crawl-ref/libretro/libretro.cc:178-197, crawl-ref/source/files.cc:375-458"
|
||||
|
||||
files: []
|
||||
# no BIOS/firmware, game data loaded via data_directories
|
||||
@@ -390,6 +390,21 @@
|
||||
"items": {"type": "string"},
|
||||
"description": "Platforms (registry keys) whose launcher points the emulator at other files, so this entry is never opened there."
|
||||
},
|
||||
"load_from": {
|
||||
"description": "Where the emulator reads the tree when not below the system directory, keyed by build when the builds differ (content_dir: beside the content). A platform pack carries only a tree read from the system directory.",
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"libretro": {"type": "string"},
|
||||
"standalone": {"type": "string"}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"source_ref": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -333,10 +333,10 @@ def write_small_list(path: Path) -> None:
|
||||
def _child(spec: dict) -> int:
|
||||
"""install.main() as the sample runs it, in its own process. Only the
|
||||
transport and the machine are stood in for: no connection leaves the
|
||||
loopback, detection finds the card folder the spec names, a file of a
|
||||
landed run reports its bytes written without fetching them, and a held
|
||||
run finds every file in place. The counts, sizes and words are the
|
||||
installer's own."""
|
||||
loopback, detection finds the card folder the spec names, a file or a
|
||||
data tree of a landed run reports its bytes written without fetching
|
||||
them, and a held run finds every file and tree in place. The counts,
|
||||
sizes and words are the installer's own."""
|
||||
import concurrent.futures # noqa: PLC0415
|
||||
import socket # noqa: PLC0415
|
||||
from unittest import mock # noqa: PLC0415
|
||||
@@ -362,6 +362,13 @@ def _child(spec: dict) -> int:
|
||||
def held(files: list[dict], _bios: Path, **_kwargs: object) -> tuple:
|
||||
return [], list(files), []
|
||||
|
||||
def tree_landed(entry: dict, _bios: Path, state: object = None) -> dict:
|
||||
return {"present": 0, "installed": len(entry["files"]), "changed": 0,
|
||||
"absent": 0, "error": "", "not_kept": []}
|
||||
|
||||
def tree_held(entry: dict, bios: Path, remember: bool = False) -> tuple:
|
||||
return bios / entry.get("dest", ""), {}, len(entry["files"])
|
||||
|
||||
patches = [
|
||||
mock.patch.object(socket, "create_connection", loopback_only),
|
||||
# One worker and the submission order: the progress lines of a log.
|
||||
@@ -376,8 +383,10 @@ def _child(spec: dict) -> int:
|
||||
]
|
||||
if spec.get("landed"):
|
||||
patches.append(mock.patch.object(_install, "_download_one", landed))
|
||||
patches.append(mock.patch.object(_install, "install_data_directory", tree_landed))
|
||||
if spec.get("held"):
|
||||
patches.append(mock.patch.object(_install, "check_local", held))
|
||||
patches.append(mock.patch.object(_install, "data_directory_state", tree_held))
|
||||
with contextlib.ExitStack() as stack:
|
||||
for patch in patches:
|
||||
stack.enter_context(patch)
|
||||
|
||||
+204
-56
@@ -69,9 +69,14 @@ from common import (
|
||||
MANUFACTURER_PREFIXES,
|
||||
require_yaml,
|
||||
ProfileSelectionError,
|
||||
core_system_dirs,
|
||||
declared_root,
|
||||
entry_serves_systems,
|
||||
entry_system_ids,
|
||||
platform_layouts,
|
||||
sanitize_pack_path,
|
||||
select_emulator_profiles,
|
||||
standalone_set,
|
||||
system_core_reach,
|
||||
upstream_profile_index,
|
||||
yaml_load,
|
||||
@@ -100,6 +105,7 @@ from validation import (
|
||||
_build_validation_index,
|
||||
_parse_validation,
|
||||
deliverable,
|
||||
read_from_system_dir,
|
||||
with_archive_members,
|
||||
extra_alternatives,
|
||||
)
|
||||
@@ -376,6 +382,97 @@ def platform_list_version(config: dict, platform: str) -> str:
|
||||
PACK_DOCUMENTS = ("README.txt", "manifest.json")
|
||||
|
||||
|
||||
def core_data_trees(
|
||||
config: dict,
|
||||
profiles: dict[str, dict],
|
||||
platform_name: str,
|
||||
data_registry: dict | None,
|
||||
target_cores: set[str] | None = None,
|
||||
) -> list[dict]:
|
||||
"""The data directories the platform's cores read, as tree references.
|
||||
|
||||
A profile's ``data_directories`` reach the pack of every platform that
|
||||
runs the core as a libretro core, below the system directory it hands
|
||||
the core (and the build's own folder, ``pack_structure``), as the
|
||||
emulator pack lays them out. Left out: a tree the platform's package
|
||||
installs, for this core or another reading the same tree
|
||||
(``installed_data_refs``), one the build bundles or the platform never
|
||||
opens (``deliverable``), and one whose registry entry names the
|
||||
platforms it serves (``for_platforms``) without this one. A standalone build reads its data below its own folder, which a
|
||||
tree's destination does not describe, so its trees stay out, as does a
|
||||
tree read beside the content (``load_from``).
|
||||
|
||||
Each reference carries the profile's systems (``source_systems``, as a
|
||||
core extra does), which --system and --split narrow on.
|
||||
"""
|
||||
if not data_registry:
|
||||
return []
|
||||
installed = installed_data_refs(platform_name)
|
||||
handed = standalone_set(config)
|
||||
# Relative to base_destination, which the members are written under. A
|
||||
# platform with none writes its BIOS folder into every destination, and
|
||||
# the trees go there as the core extras do.
|
||||
root = declared_root(config)
|
||||
trees: list[dict] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
for core in sorted(_platform_cores(config, profiles, target_cores)):
|
||||
profile = profiles[core]
|
||||
if all(standalone for _view, standalone in platform_layouts(core, profile, handed)):
|
||||
continue
|
||||
declared = [t for t in profile.get("data_directories") or [] if isinstance(t, dict)]
|
||||
directories = core_system_dirs(core, profile, handed)
|
||||
structure = str((profile.get("pack_structure") or {}).get("libretro") or "")
|
||||
for tree in deliverable(declared, False, platform_name):
|
||||
if not read_from_system_dir(tree):
|
||||
continue
|
||||
ref = str(tree.get("ref") or "")
|
||||
entry = data_registry.get(ref)
|
||||
if entry is None or ref in installed:
|
||||
continue
|
||||
allowed = entry.get("for_platforms")
|
||||
if allowed and platform_name not in allowed:
|
||||
continue
|
||||
for directory in [""] if directories is None else directories:
|
||||
destination = prefixed_destination("/".join(
|
||||
part.strip("/")
|
||||
for part in (directory, structure, str(tree.get("destination") or ""))
|
||||
if part.strip("/")
|
||||
), root) or root
|
||||
if (ref, destination) in seen:
|
||||
continue
|
||||
seen.add((ref, destination))
|
||||
trees.append({
|
||||
"ref": ref,
|
||||
"destination": destination,
|
||||
"required": tree.get("required"),
|
||||
"core": core,
|
||||
"source_systems": entry_system_ids(tree, profile),
|
||||
})
|
||||
return trees
|
||||
|
||||
|
||||
def _in_data_cache(path: str, data_registry: dict | None) -> bool:
|
||||
"""Whether a resolved file lies in one of the data directory caches."""
|
||||
real = os.path.realpath(path)
|
||||
return any(
|
||||
real.startswith(os.path.realpath(entry["local_cache"]) + os.sep)
|
||||
for entry in (data_registry or {}).values()
|
||||
if entry.get("local_cache")
|
||||
)
|
||||
|
||||
|
||||
def _tree_references(
|
||||
pack_systems: dict, core_trees: Iterable[dict],
|
||||
) -> Iterator[tuple[tuple[str, ...], dict]]:
|
||||
"""(systems, reference) for each tree a pack carries: those its systems
|
||||
declare, then those its cores read."""
|
||||
for sys_id, system in sorted(pack_systems.items()):
|
||||
for dd in system.get("data_directories", []):
|
||||
yield (sys_id,), dd
|
||||
for tree in core_trees:
|
||||
yield tuple(tree["source_systems"]), tree
|
||||
|
||||
|
||||
def _data_directory_members(
|
||||
pack_systems: dict,
|
||||
data_registry: dict | None,
|
||||
@@ -387,12 +484,20 @@ def _data_directory_members(
|
||||
seen_parents: set,
|
||||
refs_out: dict | None = None,
|
||||
required_only: bool = False,
|
||||
core_trees: Iterable[dict] = (),
|
||||
reclaim: dict[str, str] | None = None,
|
||||
) -> Iterator[tuple[str, str]]:
|
||||
"""Yield (source, destination) for the cached data directories a
|
||||
platform's systems declare.
|
||||
platform's systems declare, then those its cores read (``core_trees``,
|
||||
from core_data_trees).
|
||||
|
||||
``reclaim`` maps a destination already taken to the cache file it was
|
||||
resolved to: the manifest found the platform's entry there and cannot
|
||||
list it as a file, so the tree yields it once more and the installer
|
||||
gets it from the tree's archive.
|
||||
|
||||
``refs_out``, when given, maps each yielded destination to
|
||||
(registry key, system id, directory destination), for the manifest.
|
||||
(registry key, system ids, directory destination), for the manifest.
|
||||
|
||||
These are not BIOS and carry no hash of their own: they are whole
|
||||
trees an emulator reads, refreshed from upstream, so what ships is
|
||||
@@ -402,54 +507,61 @@ def _data_directory_members(
|
||||
core runs without, as it leaves out such a file.
|
||||
"""
|
||||
# Data directories from _data_dirs.yml
|
||||
for sys_id, system in sorted(pack_systems.items()):
|
||||
for dd in system.get("data_directories", []):
|
||||
ref_key = dd.get("ref", "")
|
||||
if not ref_key or not data_registry or ref_key not in data_registry:
|
||||
continue
|
||||
if required_only and dd.get("required") is False:
|
||||
continue
|
||||
if ref_key in installed_data_refs(platform_name):
|
||||
continue
|
||||
entry = data_registry[ref_key]
|
||||
allowed = entry.get("for_platforms")
|
||||
if allowed and platform_name not in allowed:
|
||||
continue
|
||||
local_path = entry.get("local_cache", "")
|
||||
if not local_path or not os.path.isdir(local_path):
|
||||
print(
|
||||
f" WARNING: data directory '{ref_key}' not cached at {local_path} (run refresh_data_dirs.py)"
|
||||
)
|
||||
_MISSING_DATA_DIRS.add(ref_key)
|
||||
continue
|
||||
dd_dest = dd.get("destination", "")
|
||||
if base_dest and dd_dest:
|
||||
dd_prefix = f"{base_dest}/{dd_dest}"
|
||||
elif base_dest:
|
||||
dd_prefix = base_dest
|
||||
else:
|
||||
dd_prefix = dd_dest
|
||||
# Held while the caller writes what is yielded: a refresh swapping
|
||||
# the tree waits until the walk is done.
|
||||
with cache_lock(local_path, shared=True):
|
||||
for root, _dirs, filenames in os.walk(local_path):
|
||||
for fname in filenames:
|
||||
src = os.path.join(root, fname)
|
||||
rel = os.path.relpath(src, local_path)
|
||||
full = f"{dd_prefix}/{rel}"
|
||||
if full in seen_destinations or (
|
||||
full.lower() in seen_lower and case_insensitive
|
||||
):
|
||||
continue
|
||||
if _has_path_conflict(full, seen_destinations, seen_parents):
|
||||
continue
|
||||
seen_destinations.add(full)
|
||||
_register_path(full, seen_destinations, seen_parents)
|
||||
if case_insensitive:
|
||||
seen_lower.add(full.lower())
|
||||
for systems, dd in _tree_references(pack_systems, core_trees):
|
||||
ref_key = dd.get("ref", "")
|
||||
if not ref_key or not data_registry or ref_key not in data_registry:
|
||||
continue
|
||||
if required_only and dd.get("required") is False:
|
||||
continue
|
||||
if ref_key in installed_data_refs(platform_name):
|
||||
continue
|
||||
entry = data_registry[ref_key]
|
||||
allowed = entry.get("for_platforms")
|
||||
if allowed and platform_name not in allowed:
|
||||
continue
|
||||
local_path = entry.get("local_cache", "")
|
||||
if not local_path or not os.path.isdir(local_path):
|
||||
print(
|
||||
f" WARNING: data directory '{ref_key}' not cached at {local_path} (run refresh_data_dirs.py)"
|
||||
)
|
||||
_MISSING_DATA_DIRS.add(ref_key)
|
||||
continue
|
||||
dd_dest = dd.get("destination", "")
|
||||
if base_dest and dd_dest:
|
||||
dd_prefix = f"{base_dest}/{dd_dest}"
|
||||
elif base_dest:
|
||||
dd_prefix = base_dest
|
||||
else:
|
||||
dd_prefix = dd_dest
|
||||
# Held while the caller writes what is yielded: a refresh swapping
|
||||
# the tree waits until the walk is done.
|
||||
with cache_lock(local_path, shared=True):
|
||||
for root, _dirs, filenames in os.walk(local_path):
|
||||
for fname in filenames:
|
||||
src = os.path.join(root, fname)
|
||||
rel = os.path.relpath(src, local_path)
|
||||
full = f"{dd_prefix}/{rel}" if dd_prefix else rel
|
||||
if reclaim and full in reclaim and (
|
||||
os.path.normpath(reclaim[full]) == os.path.normpath(src)
|
||||
):
|
||||
del reclaim[full]
|
||||
if refs_out is not None:
|
||||
refs_out[full] = (ref_key, sys_id, dd_prefix)
|
||||
refs_out[full] = (ref_key, systems, dd_prefix)
|
||||
yield src, full
|
||||
continue
|
||||
if full in seen_destinations or (
|
||||
full.lower() in seen_lower and case_insensitive
|
||||
):
|
||||
continue
|
||||
if _has_path_conflict(full, seen_destinations, seen_parents):
|
||||
continue
|
||||
seen_destinations.add(full)
|
||||
_register_path(full, seen_destinations, seen_parents)
|
||||
if case_insensitive:
|
||||
seen_lower.add(full.lower())
|
||||
if refs_out is not None:
|
||||
refs_out[full] = (ref_key, systems, dd_prefix)
|
||||
yield src, full
|
||||
|
||||
|
||||
def _pack_data_directories(
|
||||
@@ -464,13 +576,14 @@ def _pack_data_directories(
|
||||
seen_lower: set,
|
||||
seen_parents: set,
|
||||
required_only: bool = False,
|
||||
core_trees: Iterable[dict] = (),
|
||||
) -> int:
|
||||
"""Add the data directories to the pack. Returns how many files went in."""
|
||||
added = 0
|
||||
for src, full in _data_directory_members(
|
||||
pack_systems, data_registry, platform_name, base_dest,
|
||||
case_insensitive, seen_destinations, seen_lower, seen_parents,
|
||||
required_only=required_only,
|
||||
required_only=required_only, core_trees=core_trees,
|
||||
):
|
||||
_add_pack_member(zf, src, _flat(full, base_dest, flatten))
|
||||
added += 1
|
||||
@@ -1169,10 +1282,23 @@ def generate_pack(
|
||||
save_count += 1
|
||||
total_files += 1
|
||||
|
||||
# The trees the cores read follow them as their extras do: by the
|
||||
# systems asked for, and those of no system in the part of their own.
|
||||
core_trees = (
|
||||
[] if source == "platform"
|
||||
else core_data_trees(
|
||||
config, emu_profiles, platform_name, data_registry, target_cores
|
||||
)
|
||||
)
|
||||
if extras_only:
|
||||
owned = list(platform_scope_systems(config, emu_profiles, target_cores))
|
||||
core_trees = [t for t in core_trees if not _extras_for_systems([t], owned)]
|
||||
elif system_filter:
|
||||
core_trees = _extras_for_systems(core_trees, system_filter)
|
||||
total_files += _pack_data_directories(
|
||||
zf, pack_systems, data_registry, platform_name, base_dest,
|
||||
flatten, case_insensitive, seen_destinations, seen_lower,
|
||||
seen_parents, required_only=required_only,
|
||||
seen_parents, required_only=required_only, core_trees=core_trees,
|
||||
)
|
||||
|
||||
# README.txt for users: personalized step-by-step per platform
|
||||
@@ -4334,6 +4460,9 @@ def generate_manifest(
|
||||
# Sizes of the files the pack carries and the installer cannot fetch: the
|
||||
# collection does not index them, a data directory cache answered.
|
||||
pack_only_sizes: list[int] = []
|
||||
# Platform entries resolved into a data directory cache: destination ->
|
||||
# (cache file, size). Settled once the trees are walked.
|
||||
cache_held: dict[str, tuple[str, int]] = {}
|
||||
rename_notes: set[str] = set()
|
||||
slot_overrides = slots.pack_overrides(
|
||||
config, emu_profiles, db, zip_contents, data_registry,
|
||||
@@ -4564,7 +4693,10 @@ def generate_manifest(
|
||||
if entry is None:
|
||||
record_omission(full_dest, file_entry, sys_id, "not_found", None)
|
||||
if file_size and full_dest not in seen_destinations:
|
||||
pack_only_sizes.append(file_size)
|
||||
if _in_data_cache(local_path, data_registry):
|
||||
cache_held[full_dest] = (local_path, file_size)
|
||||
else:
|
||||
pack_only_sizes.append(file_size)
|
||||
seen_destinations.add(dedup_key)
|
||||
_register_path(dedup_key, seen_destinations, seen_parents)
|
||||
if case_insensitive:
|
||||
@@ -4643,12 +4775,28 @@ def generate_manifest(
|
||||
# Phase 3: data directories. They are not files of the collection: the
|
||||
# installer fetches each from its source archive and keeps the members
|
||||
# whose hash the list below records, the same tree the pack carries.
|
||||
data_refs: dict[str, tuple[str, str, str]] = {}
|
||||
data_refs: dict[str, tuple[str, tuple[str, ...], str]] = {}
|
||||
reclaim = {full: path for full, (path, _size) in cache_held.items()}
|
||||
data_members = list(_data_directory_members(
|
||||
pack_systems, data_registry, platform_name, base_dest,
|
||||
case_insensitive, seen_destinations, seen_lower, seen_parents,
|
||||
data_refs, required_only=required_only,
|
||||
core_trees=(
|
||||
[] if source == "platform"
|
||||
else core_data_trees(
|
||||
config, emu_profiles, platform_name, data_registry, target_cores
|
||||
)
|
||||
),
|
||||
reclaim=reclaim,
|
||||
))
|
||||
# A platform entry found in a tree the pack carries is that tree's member:
|
||||
# the installer rebuilds it from the tree's archive. One found in a tree
|
||||
# the pack does not carry stays omitted, held by the pack alone.
|
||||
for full_dest, (_path, size) in cache_held.items():
|
||||
if full_dest in reclaim:
|
||||
pack_only_sizes.append(size)
|
||||
else:
|
||||
omitted_by_destination.pop(full_dest, None)
|
||||
data_sizes = pack_only_sizes + [os.path.getsize(src) for src, _dest in data_members]
|
||||
data_directories = _manifest_data_directories(
|
||||
data_members, data_refs, data_registry or {}, base_dest
|
||||
@@ -4738,7 +4886,7 @@ def generate_manifest(
|
||||
|
||||
def _manifest_data_directories(
|
||||
members: list[tuple[str, str]],
|
||||
refs: dict[str, tuple[str, str, str]],
|
||||
refs: dict[str, tuple[str, tuple[str, ...], str]],
|
||||
registry: dict,
|
||||
base_dest: str,
|
||||
) -> list[dict]:
|
||||
@@ -4752,7 +4900,7 @@ def _manifest_data_directories(
|
||||
"""
|
||||
grouped: dict[tuple[str, str], dict] = {}
|
||||
for src, full in members:
|
||||
ref, system, prefix = refs[full]
|
||||
ref, systems, prefix = refs[full]
|
||||
entry = registry.get(ref, {})
|
||||
key = (ref, prefix)
|
||||
group = grouped.get(key)
|
||||
@@ -4775,8 +4923,7 @@ def _manifest_data_directories(
|
||||
group["source_path"] = str(entry.get("source_path", "")).format(version=version)
|
||||
if entry.get("exclude"):
|
||||
group["exclude"] = [str(e) for e in entry["exclude"]]
|
||||
if system not in group["systems"]:
|
||||
group["systems"].append(system)
|
||||
group["systems"].extend(s for s in systems if s not in group["systems"])
|
||||
rel = full[len(prefix):].lstrip("/") if prefix else full
|
||||
hashes = compute_hashes(src, frozenset({"sha1"}))
|
||||
group["files"][rel] = {"sha1": hashes["sha1"], "size": os.path.getsize(src)}
|
||||
@@ -5222,6 +5369,7 @@ from packpaths import ( # noqa: E402,F401
|
||||
from packextras import ( # noqa: E402,F401
|
||||
ExtraResolver,
|
||||
extra_full_destination,
|
||||
prefixed_destination,
|
||||
save_entry_fields,
|
||||
save_manifest_dest,
|
||||
_detect_extras_prefix,
|
||||
|
||||
+10
-3
@@ -433,19 +433,26 @@ def parse_pack_paths(output: str) -> dict[str, str]:
|
||||
_PACK_NOTES = re.compile(r"(^README\.txt$|^manifest\.json$|(^|/)(INSTRUCTIONS|RENAMED)_[^/]*\.txt$)")
|
||||
|
||||
|
||||
def _tree_members(platform: str, platforms_dir: str) -> dict[str, int]:
|
||||
def _tree_members(
|
||||
platform: str, platforms_dir: str, emulators_dir: str = "emulators",
|
||||
) -> dict[str, int]:
|
||||
"""Archive path -> size of every file the platform's data trees can add."""
|
||||
import generate_pack # noqa: PLC0415
|
||||
from common import load_platform_config # noqa: PLC0415
|
||||
from common import load_emulator_profiles, load_platform_config # noqa: PLC0415
|
||||
from packpaths import _flat # noqa: PLC0415
|
||||
|
||||
config = load_platform_config(platform, platforms_dir)
|
||||
base_dest = config.get("base_destination", "")
|
||||
registry = load_data_dir_registry(platforms_dir)
|
||||
core_trees = generate_pack.core_data_trees(
|
||||
config, load_emulator_profiles(emulators_dir), platform, registry,
|
||||
)
|
||||
members: dict[str, int] = {}
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
for src, full in generate_pack._data_directory_members(
|
||||
config.get("systems", {}), load_data_dir_registry(platforms_dir),
|
||||
config.get("systems", {}), registry,
|
||||
platform, base_dest, False, set(), set(), set(),
|
||||
core_trees=core_trees,
|
||||
):
|
||||
members[_flat(full, base_dest, True)] = os.path.getsize(src)
|
||||
return members
|
||||
|
||||
@@ -5544,7 +5544,10 @@ class DocumentedExamplesHoldTogether(unittest.TestCase):
|
||||
page = (ROOT / "wiki" / "installer.md").read_text(encoding="utf-8")
|
||||
downloading = re.search(r"Downloading (\d+) files", page).group(1)
|
||||
done = re.search(r"Done\. (\d+) files installed", page).group(1)
|
||||
self.assertEqual(done, downloading)
|
||||
# The data trees come after the download, counted in the index.
|
||||
index = page[page.index("Fetching file index for batocera"):]
|
||||
trees = re.search(r"^ .* and (\d+) data files", index, re.M)
|
||||
self.assertEqual(int(done), int(downloading) + (int(trees.group(1)) if trees else 0))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
"""A data directory a core reads reaches every platform that runs the core.
|
||||
|
||||
The registry entries of the buildbot trees named RetroArch, Lakka and
|
||||
RetroPie only, and a platform pack carried only the trees its own systems
|
||||
declared. AmberELEC and EmuELEC run the blueMSX core, whose package copies
|
||||
the .so alone: their packs filled the machine configs by name from the
|
||||
cache, and the install lists left 192 of them out. A platform that runs a
|
||||
reader now gets the tree, or the profile says its package installs it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
import common # noqa: E402
|
||||
import generate_pack as builder # noqa: E402
|
||||
from validation import deliverable, read_from_system_dir # noqa: E402
|
||||
|
||||
PROFILES = {
|
||||
"demo": {
|
||||
"emulator": "Demo",
|
||||
"type": "libretro",
|
||||
"systems": ["demo-system"],
|
||||
"cores": ["demo"],
|
||||
"pack_structure": {"libretro": "Demo"},
|
||||
"data_directories": [
|
||||
{"ref": "shared", "destination": "Sys"},
|
||||
{"ref": "restricted"},
|
||||
{"ref": "installed", "installed_on": ["plat"]},
|
||||
{"ref": "beside", "load_from": "content_dir"},
|
||||
],
|
||||
},
|
||||
"solo": {
|
||||
"emulator": "Solo",
|
||||
"type": "standalone",
|
||||
"systems": ["demo-system"],
|
||||
"cores": ["solo"],
|
||||
"data_directories": [{"ref": "shared"}],
|
||||
},
|
||||
}
|
||||
|
||||
REGISTRY = {
|
||||
"shared": {"local_cache": "data/shared"},
|
||||
"restricted": {"local_cache": "data/restricted", "for_platforms": ["other"]},
|
||||
"installed": {"local_cache": "data/installed"},
|
||||
"beside": {"local_cache": "data/beside"},
|
||||
}
|
||||
|
||||
CONFIG = {
|
||||
"platform": "Plat",
|
||||
"cores": ["demo", "solo"],
|
||||
"standalone_cores": ["solo"],
|
||||
"systems": {"demo-system": {"files": []}},
|
||||
}
|
||||
|
||||
|
||||
class CoreDataTreesTest(unittest.TestCase):
|
||||
def trees(self, platform: str = "plat", registry: dict | None = None) -> list[dict]:
|
||||
return builder.core_data_trees(
|
||||
CONFIG, PROFILES, platform, REGISTRY if registry is None else registry
|
||||
)
|
||||
|
||||
def test_tree_below_the_build_folder(self) -> None:
|
||||
shared = [t for t in self.trees() if t["ref"] == "shared"]
|
||||
self.assertEqual([(t["core"], t["destination"]) for t in shared], [("demo", "Demo/Sys")])
|
||||
self.assertEqual(shared[0]["source_systems"], ["demo-system"])
|
||||
|
||||
def test_for_platforms_names_who_gets_it(self) -> None:
|
||||
self.assertNotIn("restricted", {t["ref"] for t in self.trees()})
|
||||
self.assertIn("restricted", {t["ref"] for t in self.trees("other")})
|
||||
|
||||
def test_installed_tree_stays_out(self) -> None:
|
||||
self.assertNotIn("installed", {t["ref"] for t in self.trees()})
|
||||
self.assertIn("installed", {t["ref"] for t in self.trees("other")})
|
||||
|
||||
def test_tree_read_beside_the_content_stays_out(self) -> None:
|
||||
self.assertNotIn("beside", {t["ref"] for t in self.trees("other")})
|
||||
|
||||
def test_standalone_build_brings_no_tree(self) -> None:
|
||||
self.assertNotIn("solo", {t["core"] for t in self.trees("other")})
|
||||
|
||||
def test_tree_below_the_declared_bios_folder(self) -> None:
|
||||
config = {
|
||||
**CONFIG,
|
||||
"base_destination": "",
|
||||
"systems": {"demo-system": {"files": [{"name": "a.bin", "destination": "bios/a.bin"}]}},
|
||||
}
|
||||
trees = builder.core_data_trees(config, PROFILES, "plat", REGISTRY)
|
||||
self.assertEqual(
|
||||
[t["destination"] for t in trees if t["ref"] == "shared"], ["bios/Demo/Sys"]
|
||||
)
|
||||
|
||||
def test_no_registry_no_tree(self) -> None:
|
||||
self.assertEqual(builder.core_data_trees(CONFIG, PROFILES, "plat", None), [])
|
||||
|
||||
|
||||
class EveryReaderHasTheTreeOrAReason(unittest.TestCase):
|
||||
"""Reads the repository: platforms/_data_dirs.yml against the profiles."""
|
||||
|
||||
def test_for_platforms_leaves_no_reader_without_a_reason(self) -> None:
|
||||
platforms_dir = str(REPO_ROOT / "platforms")
|
||||
registry = common.load_data_dir_registry(platforms_dir)
|
||||
profiles = common.load_emulator_profiles(str(REPO_ROOT / "emulators"))
|
||||
unexplained = []
|
||||
for platform in common.list_registered_platforms(platforms_dir, include_archived=True):
|
||||
config = common.load_platform_config(platform, platforms_dir)
|
||||
handed = common.standalone_set(config)
|
||||
for core in sorted(common.resolve_platform_cores(config, profiles)):
|
||||
profile = profiles[core]
|
||||
if all(standalone for _view, standalone
|
||||
in common.platform_layouts(core, profile, handed)):
|
||||
continue
|
||||
trees = [t for t in profile.get("data_directories") or [] if isinstance(t, dict)]
|
||||
installed = common.installed_data_refs(platform)
|
||||
for tree in deliverable(trees, False, platform):
|
||||
allowed = registry.get(tree.get("ref"), {}).get("for_platforms")
|
||||
if tree.get("ref") in installed or not read_from_system_dir(tree):
|
||||
continue
|
||||
if allowed and platform not in allowed:
|
||||
unexplained.append(f"{tree['ref']} on {platform} ({core})")
|
||||
self.assertEqual(
|
||||
unexplained, [],
|
||||
"these platforms run a core that reads the tree, are not in its "
|
||||
"for_platforms, and no profile says their package installs it "
|
||||
"(installed_on) or that they never open it (unread_on)",
|
||||
)
|
||||
|
||||
|
||||
@unittest.skipUnless(
|
||||
os.path.isdir(REPO_ROOT / "data" / "bluemsx"), "data/bluemsx is not cached"
|
||||
)
|
||||
class AmberelecCarriesTheMachines(unittest.TestCase):
|
||||
"""AmberELEC's blueMSX package installs the core alone, so the pack and
|
||||
the install list bring the Machines tree its BIOS list names."""
|
||||
|
||||
def test_machine_configs_come_with_the_tree(self) -> None:
|
||||
platforms_dir = str(REPO_ROOT / "platforms")
|
||||
cwd = os.getcwd()
|
||||
os.chdir(REPO_ROOT)
|
||||
try:
|
||||
config = common.load_platform_config("amberelec", platforms_dir)
|
||||
registry = common.load_data_dir_registry(platforms_dir)
|
||||
profiles = common.load_emulator_profiles(str(REPO_ROOT / "emulators"))
|
||||
trees = builder.core_data_trees(config, profiles, "amberelec", registry)
|
||||
refs: dict = {}
|
||||
members = dict(
|
||||
(full, src) for src, full in builder._data_directory_members(
|
||||
config.get("systems", {}), registry, "amberelec",
|
||||
config.get("base_destination", ""), False, set(), set(), set(),
|
||||
refs, core_trees=trees,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
# Below bios/, where the platform writes every destination.
|
||||
self.assertIn("bios/Machines/COL - ColecoVision/config.ini", members)
|
||||
self.assertEqual(
|
||||
refs["bios/Machines/COL - ColecoVision/config.ini"][0], "bluemsx"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -845,9 +845,14 @@ class FirstInstallWalkthrough(unittest.TestCase):
|
||||
self.assertNotRegex(self.text, r"\d+ (in place|missing|files installed)")
|
||||
self.assertIn(f" {install.EVERYTHING_IN_PLACE}", self.text)
|
||||
step3 = _generated(self.run, "run-index")
|
||||
count, size = re.search(r"\n (\d+) files \(([\d.]+ [KMG]B)\)", step3).groups()
|
||||
count = re.search(r"\n (\d+) files \([\d.]+ [KMG]B\)", step3).group(1)
|
||||
self.assertIn(f"0 in place, {count} missing", step3)
|
||||
self.assertIn(f"Downloading {count} files ({size})", _generated(self.run, "run-download"))
|
||||
# The data trees are fetched after the files, from their archives:
|
||||
# the download line counts the files alone.
|
||||
trees = re.search(r" and (\d+) data files", step3)
|
||||
downloaded = int(count) - (int(trees.group(1)) if trees else 0)
|
||||
self.assertRegex(_generated(self.run, "run-download"),
|
||||
rf"Downloading {downloaded} files \([\d.]+ [KMG]B\)")
|
||||
step4 = _generated(self.run, "run-end")
|
||||
self.assertIn(f"Done. {count} files installed", step4)
|
||||
self.assertIn(f"{count} in place, 0 missing", _generated(self.run, "run-check"))
|
||||
|
||||
+1
-1
@@ -548,7 +548,7 @@ reports the profile as uncheckable, and the CI job that runs
|
||||
| `mode` | no | default mode: `standalone`, `libretro`, or `both` |
|
||||
| `verification` | no | how the core verifies BIOS: `existence`, `md5`, `sha1`, `crc32` |
|
||||
| `files` | yes, unless `type: alias` | list of file entries |
|
||||
| `data_directories` | no | whole directory trees the core needs, referencing `_data_dirs.yml` keys. An entry may carry `mode:` when only one build reads the tree, and `required: false` when the core runs without it (`--required-only` then leaves it out) |
|
||||
| `data_directories` | no | whole directory trees the core needs, referencing `_data_dirs.yml` keys. An entry may carry `mode:` when only one build reads the tree, `required: false` when the core runs without it (`--required-only` then leaves it out), and `load_from: content_dir` when the core reads it beside the content rather than in the system directory. Every platform that runs the core as a libretro core gets the tree in its pack, unless the entry names it in `installed_on` or `unread_on`, or the `_data_dirs.yml` entry has a `for_platforms` list without it |
|
||||
| `notes` | no | free-form technical notes |
|
||||
| `note` | no | single-paragraph variant of `notes` |
|
||||
| `exclusion_note` | no | why the profile has no files despite .info declaring firmware |
|
||||
|
||||
@@ -52,7 +52,7 @@ Every module in `tests/`, by the area it guards.
|
||||
| Core regression suites | synthetic | `test_e2e`, `test_audit_regressions`, `test_tool_messages` |
|
||||
| File resolution and identity | synthetic + collection | `test_adler32_resolution`, `test_agnostic_scan`, `test_agnostic_scan_mode`, `test_agnostic_scope`, `test_agnostic_substitute`, `test_archive_choice`, `test_archive_recipes`, `test_bios_companions`, `test_bundled_identity`, `test_casefold_cache`, `test_db_aliases`, `test_declared_names`, `test_derivations`, `test_directory_entries`, `test_extra_identity`, `test_foreign_tree`, `test_foreign_tree_names`, `test_frontend_digest`, `test_game_data_homonyms`, `test_hashing`, `test_identity`, `test_md5_pack_context`, `test_name_tiebreak`, `test_nec_filing`, `test_np2_fonts`, `test_owner_resolution`, `test_path_suffix`, `test_resolution_evidence`, `test_sega_filing`, `test_shipped_content`, `test_twin_declarations`, `test_validated_choice`, `test_variant_choice`, `test_variant_identity`, `test_zipped_member` |
|
||||
| Verification, cross-reference and profile checks | synthetic + profiles | `test_bios_layout`, `test_cache_independent_counts`, `test_case_insensitive_platforms`, `test_checks_have_values`, `test_core_file_fills_declaration`, `test_core_size_refusal`, `test_cross_reference_sources`, `test_crypto_aes`, `test_crypto_unique_data`, `test_crypto_verify`, `test_entry_categories`, `test_file_consumers`, `test_fileless_audit`, `test_flycast_layout`, `test_gap_settlement`, `test_hidden_extras`, `test_launcher_aliases`, `test_launcher_layer`, `test_native_mode`, `test_no_case_collisions`, `test_path_conflict_status`, `test_path_null_copies`, `test_profile_checks`, `test_profile_names`, `test_profile_paths`, `test_profile_repositories`, `test_romm_hash_rule`, `test_shared_groups`, `test_standalone_links`, `test_standalone_mode`, `test_undeclared_dedup`, `test_unplaced_extras`, `test_validation_scope`, `test_verify_all_dir`, `test_verify_emulator_evidence`, `test_verify_listing`, `test_verify_pack_choice`, `test_verify_release_asset` |
|
||||
| Packs and manifests | synthetic + real packs | `test_artifact_lock`, `test_atomic_artifacts`, `test_batocera_cores`, `test_deterministic_zip`, `test_emulator_pack_counts`, `test_finalize_pack_names`, `test_large_file_cache`, `test_libretro_layout`, `test_manifest_entry`, `test_minui_platform`, `test_no_game_data`, `test_pack_conformance_dirs`, `test_pack_core_extras`, `test_pack_counts`, `test_pack_destinations`, `test_pack_failures`, `test_pack_file_names`, `test_pack_grouping`, `test_pack_integrity`, `test_pack_member_identity`, `test_pack_readme`, `test_platform_inheritance`, `test_platform_keys`, `test_release_packs`, `test_required_only_archives`, `test_required_only_twins`, `test_save_root`, `test_split_extras`, `test_split_names`, `test_split_pack`, `test_system_pack_extras`, `test_system_pack_parity`, `test_torrentzip`, `test_which_pack` |
|
||||
| Packs and manifests | synthetic + real packs | `test_artifact_lock`, `test_atomic_artifacts`, `test_batocera_cores`, `test_core_data_trees`, `test_deterministic_zip`, `test_emulator_pack_counts`, `test_finalize_pack_names`, `test_large_file_cache`, `test_libretro_layout`, `test_manifest_entry`, `test_minui_platform`, `test_no_game_data`, `test_pack_conformance_dirs`, `test_pack_core_extras`, `test_pack_counts`, `test_pack_destinations`, `test_pack_failures`, `test_pack_file_names`, `test_pack_grouping`, `test_pack_integrity`, `test_pack_member_identity`, `test_pack_readme`, `test_platform_inheritance`, `test_platform_keys`, `test_release_packs`, `test_required_only_archives`, `test_required_only_twins`, `test_save_root`, `test_split_extras`, `test_split_names`, `test_split_pack`, `test_system_pack_extras`, `test_system_pack_parity`, `test_torrentzip`, `test_which_pack` |
|
||||
| Slots and regions | synthetic + profiles | `test_region`, `test_region_agnostic_scan`, `test_region_groups`, `test_region_held`, `test_region_platform_index`, `test_region_required_only`, `test_slot`, `test_slots`, `test_slots_shipped_file` |
|
||||
| Hardware targets and systems | synthetic + target files | `test_extract_targets`, `test_lakka_targets`, `test_libretro_slugs`, `test_system_ids`, `test_system_selection`, `test_target_names`, `test_target_scrapers`, `test_target_systems`, `test_target_verify` |
|
||||
| Installer and downloads | loopback server | `test_check_release`, `test_download`, `test_install`, `test_install_cores`, `test_install_experience`, `test_install_app`, `test_install_from`, `test_install_games`, `test_install_offline`, `test_install_selection`, `test_install_tui` |
|
||||
|
||||
Reference in new issue
Block a user