"""What an install manifest records about each file so the installer can narrow it. The installer runs without the profiles or the database, yet a narrowed install must hold the same files as a pack built with the same narrowing. Each entry therefore carries the platform systems it belongs to and, when the code selects it by territory, its regions and the groups in which it competes. The installer replays region.resolve_region_drops over them. The pack's `--system X` keeps the platform's declarations under X and every core extra owned by X, then groups regions over the declarations it kept and over every core extra of the platform. A group membership is therefore either unconditional (a core extra) or present only when the declaring system is kept (a platform declaration), and the manifest records the two apart. """ from __future__ import annotations from dataclasses import dataclass import re import region as region_mod from common import _norm_system_id, native_layer, sanitize_pack_path from packextras import _extra_system_ids, _kept, extra_region_groups, system_spellings def _dest(entry: dict) -> str: return sanitize_pack_path(entry.get("destination", entry.get("name", ""))) @dataclass(frozen=True) class SelectionIndex: """System and region facts for one platform, as the pack builder reads them.""" region_index: dict[str, dict] competing: frozenset[str] systems_by_dest: dict[str, frozenset[str]] declared_by: dict[str, frozenset[str]] extra_groups: dict[str, frozenset[str]] @classmethod def build( cls, region_groups: dict[str, list[tuple[str, str]]], region_index: dict[str, dict], pack_systems: dict[str, dict], extras: list[dict], required_only: bool = False, ) -> SelectionIndex: by_norm: dict[str, set[str]] = {} for sys_id in pack_systems: by_norm.setdefault(_norm_system_id(sys_id), set()).add(sys_id) declared_by: dict[str, set[str]] = {} for sys_id, system in pack_systems.items(): for file_entry in _kept(system.get("files", []), required_only): dest = _dest(file_entry) if dest: declared_by.setdefault(dest, set()).add(sys_id) owned_by: dict[str, set[str]] = {} extra_groups: dict[str, set[str]] = {} spellings = system_spellings(pack_systems) for extra in extras: dest = _dest(extra) if not dest: continue extra_groups.setdefault(dest, set()).update( extra_region_groups(extra, spellings) ) # A profile system the platform does not have is no system the # pack's --system accepts: it would offer what the pack refuses. for sys_id in _extra_system_ids(extra): owned_by.setdefault(dest, set()).update( by_norm.get(_norm_system_id(sys_id), ()) ) systems_by_dest = { dest: frozenset(declared_by.get(dest, set()) | owned_by.get(dest, set())) for dest in set(declared_by) | set(owned_by) } return cls( region_index, frozenset( dest for members in region_groups.values() for dest, _name in members ), systems_by_dest, {dest: frozenset(ids) for dest, ids in declared_by.items()}, {dest: frozenset(ids) for dest, ids in extra_groups.items()}, ) def fields(self, dest: str, name: str) -> dict: """What an entry carries for the installer's selection. `systems` drives --system. A regional entry adds its regions, `region_groups` it always competes in as a core extra, and `region_system_groups` it competes in only when that platform system is kept, and `priority` the rank the code searches it at. An entry that never competes carries no region field: an untagged file always survives a region filter. """ out: dict = {} systems = self.systems_by_dest.get(dest) if systems: out["systems"] = sorted(systems) if dest not in self.competing: return out regions = region_mod.lookup_regions(self.region_index, dest, name) if not regions: return out out["regions"] = sorted(regions) always = self.extra_groups.get(dest) if always: out["region_groups"] = sorted(always) declared = self.declared_by.get(dest) if declared: out["region_system_groups"] = sorted(declared) # The code's search rank decides whether a world file may withdraw # this one when no region matches. priority = region_mod.lookup_priority(self.region_index, dest, name) if priority is not None: out["priority"] = priority return out def _name_key(name: str) -> str: """Letters and digits only, as the installer compares names.""" return re.sub(r"[^a-z0-9]", "", name.lower()) def native_system_names(config: dict, systems: list[str]) -> dict[str, list[str]]: """Per platform system, the names the platform's own file gives it. A Batocera user knows PlayStation as `psx`, the folder under roms/, and the platform YAML records that name as `native_system` on each file it transcribes. Only the platform's own entries count (`native_layer`), a name spelled like the system id adds nothing, and a name the platform gives to two systems, or that reads as another system's id, names neither. """ native = native_layer(config) or {} by_name: dict[str, set[str]] = {} for sys_id, system in native.get("systems", {}).items(): if sys_id not in systems: continue for entry in system.get("files", []): name = entry.get("native_system") if isinstance(name, str) and name.strip(): by_name.setdefault(name.strip(), set()).add(sys_id) ids = {_name_key(sys_id) for sys_id in systems} out: dict[str, list[str]] = {} for name, owners in by_name.items(): if len(owners) != 1 or _name_key(name) in ids: continue out.setdefault(next(iter(owners)), []).append(name) return {sys_id: sorted(names) for sys_id, names in sorted(out.items())}