mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-11 12:33:53 -06:00
feat: install for the card's games without asking
--yes alone installed the whole list, 6.4 GB on a dArkOS card holding two games, while the screen offers what the games need first. A run that asks nothing now takes that first choice when the card holds games, and says so. --all-files installs the whole list. A run given its list (the screen's own runs) or narrowed by name is unchanged.
This commit is contained in:
1 parent
6316fd12ff
commit
097914507b
8 files changed
+142
-21
No files matched your search
+1
-1
@@ -16,7 +16,7 @@
|
||||
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
|
||||
# the site's build rewrites this line there (scripts/sitebootstrap.py).
|
||||
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
|
||||
$defaultInstallSha256 = "b17220355845692a087bbc29cdb4a1008f68b1423d1126f34f00fa0259b53184"
|
||||
$defaultInstallSha256 = "e5d6473efdd2c4f05cc8db178b6b00ec2b9637f7abfa3a2d81e23265f13c7132"
|
||||
$maximumInstallerBytes = 2MB
|
||||
$installerExit = 0
|
||||
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
|
||||
|
||||
+52
-7
@@ -3726,6 +3726,9 @@ def _check_data_directory(entry: object, where: str) -> None:
|
||||
raise ValueError(f"{where}.{field} is invalid")
|
||||
if entry.get("optional", True) is not True:
|
||||
raise ValueError(f"{where}.optional is invalid")
|
||||
cores = entry.get("cores", ["-"])
|
||||
if not isinstance(cores, list) or not cores or not all(isinstance(c, str) and c for c in cores):
|
||||
raise ValueError(f"{where}.cores is invalid")
|
||||
files = entry.get("files")
|
||||
if not isinstance(files, dict) or not files or len(files) > MAX_MANIFEST_FILES:
|
||||
raise ValueError(f"{where}.files is invalid")
|
||||
@@ -6077,7 +6080,7 @@ _CHOICE_OPTIONS = ("--system", "--core", "--region", "--for-games", "--required-
|
||||
# An option not named here comes after them.
|
||||
_COMMAND_ORDER = (
|
||||
"--manifest", "--from-pack", "--system", "--core", "--emulator", "--region",
|
||||
"--for-games", "--required-only", "--target", "--no-game-data", "--installed-cores", "--cores-dir", "--every-board",
|
||||
"--for-games", "--required-only", "--all-files", "--target", "--no-game-data", "--installed-cores", "--cores-dir", "--every-board",
|
||||
"--declared-only", "--standalone-copies", "--save-dir", "--jobs", "--from",
|
||||
"--add-app", "--offline", "--check", "--dry-run",
|
||||
)
|
||||
@@ -6114,6 +6117,7 @@ class Invocation:
|
||||
options.append(("--for-games", None if games is True else str(games)))
|
||||
for flag, on in (
|
||||
("--required-only", getattr(args, "required_only", False)),
|
||||
("--all-files", getattr(args, "all_files", False)),
|
||||
("--every-board", args.every_board),
|
||||
("--installed-cores", args.installed_cores and not args.cores_dir),
|
||||
("--no-game-data", args.no_game_data),
|
||||
@@ -7396,13 +7400,20 @@ def _install_data_member(
|
||||
|
||||
|
||||
def select_data_directories(manifest: dict, files: list[dict], narrowed: bool,
|
||||
required_only: bool = False) -> list[dict]:
|
||||
required_only: bool = False,
|
||||
games: GamesPlan | None = None) -> list[dict]:
|
||||
"""The data trees this install keeps: all, or those of a kept system.
|
||||
--required-only leaves out a tree every core reading it runs without
|
||||
(`optional`): a GBA and a SNES game took FBNeo's cheats and samples,
|
||||
3,472 files no game opened."""
|
||||
3,472 files no game opened. For the games of a card, a tree only cores
|
||||
read (`cores`) is kept when the plan runs one of them, or runs a system
|
||||
of the tree on a core no setup names."""
|
||||
entries = [e for e in manifest.get("data_directories") or []
|
||||
if not (required_only and e.get("optional"))]
|
||||
if games is not None:
|
||||
entries = [e for e in entries if not e.get("cores")
|
||||
or set(e["cores"]) & games.profiles
|
||||
or set(e.get("systems") or []) & games.any_core]
|
||||
if not narrowed:
|
||||
return entries
|
||||
kept = {system for f in files for system in entry_systems(f)}
|
||||
@@ -8832,7 +8843,8 @@ def _prompt_custom_selection(
|
||||
kept = menu_choice(files, omitted, installed, current, False)[0]
|
||||
kept_trees = select_data_directories({"data_directories": trees}, kept,
|
||||
narrowed or current is not None,
|
||||
isinstance(current, GamesChoice) and current.required)
|
||||
isinstance(current, GamesChoice) and current.required,
|
||||
current.plan if isinstance(current, GamesChoice) else None)
|
||||
if kept and all_in_place(kept, kept_trees, bios_path):
|
||||
# Read before the menu, as the screen reads before it asks: a
|
||||
# rerun was offered an install that installs nothing, then told
|
||||
@@ -8962,7 +8974,8 @@ def _print_menu_head(
|
||||
# and PPSSPP were counted for every --system tried.
|
||||
kept_trees = select_data_directories({"data_directories": trees}, shown,
|
||||
narrowed or current is not None,
|
||||
isinstance(current, GamesChoice) and current.required)
|
||||
isinstance(current, GamesChoice) and current.required,
|
||||
current.plan if isinstance(current, GamesChoice) else None)
|
||||
data = sum(len(entry.get("files") or {}) for entry in kept_trees)
|
||||
data_size = sum(meta.get("size", 0) for entry in kept_trees
|
||||
for meta in (entry.get("files") or {}).values())
|
||||
@@ -10526,6 +10539,10 @@ class GamesPlan:
|
||||
self.required: set[str] = set()
|
||||
self.unknown_core: list[str] = []
|
||||
self.truncated = False
|
||||
# The cores the games run on (a folder's default and its boards'),
|
||||
# and the systems of a folder no setup names a core for.
|
||||
self.profiles: set[str] = set()
|
||||
self.any_core: set[str] = set()
|
||||
|
||||
@property
|
||||
def fallback(self) -> list[GameFacts]:
|
||||
@@ -10753,6 +10770,11 @@ def plan_games(plat: str, bios_path: Path, manifest: dict, folder: Path | None =
|
||||
cache.save()
|
||||
for game_folder, group in read:
|
||||
if group:
|
||||
for profile in _folder_profiles(game_folder.row):
|
||||
if profile:
|
||||
plan.profiles.add(str(profile))
|
||||
else:
|
||||
plan.any_core.update(str(s) for s in game_folder.row.get("systems") or [])
|
||||
arcade = game_folder.row.get("arcade")
|
||||
sets = (arcade_index(arcade) or {}).get("support", set()) \
|
||||
if isinstance(arcade, str) else set()
|
||||
@@ -13357,6 +13379,13 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
action="store_true",
|
||||
help="with --for-games, only the files the games cannot start without",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all-files",
|
||||
action="store_true",
|
||||
help="install the whole list even when the card holds games. A run that "
|
||||
"asks nothing (--yes, --no-input, --events) otherwise installs only "
|
||||
"what the card's games need, the plan's first choice",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-game-data",
|
||||
action="store_true",
|
||||
@@ -13556,6 +13585,8 @@ def main() -> None:
|
||||
parser.error("--quiet applies to an install, not to --check, --dry-run or a listing")
|
||||
if args.tui and args.plain:
|
||||
parser.error("--tui and --plain ask for two different interfaces")
|
||||
if args.all_files and args.for_games is not None:
|
||||
parser.error("--all-files and --for-games ask for two different lists")
|
||||
if args.required_only and args.for_games is None:
|
||||
parser.error("--required-only applies to --for-games: the list alone does not "
|
||||
"say which files a game cannot start without")
|
||||
@@ -15537,8 +15568,18 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
# some, what they need is the menu's first choice, and --for-games
|
||||
# narrows the list here, before the systems are counted.
|
||||
will_ask = interactive and not args.check and not listing
|
||||
# A run that asks nothing takes the plan's first choice: the games of
|
||||
# the card. `--yes` alone installed 6.4 GB on a dArkOS card holding
|
||||
# two games, while the screen offers what they need first. Not for a
|
||||
# run given its list (the screen's own runs, the choice already
|
||||
# made), nor one narrowed by name.
|
||||
unasked = (not interactive and not args.check and not listing
|
||||
and not getattr(args, "all_files", False)
|
||||
and LOCAL.manifest is None and LOCAL.pack is None
|
||||
and not (args.installed_cores or args.no_game_data or args.system
|
||||
or args.core or args.region or args.emulator or args.declared_only))
|
||||
games_plan = None
|
||||
if args.for_games is not None or (will_ask and not (
|
||||
if args.for_games is not None or unasked or (will_ask and not (
|
||||
args.installed_cores or args.no_game_data or LOCAL.pack is not None)):
|
||||
games_plan = read_games(args, plat_name, bios_path, manifest, term, os_type)
|
||||
games_applied = False
|
||||
@@ -15551,6 +15592,9 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
games_applied = True
|
||||
print(f" For the games: {_plural(len(files), 'file')} of the list"
|
||||
+ (f", {GAMES_REQUIRED_WORDS}" if args.required_only else ""))
|
||||
if args.for_games is None:
|
||||
print(" The card holds games, so only what they need is installed. "
|
||||
"--all-files installs the whole list.")
|
||||
version_notes += [line.strip() for line in fallback_lines(games_plan)]
|
||||
# --no-game-data leaves the game data out once the list is narrowed
|
||||
# to the choice: taken out first, its line named the 5099 game data
|
||||
@@ -15691,7 +15735,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
or games_applied
|
||||
)
|
||||
data_dirs = select_data_directories(manifest, files, narrowed,
|
||||
bool(args.required_only))
|
||||
bool(args.required_only),
|
||||
games_plan if games_applied else None)
|
||||
# The kinds as the plan and the menu head say them (parts_words),
|
||||
# the data files of the folders kept among them: this head said
|
||||
# BIOS files and game data beside a plan of three kinds, neither
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
|
||||
# curl or wget stop on a cut transfer before the hash is read: the site's
|
||||
# build rewrites this line there (scripts/sitebootstrap.py).
|
||||
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
|
||||
DEFAULT_INSTALL_SHA256="b17220355845692a087bbc29cdb4a1008f68b1423d1126f34f00fa0259b53184"
|
||||
DEFAULT_INSTALL_SHA256="e5d6473efdd2c4f05cc8db178b6b00ec2b9637f7abfa3a2d81e23265f13c7132"
|
||||
MAX_INSTALLER_BYTES=2097152
|
||||
|
||||
cleanup() {
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{
|
||||
"dest": "roms/ports/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1113711,
|
||||
"sha256": "b17220355845692a087bbc29cdb4a1008f68b1423d1126f34f00fa0259b53184",
|
||||
"size": 1116557,
|
||||
"sha256": "e5d6473efdd2c4f05cc8db178b6b00ec2b9637f7abfa3a2d81e23265f13c7132",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -71,8 +71,8 @@
|
||||
"muos": {
|
||||
"archive": {
|
||||
"dest": "ARCHIVE/RetroBIOS.muxapp",
|
||||
"size": 1167338,
|
||||
"sha256": "3ca24c5ba64437c1b2554238910b38463c160e74fcd59db675bac3fccd426d67"
|
||||
"size": 1170184,
|
||||
"sha256": "bf4d73f0cb82edc2b299821e3b1b950ca3b05d32d156cedca4e6a94712e7de34"
|
||||
},
|
||||
"members": [
|
||||
{
|
||||
@@ -92,8 +92,8 @@
|
||||
{
|
||||
"name": "RetroBIOS/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1113711,
|
||||
"sha256": "b17220355845692a087bbc29cdb4a1008f68b1423d1126f34f00fa0259b53184",
|
||||
"size": 1116557,
|
||||
"sha256": "e5d6473efdd2c4f05cc8db178b6b00ec2b9637f7abfa3a2d81e23265f13c7132",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
@@ -166,8 +166,8 @@
|
||||
{
|
||||
"dest": ".config/modules/retrobios/install.py",
|
||||
"repo_path": "install.py",
|
||||
"size": 1113711,
|
||||
"sha256": "b17220355845692a087bbc29cdb4a1008f68b1423d1126f34f00fa0259b53184",
|
||||
"size": 1116557,
|
||||
"sha256": "e5d6473efdd2c4f05cc8db178b6b00ec2b9637f7abfa3a2d81e23265f13c7132",
|
||||
"executable": false
|
||||
},
|
||||
{
|
||||
|
||||
@@ -82,8 +82,8 @@ def llms_txt(platforms: list[tuple[str, str]]) -> str:
|
||||
"Most people want to play now, with the fewest files. The installer does that in one "
|
||||
"command: it finds the setup and its BIOS folder, reads the games on the card, and with "
|
||||
"`--for-games --required-only` installs only the files those games cannot start "
|
||||
"without. Without these two options it installs every file of the setup's list "
|
||||
"(gigabytes), even with `--yes`.",
|
||||
"without. `--yes` alone installs what the card's games need, optional files "
|
||||
"included, and `--all-files` the setup's whole list (gigabytes).",
|
||||
"",
|
||||
f"1. Preview, nothing written: `{SH} --for-games --required-only --dry-run`",
|
||||
f"2. Install, no questions: `{SH} --for-games --required-only --yes`",
|
||||
|
||||
@@ -899,7 +899,7 @@ GLANCE_HEADING = "## Options at a glance"
|
||||
GLANCE_GROUPS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("Where and for what", ("--platform", "--dest", "--target", "--every-board", "--save-dir",
|
||||
"--romm-firmware", "--standalone-copies", "--add-app")),
|
||||
("Fewer files", ("--for-games", "--required-only", "--system", "--core", "--emulator",
|
||||
("Fewer files", ("--for-games", "--required-only", "--all-files", "--system", "--core", "--emulator",
|
||||
"--region", "--no-game-data", "--declared-only", "--installed-cores",
|
||||
"--cores-dir")),
|
||||
("Look first", ("--check", "--dry-run", "--list-platforms", "--list-targets",
|
||||
|
||||
@@ -912,6 +912,78 @@ class CommandLine(unittest.TestCase):
|
||||
self.assertTrue((self.card.cache / install.GAMES_SCAN_FILE).is_file())
|
||||
|
||||
|
||||
class ARunThatAsksNothing(unittest.TestCase):
|
||||
"""`--yes` alone installed the whole list (6.4 GB on a dArkOS card that
|
||||
held two games), while the screen offers what the games need first."""
|
||||
|
||||
def install(self, *options: str) -> tuple[list[str], str]:
|
||||
card = Card()
|
||||
self.addCleanup(card.close)
|
||||
card.game("SEGACD", "sonic.iso", mcd_iso(100))
|
||||
card.game("MD", "eu.md", MD_EU)
|
||||
root = card.root / "repo"
|
||||
(root / "install").mkdir(parents=True)
|
||||
shutil.copytree(card.lists / "games", root / "install" / "games")
|
||||
listed = manifest()
|
||||
(root / "bios").mkdir()
|
||||
for entry in listed["files"]:
|
||||
payload = entry["dest"].encode().ljust(entry["size"], b".")[:entry["size"]]
|
||||
(root / entry["repo_path"]).write_bytes(payload)
|
||||
(root / "install" / "retroarch.json").write_text(json.dumps(listed))
|
||||
handler = functools.partial(_Quiet, directory=str(root))
|
||||
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
self.addCleanup(server.server_close)
|
||||
self.addCleanup(server.shutdown)
|
||||
proc = run_cli(card, *options, offline=False,
|
||||
base=f"http://127.0.0.1:{server.server_address[1]}")
|
||||
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
|
||||
return sorted(p.relative_to(card.bios).as_posix()
|
||||
for p in card.bios.rglob("*") if p.is_file()), proc.stdout
|
||||
|
||||
def test_it_installs_what_the_games_need(self):
|
||||
unasked, said = self.install("--yes")
|
||||
self.assertEqual(unasked, self.install("--for-games", "--yes")[0])
|
||||
self.assertIn("--all-files installs the whole list", said)
|
||||
|
||||
def test_all_files_installs_the_whole_list(self):
|
||||
everything, said = self.install("--all-files", "--yes")
|
||||
self.assertEqual(everything, sorted(entry["dest"] for entry in manifest()["files"]))
|
||||
self.assertNotIn("For the games", said)
|
||||
|
||||
def test_all_files_and_for_games_are_refused_together(self):
|
||||
card = Card()
|
||||
self.addCleanup(card.close)
|
||||
proc = run_cli(card, "--all-files", "--for-games", "--check")
|
||||
self.assertEqual(proc.returncode, 2)
|
||||
self.assertIn("--all-files and --for-games ask for two different lists", proc.stderr)
|
||||
|
||||
|
||||
class DataTreesFollowTheGamesCores(unittest.TestCase):
|
||||
"""One GBA and one SNES game took FBNeo's cheats and samples: a tree
|
||||
only cores read is kept when the plan runs one of them."""
|
||||
|
||||
def test_a_tree_of_a_core_the_plan_does_not_run_is_left_out(self):
|
||||
plan = install.GamesPlan(Path("card"), True)
|
||||
plan.profiles = {"snes9x"}
|
||||
fbneo = {"ref": "fbneo-cheats", "systems": ["nintendo-snes"], "cores": ["fbneo"]}
|
||||
dolphin = {"ref": "dolphin-sys", "systems": ["nintendo-snes"]}
|
||||
trees = {"data_directories": [fbneo, dolphin]}
|
||||
files = [{"dest": "a.bin", "systems": ["nintendo-snes"]}]
|
||||
self.assertEqual(install.select_data_directories(trees, files, True, games=plan), [dolphin])
|
||||
plan.profiles.add("fbneo")
|
||||
self.assertEqual(install.select_data_directories(trees, files, True, games=plan),
|
||||
[fbneo, dolphin])
|
||||
|
||||
def test_a_system_with_no_named_core_keeps_every_cores_tree(self):
|
||||
plan = install.GamesPlan(Path("card"), True)
|
||||
plan.any_core = {"nintendo-gba"}
|
||||
fbneo = {"ref": "fbneo-cheats", "systems": ["nintendo-gba"], "cores": ["fbneo"]}
|
||||
self.assertEqual(install.select_data_directories(
|
||||
{"data_directories": [fbneo]}, [{"systems": ["nintendo-gba"]}], True, games=plan),
|
||||
[fbneo])
|
||||
|
||||
|
||||
class _Quiet(http.server.SimpleHTTPRequestHandler):
|
||||
def log_message(self, format, *args): # noqa: A002 - stdlib signature
|
||||
return
|
||||
|
||||
+5
-1
@@ -864,7 +864,10 @@ games on this card: yes**, and Install takes only what those games need. One
|
||||
press on the row installs everything instead. Line mode asks the same: Enter
|
||||
installs for the games, `e` everything, `r` only what starts. On the command
|
||||
line it is `--for-games`, and `--for-games FOLDER` reads a games folder on a
|
||||
computer, one subfolder per system.
|
||||
computer, one subfolder per system. A run that asks nothing (`--yes`,
|
||||
`--no-input`, `--events`) takes the same first choice when it finds games on
|
||||
the card, and says so. `--all-files` installs the whole list instead, as does
|
||||
a run narrowed by name (`--system`, `--core`, `--region`, `--emulator`).
|
||||
|
||||
The rule is that every game must start. Per system, the run keeps what the
|
||||
system's default core reads. One region is kept only where the game's own
|
||||
@@ -981,6 +984,7 @@ what the options name, everything by default.
|
||||
| <span id="option-region"></span>`--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` |
|
||||
| <span id="option-for-games"></span>`--for-games FOLDER` | Install only what the games on the card need. See [For the games on this card](#for-the-games-on-this-card) |
|
||||
| <span id="option-required-only"></span>`--required-only` | With `--for-games`, only the files the games cannot start without |
|
||||
| <span id="option-all-files"></span>`--all-files` | The whole list, even when the card holds games. A run that asks nothing (`--yes`, `--no-input`, `--events`) otherwise installs what the card's games need, the plan's first choice. See [For the games on this card](#for-the-games-on-this-card) |
|
||||
| <span id="option-no-game-data"></span>`--no-game-data` | Leave out game data (a game's own files, not firmware) and install the firmware only. What it leaves out is named. See [Firmware without game data](#firmware-without-game-data) |
|
||||
| <span id="option-declared-only"></span>`--declared-only` | Keep only the files the platform's own list names (`cores: null` in the manifest), not the extra files cores load. With `--check`, the answer is about the list the platform itself checks. Refused with `--core`, the listings it does not change, `--standalone-copies` and `--tui` |
|
||||
| <span id="option-list-systems"></span>`--list-systems` | Print a platform's systems with the files and size each holds, the files other systems share too counted apart. `--system`, `--core`, `--region`, `--target` and `--installed-cores` narrow it, as they narrow an install. A run prints one list: two listing options are refused |
|
||||
|
||||
Reference in new issue
Block a user