diff --git a/emulators/fbneo.yml b/emulators/fbneo.yml index 2a2a825e9..8534afb8c 100644 --- a/emulators/fbneo.yml +++ b/emulators/fbneo.yml @@ -51,10 +51,12 @@ data_directories: - ref: fbneo-cheats unread_on: [romm] destination: fbneo/cheats + required: false source_ref: "source src/burner/conc.cpp:167" - ref: fbneo-samples unread_on: [romm] destination: fbneo + required: false source_ref: "source src/burn/snd/samples.cpp:21-35" notes: | diff --git a/install.ps1 b/install.ps1 index 28de54233..01c164290 100644 --- a/install.ps1 +++ b/install.ps1 @@ -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 = "57e3cb6739a701181908773e603f17d2b10e1b0e2a5cde5cc4d5c97426da5dd4" + $defaultInstallSha256 = "3a78bf2e5a910d0f5c21bc2dc490aa600fc3273c56ae01e9ce5061d9b3578f07" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 04a697af5..a5cf46c28 100644 --- a/install.py +++ b/install.py @@ -3724,6 +3724,8 @@ def _check_data_directory(entry: object, where: str) -> None: values = entry.get(field, []) if not isinstance(values, list) or not all(isinstance(v, str) for v in values): raise ValueError(f"{where}.{field} is invalid") + if entry.get("optional", True) is not True: + raise ValueError(f"{where}.optional 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") @@ -7393,11 +7395,16 @@ def _install_data_member( scratch.unlink(missing_ok=True) -def select_data_directories(manifest: dict, files: list[dict], narrowed: bool) -> list[dict]: - """The data trees this install keeps: all, or those of a kept system.""" - entries = manifest.get("data_directories") or [] +def select_data_directories(manifest: dict, files: list[dict], narrowed: bool, + required_only: bool = False) -> 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.""" + entries = [e for e in manifest.get("data_directories") or [] + if not (required_only and e.get("optional"))] if not narrowed: - return list(entries) + return entries kept = {system for f in files for system in entry_systems(f)} return [e for e in entries if kept & set(e.get("systems") or [])] @@ -8824,7 +8831,8 @@ def _prompt_custom_selection( if bios_path is not None and not dry_run and current != INSTALLED_ONLY: kept = menu_choice(files, omitted, installed, current, False)[0] kept_trees = select_data_directories({"data_directories": trees}, kept, - narrowed or current is not None) + narrowed or current is not None, + isinstance(current, GamesChoice) and current.required) 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 @@ -8953,7 +8961,8 @@ def _print_menu_head( # The trees of the systems kept: 2636 data files of blueMSX, Dolphin # and PPSSPP were counted for every --system tried. kept_trees = select_data_directories({"data_directories": trees}, shown, - narrowed or current is not None) + narrowed or current is not None, + isinstance(current, GamesChoice) and current.required) 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()) @@ -9764,6 +9773,12 @@ def games_setups(index: dict, plat: str, bios_path: Path, os_type: str = "") -> if not named: above = card_root_above(bios_path) named = above[0] if above else "" + if not named: + # --dest on a handheld's games part from a computer (ArkOS's + # EASYROMS/bios): detection never ran there, and the card's own rom + # folders sit beside the BIOS folder. + part = games_part(bios_path.parent) + named = part.name if part else "" key = _setup_key(named) if named else "" found = [name for name in sorted(setups) if key and (name == key or name.startswith(key + "-"))] if not found and plat in setups: @@ -15671,7 +15686,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: != len(manifest.get("files", [])) + len(variants if case_kept else []) or games_applied ) - data_dirs = select_data_directories(manifest, files, narrowed) + data_dirs = select_data_directories(manifest, files, narrowed, + bool(args.required_only)) # 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 @@ -21812,7 +21828,8 @@ def _plan_data(state: TuiState, check: dict) -> tuple[int, int | None]: if not _tui_estimating(state): data = check.get("data_missing", 0) return data, check.get("data_bytes") if data else None - trees = select_data_directories(state.manifest or {}, _choice_entries(state), True) + required = any(flag == "--required-only" for flag, _value in tui_flags(state)) + trees = select_data_directories(state.manifest or {}, _choice_entries(state), True, required) sizes = [meta.get("size", 0) for entry in trees for meta in (entry.get("files") or {}).values()] return len(sizes), sum(sizes) diff --git a/install.sh b/install.sh index a03428f05..fce43f99f 100755 --- a/install.sh +++ b/install.sh @@ -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="57e3cb6739a701181908773e603f17d2b10e1b0e2a5cde5cc4d5c97426da5dd4" +DEFAULT_INSTALL_SHA256="3a78bf2e5a910d0f5c21bc2dc490aa600fc3273c56ae01e9ce5061d9b3578f07" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/install/amberelec.json b/install/amberelec.json index 104db8e5e..c1390a71e 100644 --- a/install/amberelec.json +++ b/install/amberelec.json @@ -6,7 +6,7 @@ "platform": "amberelec", "display_name": "AmberELEC", "version": "prerelease-20261019_1835", - "generated": "2026-11-05T23:59:37Z", + "generated": "2026-11-06T07:21:27Z", "base_destination": "", "detect": [ { @@ -77381,7 +77381,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-samples", @@ -77615,7 +77616,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true }, { "ref": "freeintv-overlays", @@ -77988,7 +77990,8 @@ } }, "strip_components": 1, - "archive_size": 21311170 + "archive_size": 21311170, + "optional": true } ] } \ No newline at end of file diff --git a/install/batocera.json b/install/batocera.json index 583de52ee..c1f5c14e2 100644 --- a/install/batocera.json +++ b/install/batocera.json @@ -6,7 +6,7 @@ "platform": "batocera", "display_name": "Batocera", "version": "43.1", - "generated": "2026-11-05T23:59:37Z", + "generated": "2026-11-06T07:21:27Z", "base_destination": "bios", "detect": [ { @@ -225291,7 +225291,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-samples", @@ -225525,7 +225526,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true } ], "save_directory": { diff --git a/install/emudeck.json b/install/emudeck.json index 32f4ed6a2..3f01646ff 100644 --- a/install/emudeck.json +++ b/install/emudeck.json @@ -6,7 +6,7 @@ "platform": "emudeck", "display_name": "EmuDeck", "version": "2.7EA", - "generated": "2026-11-05T23:59:37Z", + "generated": "2026-11-06T07:21:27Z", "base_destination": "bios", "detect": [ { @@ -123400,7 +123400,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-samples", @@ -123634,7 +123635,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true }, { "ref": "freeintv-overlays", @@ -124007,7 +124009,8 @@ } }, "strip_components": 1, - "archive_size": 21311170 + "archive_size": 21311170, + "optional": true }, { "ref": "lrps2", diff --git a/install/emuelec.json b/install/emuelec.json index 32c8e5488..35ec58389 100644 --- a/install/emuelec.json +++ b/install/emuelec.json @@ -6,7 +6,7 @@ "platform": "emuelec", "display_name": "EmuELEC", "version": "4.8", - "generated": "2026-11-05T23:59:37Z", + "generated": "2026-11-06T07:21:27Z", "base_destination": "", "detect": [ { @@ -83639,7 +83639,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-samples", @@ -83873,7 +83874,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true }, { "ref": "freeintv-overlays", @@ -84246,7 +84248,8 @@ } }, "strip_components": 1, - "archive_size": 21311170 + "archive_size": 21311170, + "optional": true } ] } \ No newline at end of file diff --git a/install/knulli.json b/install/knulli.json index ccdbd73b9..fd6a9e45b 100644 --- a/install/knulli.json +++ b/install/knulli.json @@ -6,7 +6,7 @@ "platform": "knulli", "display_name": "KNULLI", "version": "20260511", - "generated": "2026-11-05T23:59:37Z", + "generated": "2026-11-06T07:21:27Z", "base_destination": "bios", "detect": [ { @@ -157933,7 +157933,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-samples", @@ -158167,7 +158168,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true }, { "ref": "lrps2", diff --git a/install/lakka.json b/install/lakka.json index 5866fe5db..09f9de198 100644 --- a/install/lakka.json +++ b/install/lakka.json @@ -6,7 +6,7 @@ "platform": "lakka", "display_name": "Lakka", "version": "v6.1", - "generated": "2026-11-05T23:59:37Z", + "generated": "2026-11-06T07:21:27Z", "base_destination": "system", "detect": [ { @@ -130677,7 +130677,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-samples", @@ -130911,7 +130912,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true }, { "ref": "freeintv-overlays", @@ -131284,7 +131286,8 @@ } }, "strip_components": 1, - "archive_size": 21311170 + "archive_size": 21311170, + "optional": true }, { "ref": "yaps2-resources", diff --git a/install/myminui.json b/install/myminui.json index 85de7b9bc..7cb542e4e 100644 --- a/install/myminui.json +++ b/install/myminui.json @@ -6,7 +6,7 @@ "platform": "myminui", "display_name": "MyMinUI", "version": "2026-08-09", - "generated": "2026-11-05T23:59:37Z", + "generated": "2026-11-06T07:21:27Z", "base_destination": "", "detect": [ { @@ -18958,7 +18958,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-cheats", @@ -32688,7 +32689,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-samples", @@ -32922,7 +32924,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true }, { "ref": "fbneo-samples", @@ -33156,7 +33159,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true } ] } \ No newline at end of file diff --git a/install/nextui.json b/install/nextui.json index 4518d3d5c..933ec3af5 100644 --- a/install/nextui.json +++ b/install/nextui.json @@ -6,7 +6,7 @@ "platform": "nextui", "display_name": "NextUI", "version": "2026-10-20", - "generated": "2026-11-05T23:59:37Z", + "generated": "2026-11-06T07:21:27Z", "base_destination": "", "detect": [ { @@ -19366,7 +19366,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-samples", @@ -19600,7 +19601,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true } ] } \ No newline at end of file diff --git a/install/recalbox.json b/install/recalbox.json index 9b97c9d50..6db9f7daa 100644 --- a/install/recalbox.json +++ b/install/recalbox.json @@ -6,7 +6,7 @@ "platform": "recalbox", "display_name": "Recalbox", "version": "10.1.1", - "generated": "2026-11-05T23:59:37Z", + "generated": "2026-11-06T07:21:27Z", "base_destination": "bios", "detect": [ { @@ -62834,7 +62834,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-samples", @@ -63068,7 +63069,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true } ], "save_directory": { diff --git a/install/retroarch.json b/install/retroarch.json index 11b60b965..5e89fef85 100644 --- a/install/retroarch.json +++ b/install/retroarch.json @@ -6,7 +6,7 @@ "platform": "retroarch", "display_name": "RetroArch", "version": "v1.22.2", - "generated": "2026-11-05T23:59:37Z", + "generated": "2026-11-06T07:21:27Z", "base_destination": "system", "detect": [ { @@ -140789,7 +140789,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-samples", @@ -141023,7 +141024,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true }, { "ref": "freeintv-overlays", @@ -141396,7 +141398,8 @@ } }, "strip_components": 1, - "archive_size": 21311170 + "archive_size": 21311170, + "optional": true }, { "ref": "ppsspp-assets", diff --git a/install/retrobat.json b/install/retrobat.json index ca7459771..aec7d58a0 100644 --- a/install/retrobat.json +++ b/install/retrobat.json @@ -6,7 +6,7 @@ "platform": "retrobat", "display_name": "RetroBat", "version": "8.3.1", - "generated": "2026-11-05T23:59:37Z", + "generated": "2026-11-06T07:21:27Z", "base_destination": "bios", "detect": [ { @@ -89076,7 +89076,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-samples", @@ -89310,7 +89311,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true }, { "ref": "freeintv-overlays", @@ -89683,7 +89685,8 @@ } }, "strip_components": 1, - "archive_size": 21311170 + "archive_size": 21311170, + "optional": true } ], "save_directory": { diff --git a/install/retrodeck.json b/install/retrodeck.json index b9b4b4a10..2938c951d 100644 --- a/install/retrodeck.json +++ b/install/retrodeck.json @@ -6,7 +6,7 @@ "platform": "retrodeck", "display_name": "RetroDECK", "version": "0.10.10b", - "generated": "2026-11-05T23:59:37Z", + "generated": "2026-11-06T07:21:27Z", "base_destination": "", "detect": [ { @@ -153141,7 +153141,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-samples", @@ -153375,7 +153376,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true }, { "ref": "freeintv-overlays", @@ -153748,7 +153750,8 @@ } }, "strip_components": 1, - "archive_size": 21311170 + "archive_size": 21311170, + "optional": true }, { "ref": "yaps2-resources", diff --git a/install/retropie.json b/install/retropie.json index 3db3da56e..b6ab50768 100644 --- a/install/retropie.json +++ b/install/retropie.json @@ -6,7 +6,7 @@ "platform": "retropie", "display_name": "RetroPie", "version": "4.8.12", - "generated": "2026-11-05T23:59:37Z", + "generated": "2026-11-06T07:21:27Z", "base_destination": "BIOS", "detect": [ { @@ -146474,7 +146474,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-samples", @@ -146708,7 +146709,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true }, { "ref": "freeintv-overlays", @@ -147081,7 +147083,8 @@ } }, "strip_components": 1, - "archive_size": 21311170 + "archive_size": 21311170, + "optional": true }, { "ref": "yaps2-resources", diff --git a/install/rocknix.json b/install/rocknix.json index f06bd3144..e15cda7e5 100644 --- a/install/rocknix.json +++ b/install/rocknix.json @@ -6,7 +6,7 @@ "platform": "rocknix", "display_name": "ROCKNIX", "version": "20261001", - "generated": "2026-11-05T23:59:37Z", + "generated": "2026-11-06T07:21:27Z", "base_destination": "bios", "detect": [ { @@ -109453,7 +109453,8 @@ } }, "strip_components": 1, - "archive_size": 5083767 + "archive_size": 5083767, + "optional": true }, { "ref": "fbneo-samples", @@ -109687,7 +109688,8 @@ } }, "strip_components": 0, - "archive_size": 53006540 + "archive_size": 53006540, + "optional": true }, { "ref": "freeintv-overlays", @@ -110060,7 +110062,8 @@ } }, "strip_components": 1, - "archive_size": 21311170 + "archive_size": 21311170, + "optional": true }, { "ref": "lrps2", diff --git a/schemas/install-manifest.schema.json b/schemas/install-manifest.schema.json index 264c08558..a72245722 100644 --- a/schemas/install-manifest.schema.json +++ b/schemas/install-manifest.schema.json @@ -452,6 +452,11 @@ }, "description": "Platform systems whose cores read the tree." }, + "optional": { + "type": "boolean", + "const": true, + "description": "Every core reading the tree runs without it (required: false in its profile): --required-only leaves it out." + }, "files": { "type": "object", "additionalProperties": { diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 3934830a9..c0a20376b 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -518,7 +518,8 @@ def _data_directory_members( gets it from the tree's archive. ``refs_out``, when given, maps each yielded destination to - (registry key, system ids, directory destination), for the manifest. + (registry key, system ids, directory destination, whether the core + needs the tree), 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 @@ -532,7 +533,8 @@ def _data_directory_members( 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: + needed = dd.get("required") is not False + if required_only and not needed: continue if ref_key in installed_data_refs(platform_name): continue @@ -567,7 +569,7 @@ def _data_directory_members( ): del reclaim[full] if refs_out is not None: - refs_out[full] = (ref_key, systems, dd_prefix) + refs_out[full] = (ref_key, systems, dd_prefix, needed) yield src, full continue if full in seen_destinations or ( @@ -581,7 +583,7 @@ def _data_directory_members( if case_insensitive: seen_lower.add(full.lower()) if refs_out is not None: - refs_out[full] = (ref_key, systems, dd_prefix) + refs_out[full] = (ref_key, systems, dd_prefix, needed) yield src, full @@ -4910,7 +4912,7 @@ 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, tuple[str, ...], str]] = {} + data_refs: dict[str, tuple[str, tuple[str, ...], str, bool]] = {} 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, @@ -5021,7 +5023,7 @@ def generate_manifest( def _manifest_data_directories( members: list[tuple[str, str]], - refs: dict[str, tuple[str, tuple[str, ...], str]], + refs: dict[str, tuple[str, tuple[str, ...], str, bool]], registry: dict, base_dest: str, ) -> list[dict]: @@ -5034,11 +5036,14 @@ def _manifest_data_directories( changed since is refused rather than installed. archive_size is the size of the archive refresh_data_dirs last downloaded, what fetching the tree costs: a tarball whose tree is 10 KB is a download of 21 MB. + ``optional`` marks a tree every core reading it runs without + (``required: false``): install.py --required-only leaves it out, as the + pack built with --required-only does. """ grouped: dict[tuple[str, str], dict] = {} versions: dict[str, dict[str, dict]] = {} for src, full in members: - ref, systems, prefix = refs[full] + ref, systems, prefix, needed = refs[full] entry = registry.get(ref, {}) key = (ref, prefix) group = grouped.get(key) @@ -5072,11 +5077,19 @@ def _manifest_data_directories( size = versions[str(recorded)].get(ref, {}).get("archive_size") if isinstance(size, int) and size > 0: group["archive_size"] = size + if needed: + group["needed"] = True 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)} - return [grouped[key] for key in sorted(grouped)] + trees = [] + for key in sorted(grouped): + group = grouped[key] + if not group.pop("needed", False): + group["optional"] = True + trees.append(group) + return trees def _manifest_core_index( diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index c5e4a8ecc..685e4082a 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -6923,9 +6923,11 @@ class RequiredOnlyReachesDataDirectories(unittest.TestCase): def test_every_tree_walk_reads_the_flag(self): source = (ROOT / "scripts" / "generate_pack.py").read_text(encoding="utf-8") - self.assertEqual( - source.count('if required_only and dd.get("required") is False:'), 2 - ) + # The pack's walk and the manifest's, which records the flag for + # install.py --required-only. + self.assertEqual(source.count('if required_only and dd.get("required") is False:'), 1) + self.assertIn('needed = dd.get("required") is not False\n if required_only and not needed:', + source) self.assertIn("seen_parents, required_only=required_only,", source) self.assertIn("data_refs, required_only=required_only,", source) diff --git a/tests/test_install.py b/tests/test_install.py index 9effcdd49..7e6689029 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -3880,6 +3880,20 @@ class DataDirectoriesAreInstalled(unittest.TestCase): }, } + def test_required_only_leaves_out_an_optional_tree(self): + """A GBA and a SNES game with --for-games --required-only took + FBNeo's cheats and samples, 3,472 files no game opened.""" + cheats = {**self._entry(), "ref": "fbneo-cheats", "systems": ["snes"], "optional": True} + sys_tree = {**self._entry(), "systems": ["snes"]} + manifest = {"data_directories": [cheats, sys_tree]} + files = [{"dest": "a.rom", "systems": ["snes"]}] + self.assertEqual(install.select_data_directories(manifest, files, True), [cheats, sys_tree]) + self.assertEqual(install.select_data_directories(manifest, files, True, True), [sys_tree]) + self.assertEqual(install.select_data_directories(manifest, files, False, True), [sys_tree]) + install._check_data_directory(cheats, "data_directories[0]") + with self.assertRaises(ValueError): + install._check_data_directory({**cheats, "optional": False}, "data_directories[0]") + def test_the_archive_is_drawn_and_counted(self): """82 MB of archives came with no progress line, and the run ended on "310.5 KB downloaded", the BIOS files alone.""" diff --git a/tests/test_install_tui.py b/tests/test_install_tui.py index a7584e71b..f8a7eea48 100644 --- a/tests/test_install_tui.py +++ b/tests/test_install_tui.py @@ -12912,6 +12912,18 @@ class DriveIsACardByItsFiles(unittest.TestCase): self.assertEqual([install.card_system(mount) for mount in mounts], [None, None, None]) + def test_a_dest_on_a_games_part_reads_its_rom_folders(self): + """--dest EASYROMS/bios --for-games from a computer: detection never + ran on the card, the generic RetroArch folders were tried, and the + run kept every file of the list.""" + arkos = games_part(self.root / "media" / "EASYROMS") + index = {"setups": {"arkos": {}, "retroarch": {}}} + with mock.patch.object(install, "_mount_points", return_value=frozenset()), \ + mock.patch.object(install, "_drive_not_removable", return_value=False): + self.assertEqual(install.games_setups(index, "retroarch", arkos / "bios"), ["arkos"]) + self.assertEqual(install.games_setups(index, "retroarch", self.root / "bios"), + ["retroarch"]) + def test_a_card_beside_a_data_drive_comes_first(self): """The ArkOS card and the data drive mounted, the data drive sorted first and took Enter. The card comes first in diff --git a/wiki/installer.md b/wiki/installer.md index 6378b191a..1f109d861 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -405,7 +405,7 @@ lists record these files as omitted. -A RetroArch dry run into an empty folder, with the list of 2026-11-05, printed this: +A RetroArch dry run into an empty folder, with the list of 2026-11-06, printed this: ``` Fetching file index for retroarch... @@ -435,7 +435,7 @@ the installer can fetch them. Each line a first install prints, and what it means. The run below is a -Batocera install into an empty folder, from the list of 2026-11-05. +Batocera install into an empty folder, from the list of 2026-11-06. The figures change with each release. @@ -531,7 +531,7 @@ would save, once that saving is over half. -RetroArch with `--system sony-playstation`, from the list of 2026-11-05: +RetroArch with `--system sony-playstation`, from the list of 2026-11-06: ``` duckstation pulls 316.5 MB of this 329.0 MB download for sony-playstation. --core pcsx1 keeps 11.0 MB of it and saves 318.0 MB (96%). @@ -876,7 +876,9 @@ BIOS sets of its parent chain (`neogeo.zip` for `mslug.zip`). A game that is not recognised gets every BIOS of its system, and the run names it. So does an arcade set the core does not know. **Only what starts** -(`--required-only`) leaves out the files the cores mark optional. +(`--required-only`) leaves out the files the cores mark optional, and the data +folders every core reading them runs without (FinalBurn Neo's cheats and +samples). The games are read without hashing whole discs, and a cache keeps what was read, so a second run over the same card reads only new files. It is a