feat: choose systems, cores and regions to install

This commit is contained in:
Abdessamad Derraz committed 2026-10-10 04:31:57 +02:00
1 parent b0bd20f403
commit ece2c06aaf
13 files changed
+1426 -34

No files matched your search

+36 -10
View File
@@ -64,6 +64,7 @@ from common import (
import packresolve
from refresh_data_dirs import cache_lock
import region as region_mod
from manifestselect import SelectionIndex
import slot as slot_mod
import slots
import split_pack
@@ -2952,6 +2953,7 @@ def _manifest_entry(
cores: list[str] | None,
db: dict,
repo_root: str,
selection_fields: dict,
) -> tuple[dict | None, int]:
"""The download record of a resolved file, and the size it occupies.
@@ -2962,6 +2964,7 @@ def _manifest_entry(
scan, a hash computed wrong) has neither a repo path nor a release
asset. None then, so the caller records an omission instead of a dead
entry that makes the installer refuse the whole manifest.
`selection_fields` carries what the installer narrows on.
"""
sha1 = ""
sha256 = ""
@@ -2982,6 +2985,7 @@ def _manifest_entry(
"size": file_size,
"repo_path": repo_path,
"cores": cores,
**selection_fields,
}
if is_release_asset:
entry["storage"] = "release"
@@ -3007,6 +3011,7 @@ def _manifest_core_entries(
omitted_by_destination: dict,
record_omission,
pack_only_sizes: list[int],
selection: SelectionIndex,
required_only: bool = False,
) -> int:
"""Add the files a platform's cores need but its list does not name.
@@ -3073,6 +3078,7 @@ def _manifest_core_entries(
entry, file_size = _manifest_entry(
local_path, manifest_dest, [source_emu] if source_emu else [],
db, repo_root,
selection.fields(dest, fe.get("name", "")),
)
if entry is None:
systems = _extra_system_ids(fe)
@@ -3095,7 +3101,7 @@ def _manifest_core_entries(
return total_size
def _manifest_region_drops(
def _manifest_selection(
config: dict,
pack_systems: dict,
emulators_dir: str,
@@ -3106,10 +3112,13 @@ def _manifest_region_drops(
source: str,
required_only: bool,
regions: list[str] | None,
) -> set[str]:
"""Destinations a region list removes, grouped as the pack groups them."""
if not regions:
return set()
) -> tuple[set[str], SelectionIndex]:
"""Destinations a region list removes, and the groups the installer replays.
Grouped as the pack groups them, so a region the installer applies to
the full manifest withdraws what `--region` withdraws from the pack.
"""
extras: list[dict] = []
region_groups, _extra_dests = platform_region_groups(
config,
pack_systems,
@@ -3121,9 +3130,16 @@ def _manifest_region_drops(
include_extras=(source != "platform"),
include_all=(source == "truth"),
required_only=required_only,
extras_out=extras,
)
return region_mod.resolve_region_drops(
region_groups, region_mod.build_region_index(emu_profiles), regions
region_index = region_mod.build_region_index(emu_profiles)
drops = (
region_mod.resolve_region_drops(region_groups, region_index, regions)
if regions
else set()
)
return drops, SelectionIndex.build(
region_groups, region_index, pack_systems, extras, required_only
)
@@ -3234,9 +3250,17 @@ def generate_manifest(
"required": bool(file_entry.get("required", True)),
"reason": reason,
"cores": cores,
# The builder counts an unavailable regional file in its groups:
# the installer must too, or it keeps a fallback the pack drops.
**selection.fields(
sanitize_pack_path(
file_entry.get("destination", file_entry.get("name", ""))
),
str(file_entry.get("name") or ""),
),
}
region_drops = _manifest_region_drops(
region_drops, selection = _manifest_selection(
config, pack_systems, emulators_dir, db, base_dest, emu_profiles,
target_cores, source, required_only, regions,
)
@@ -3325,7 +3349,8 @@ def generate_manifest(
)
entry, file_size = _manifest_entry(
local_path, dest, None, db, repo_root
local_path, dest, None, db, repo_root,
selection.fields(dest, file_entry["name"]),
)
if entry is None:
record_omission(full_dest, file_entry, sys_id, "not_found", None)
@@ -3366,7 +3391,8 @@ def generate_manifest(
core_files, config, db, bios_dir, base_dest, repo_root,
zip_contents, offline, region_drops, case_insensitive,
seen_destinations, seen_lower, seen_parents, manifest_files,
omitted_by_destination, record_omission, pack_only_sizes, required_only,
omitted_by_destination, record_omission, pack_only_sizes, selection,
required_only,
)
# Phase 3: data directories. The installer does not fetch them, so they
+1 -1
View File
@@ -372,7 +372,7 @@ def generate_readme(db: dict, platforms_dir: str) -> str:
"",
"## Download BIOS packs",
"",
"One pack per platform, and it holds everything the platform runs: its own BIOS list plus every file its emulator cores load. Pick your platform, download the ZIP, extract to the BIOS path. The installer above does the same file by file, and `--target` narrows it to one machine; for a region or a bare minimum, build your own pack below.",
"One pack per platform, and it holds everything the platform runs: its own BIOS list plus every file its emulator cores load. Pick your platform, download the ZIP, extract to the BIOS path. The installer above does the same file by file. At a terminal it asks whether to install everything or to choose systems, cores and regions, and `--system`, `--core`, `--region` and `--target` make the same choice without questions; for a bare minimum, build your own pack below.",
"Files is how many the pack holds once extracted, the figure a file"
" manager shows for the folder. It differs from one platform to the"
" next because a pack carries only what that platform's emulators"
+7 -5
View File
@@ -3405,11 +3405,13 @@ guarantee either: source profiles can document files nobody has dumped or
that only the user can provide, all of them visible in the
[gap analysis](gaps.md).
Want less than everything? The installer takes `--target switch` to install
only what one machine's cores need. From a clone of the repository,
`python scripts/generate_pack.py --platform retroarch --region us` keeps one
BIOS per region and `--required-only` the bare minimum each core needs to
start; `--help` lists every way to build your own.
Want less than everything? At a terminal the installer asks whether to install
everything or to choose systems, cores and regions, and `--system`, `--core`,
`--region` and `--target` make the same choice without questions (see the
[installer reference](wiki/installer.md#choosing-what-to-install)). From a
clone of the repository, `python scripts/generate_pack.py --platform retroarch
--region us` builds the same narrowing as a pack and `--required-only` the bare
minimum each core needs to start; `--help` lists every way to build your own.
---
+112
View File
@@ -0,0 +1,112 @@
"""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 region as region_mod
from common import _norm_system_id, sanitize_pack_path
from packextras import _extra_system_ids, _kept, extra_region_groups
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]] = {}
for extra in extras:
dest = _dest(extra)
if not dest:
continue
extra_groups.setdefault(dest, set()).update(extra_region_groups(extra))
# 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. 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)
return out
+18 -11
View File
@@ -685,6 +685,7 @@ def platform_region_groups(
include_extras: bool = True,
include_all: bool = False,
required_only: bool = False,
extras_out: list[dict] | None = None,
) -> tuple[dict[str, list[tuple[str, str]]], dict[tuple[str, str, str], str]]:
"""Group a platform's pack candidates the way region filtering reads them.
@@ -692,7 +693,8 @@ def platform_region_groups(
grouped under, keyed by (emulator, name). verify.py needs that mapping to
withdraw from its report exactly what the builder withdraws from the pack:
grouping the declared files here and the core extras there would let the
two answer differently on the same request.
two answer differently on the same request. `extras_out` receives the
core extras that were grouped, for the install manifest.
"""
groups: dict[str, list[tuple[str, str]]] = {}
for sys_id, system in systems.items():
@@ -733,19 +735,24 @@ def platform_region_groups(
extra.get("source_path", ""),
)
] = dest
variant = extra.get("variant_group")
if variant:
# A variant group is one slot of one core, whatever system each
# member is filed under: PicoDrive files its US Mega CD BIOS under
# sega-segacd and the EU/JP ones under sega-megacd, and find_bios
# picks one of the twelve. Keyed as emulator mode keys it.
group_ids = [f"{extra.get('source_emulator', '')}:variant:{variant}"]
else:
group_ids = _extra_system_ids(extra) or ["_extras"]
for group_id in group_ids:
for group_id in extra_region_groups(extra):
groups.setdefault(group_id, []).append((dest, name))
if extras_out is not None:
extras_out.append(extra)
return groups, extra_dests
def extra_region_groups(extra: dict) -> list[str]:
"""The region groups a core extra competes in."""
variant = extra.get("variant_group")
if variant:
# A variant group is one slot of one core, whatever system each
# member is filed under: PicoDrive files its US Mega CD BIOS under
# sega-segacd and the EU/JP ones under sega-megacd, and find_bios
# picks one of the twelve. Keyed as emulator mode keys it.
return [f"{extra.get('source_emulator', '')}:variant:{variant}"]
return _extra_system_ids(extra) or ["_extras"]
def emulator_region_drops(
selected: list[tuple[str, dict]],
standalone: bool,