feat: install one emulator's files in one command

install.py --emulator NAME keeps what the named emulators read on the detected platform: their own files and the platform's files of the systems they run, as a pack built for them carries, without the systems no core is mapped to. Each emulator page now gives that command for Linux, macOS and Windows, beside the per-file links.
This commit is contained in:
Abdessamad Derraz committed 2026-10-20 09:09:05 +02:00
1 parent 7c94a58825
commit e424a35aee
7 files changed
+141 -2

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@ param(
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "300fcfd077c970a6807b2414b19cbae87b330664f3f7092d8275f5da2fe6e022"
$defaultInstallSha256 = "6245355aa8a65872b761229f2429ca6e0953573f0b75f6f30286a2f067c83afd"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+55
View File
@@ -3505,6 +3505,49 @@ def _older_list_advice() -> str:
"installer again to fetch the current one.")
def _narrow_to_emulators(
names: list[str], manifest: dict, plat: str,
files: list[dict], omitted: list[dict],
) -> tuple[list[dict], list[dict]]:
"""Keep what the named emulators read, as the pack built for them.
An emulator page offers this as one command, so a name the platform
does not run is refused rather than read as nothing to install.
"""
if "system_cores" not in manifest:
print(f"Error: the {plat} file list does not say which emulators read "
"which systems, which --emulator needs.", file=sys.stderr)
print(_older_list_advice(), file=sys.stderr)
sys.exit(1)
known = sorted({
name
for profile, aliases in (manifest.get("core_names") or {}).items()
for name in [profile, *aliases]
} | set(available_choices(files, omitted)["cores"]))
try:
chosen = resolve_choices(names, known, "emulator")
except ValueError as exc:
print(f"Error: {plat}: {exc}", file=sys.stderr)
sys.exit(1)
before = len(files)
files, omitted = narrow_to_cores(manifest, chosen, files, omitted)
# A whole-platform install keeps the platform's files of a system no
# core is mapped to. Here only the named emulators' systems are wanted.
cores = designated_cores(manifest, chosen)
reached = {
system for system, readers in (manifest.get("system_cores") or {}).items()
if cores.intersection(readers)
}
def theirs(entry: dict) -> bool:
return entry.get("cores") is not None or bool(reached.intersection(entry_systems(entry)))
files = [entry for entry in files if theirs(entry)]
omitted = [entry for entry in omitted if theirs(entry)]
print(f" Narrowed {before} -> {len(files)} files to {', '.join(chosen)}")
return files, omitted
def _requested_narrowing(
args: argparse.Namespace, plat: str, files: list[dict], omitted: list[dict]
) -> tuple[list[str], list[str], list[str]] | None:
@@ -3588,6 +3631,13 @@ def main() -> None:
action="store_true",
help="opt in to copies into detected standalone-emulator directories",
)
parser.add_argument(
"--emulator",
action="append",
metavar="NAME",
help="install what these emulators read on the platform, as a pack "
"built for them carries (names: --list-cores)",
)
parser.add_argument(
"--installed-cores",
action="store_true",
@@ -3946,6 +3996,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
)
print(f" Narrowed {before} -> {len(files)} files to the installed cores")
if args.emulator:
files, omitted_files = _narrow_to_emulators(
args.emulator, manifest, plat_name, files, omitted_files
)
selection = _requested_narrowing(args, plat_name, files, omitted_files)
if (
selection is None
+1 -1
View File
@@ -18,7 +18,7 @@ esac
TEMP_INSTALLER=""
TEMP_DIRECTORY=""
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
DEFAULT_INSTALL_SHA256="300fcfd077c970a6807b2414b19cbae87b330664f3f7092d8275f5da2fe6e022"
DEFAULT_INSTALL_SHA256="6245355aa8a65872b761229f2429ca6e0953573f0b75f6f30286a2f067c83afd"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+33
View File
@@ -2969,6 +2969,37 @@ def _render_metadata_rows(profile: dict) -> list[str]:
return lines
def _install_block(name: str, profile: dict) -> list[str]:
"""One command that installs this emulator's files where it reads them.
The installer finds the platform and its BIOS folder, then keeps what
this emulator reads there: its own files and the platform's files of
the systems it runs. Each file is also linked one by one below.
"""
if profile.get("type") in ("alias", "launcher", "test"):
return []
base = "https://raw.githubusercontent.com/Abdess/retrobios/main"
return [
'??? tip "Install this emulator\'s files"',
"",
" The installer finds your platform and its BIOS folder, then downloads "
"only the files this emulator reads there and checks each one.",
"",
" Linux, macOS, Steam Deck:",
"",
" ```sh",
f" curl -fsSL {base}/install.sh | sh -s -- --emulator {name}",
" ```",
"",
" Windows (PowerShell):",
"",
" ```powershell",
f" &([scriptblock]::Create((irm {base}/install.ps1))) --emulator {name}",
" ```",
"",
]
def _render_platform_details(profile: dict) -> list[str]:
"""The per-platform block, one level of nesting deep."""
lines: list[str] = []
@@ -3287,6 +3318,8 @@ def generate_emulator_page(
lines.append("</div>")
lines.append("")
lines.extend(_install_block(name, profile))
lines.extend(_render_platform_details(profile))
lines.extend(_render_structured_blocks(profile))
+33
View File
@@ -2902,6 +2902,39 @@ class ARefusedStandaloneCopyIsNamed(unittest.TestCase):
class OneEmulatorsFilesInOneCommand(unittest.TestCase):
"""An emulator page offers one command for that emulator's files.
--core kept the platform's whole list, and narrowing to cores kept the
platform files of every system no core is mapped to."""
MANIFEST = {
"system_cores": {"ibm-pc": ["pcem"], "nintendo-nes": ["fceumm"]},
"core_names": {"pcem": ["pcem"], "fceumm": ["fceumm"]},
"files": [],
}
def test_only_its_files_and_its_systems(self):
import contextlib # noqa: PLC0415
files = [
{"dest": "pcem/ibmpc.bin", "cores": ["pcem"], "systems": ["ibm-pc"]},
{"dest": "disksys.rom", "cores": None, "systems": ["nintendo-nes"]},
{"dest": "ep128emu/exos.rom", "cores": None, "systems": ["enterprise"]},
{"dest": "ibm.rom", "cores": None, "systems": ["ibm-pc"]},
]
with contextlib.redirect_stdout(io.StringIO()):
kept, _omitted = install._narrow_to_emulators(
["PCem"], self.MANIFEST, "retroarch", files, [])
self.assertEqual(sorted(e["dest"] for e in kept), ["ibm.rom", "pcem/ibmpc.bin"])
def test_an_unknown_name_is_refused(self):
import contextlib # noqa: PLC0415
with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit):
install._narrow_to_emulators(["nope"], self.MANIFEST, "retroarch", [], [])
class ThePagesQuoteTheBootstrapsOwnWords(unittest.TestCase):
"""The installer page and the FAQ quoted "install.py SHA-256 mismatch",
a message the bootstraps no longer print."""
+17
View File
@@ -818,5 +818,22 @@ class ACardSaysWhyAFileIsLeftOut(unittest.TestCase):
self.assertIn("installed with the standalone emulator", badges)
class AnEmulatorPageOffersItsInstall(unittest.TestCase):
"""An emulator page linked each file one by one and offered no way to
install them together."""
def test_the_command_names_the_emulator(self):
from generate_site import _install_block
text = "\n".join(_install_block("pcem", {"type": "libretro"}))
self.assertIn("install.sh | sh -s -- --emulator pcem", text)
self.assertIn("install.ps1))) --emulator pcem", text)
self.assertEqual(_install_block("x", {"type": "alias"}), [])
def test_the_installer_takes_the_option(self):
source = (ROOT / "install.py").read_text(encoding="utf-8")
self.assertIn('"--emulator"', source)
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -219,6 +219,7 @@ what the options name, everything by default.
| `--installed-cores` | Keep only what the cores already installed read (RetroArch, Lakka, RetroPie). See [Installed cores](#installed-cores) |
| `--system NAMES` | Install only these systems. Comma-separated or repeated. `--list-systems` prints the names, `other` included |
| `--core NAMES` | Keep the platform's own list and only the extra files these cores load. `--list-cores` prints the names |
| `--emulator NAMES` | Install what these emulators read on the detected platform, and nothing else: their own files and the platform's files of the systems they run. Each emulator page on the site gives this command. `--list-cores` prints the names |
| `--region LIST` | Preferred regions, best first (`us,eu,jp`): keep one BIOS per region where a system has several. Accepts the names and aliases of `generate_pack.py --region` |
| `--list-systems` | Print a platform's systems with the files and size each holds |
| `--list-cores` | Print the cores whose extra files a platform installs |