mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-15 06:23:47 -06:00
chore: merge origin/main
This commit is contained in:
commit
2dc427d5d0
106 files changed
+5458
-13655
No files matched your search
+145
-18
@@ -3141,6 +3141,72 @@ def fold_case_twins(files: list[dict]) -> tuple[list[dict], list[str]]:
|
||||
return kept, folded
|
||||
|
||||
|
||||
# The characters FAT and exFAT refuse in a name, as Windows does on any drive
|
||||
# (Linux 7475ea8b, fs/fat/namei_vfat.c:201-207 and fs/exfat/nls.c:396-403;
|
||||
# "Naming Files, Paths, and Namespaces" in the Win32 documentation). The
|
||||
# slash, the backslash and the controls never reach a destination
|
||||
# (_safe_relative_path). RetroDECK names three files zxs:48.rom,
|
||||
# zxs:if1-2.rom and snes:st018.data.rom (its retroarch component manifest):
|
||||
# on an exFAT card each write failed, on every run.
|
||||
FAT_REFUSED = re.compile(r'[<>:"|?*]')
|
||||
FAT_REFUSED_SHOWN = '< > : " | ? *'
|
||||
|
||||
|
||||
def holds_fat_refused_names(directory: Path) -> bool:
|
||||
"""Whether a name holding a colon can be created there.
|
||||
|
||||
Probed on the nearest existing directory, as keeps_case is: an ext4
|
||||
card on a Steam Deck holds one, an exFAT card refuses it with EINVAL.
|
||||
Windows refuses it on every drive. A probe refused for another cause
|
||||
(a folder this user cannot write) decides nothing.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
return False
|
||||
probe_dir = directory
|
||||
while not probe_dir.is_dir() and probe_dir != probe_dir.parent:
|
||||
probe_dir = probe_dir.parent
|
||||
try:
|
||||
handle, name = tempfile.mkstemp(prefix=".retrobios-name-", suffix=":",
|
||||
dir=str(probe_dir))
|
||||
except OSError as exc:
|
||||
return exc.errno != errno.EINVAL
|
||||
os.close(handle)
|
||||
os.unlink(name)
|
||||
return True
|
||||
|
||||
|
||||
def split_fat_refused(files: list[dict]) -> tuple[list[dict], list[str]]:
|
||||
"""The entries whose destination a FAT, exFAT or Windows folder can
|
||||
hold, and the destinations it cannot."""
|
||||
kept = [entry for entry in files if not FAT_REFUSED.search(str(entry.get("dest", "")))]
|
||||
refused = [str(entry.get("dest", "")) for entry in files
|
||||
if FAT_REFUSED.search(str(entry.get("dest", "")))]
|
||||
return kept, refused
|
||||
|
||||
|
||||
# Android's media scanner marks a folder holding a .nomedia file hidden, and
|
||||
# every folder below it (MediaProvider a183e2b9, FileUtils.java:1587-1629
|
||||
# isDirectoryHidden, ModernMediaScanner.java:845-846 and 1017-1020). Without
|
||||
# one, the samples, overlays and videos of a BIOS folder in shared storage
|
||||
# showed in the phone's gallery and music apps. RetroArch writes none
|
||||
# (libretro-common/file/file_path_io.c:194 only skips one).
|
||||
NOMEDIA = ".nomedia"
|
||||
|
||||
|
||||
def hide_from_media_scanner(folder: Path) -> bool:
|
||||
"""Leave an empty .nomedia in folder, True when this call wrote it."""
|
||||
marker = folder / NOMEDIA
|
||||
if not _is_dir(folder) or _exists(marker):
|
||||
return False
|
||||
try:
|
||||
marker.touch()
|
||||
except OSError as exc:
|
||||
print(f" {NOMEDIA} not written in {shown_path(folder)}: {exc.strerror}",
|
||||
file=sys.stderr)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def installed_cores(dirs: list[Path]) -> list[str]:
|
||||
"""The cores present in these directories, by the name the buildbot uses.
|
||||
|
||||
@@ -7930,8 +7996,13 @@ def _install_data_member(
|
||||
|
||||
def select_data_directories(manifest: dict, files: list[dict], narrowed: bool,
|
||||
required_only: bool = False,
|
||||
games: GamesPlan | None = None) -> list[dict]:
|
||||
games: GamesPlan | None = None,
|
||||
systems: list[str] | None = None) -> list[dict]:
|
||||
"""The data trees this install keeps: all, or those of a kept system.
|
||||
systems, the systems chosen by name, decide when given: a file several
|
||||
systems share named them all, and the arcade choice took blueMSX's
|
||||
machines through FBNeo's msx.zip, which the pack built with --system
|
||||
arcade leaves out.
|
||||
--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. For the games of a card, a tree only cores
|
||||
@@ -7945,8 +8016,14 @@ def select_data_directories(manifest: dict, files: list[dict], narrowed: bool,
|
||||
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)}
|
||||
return [e for e in entries if kept & set(e.get("systems") or [])]
|
||||
kept = (set(systems) if systems else
|
||||
{system for f in files for system in entry_systems(f)})
|
||||
# A tree no platform system serves goes with the files of none, as the
|
||||
# pack built with --system puts it in the part of its own.
|
||||
other = (OTHER_SYSTEMS in kept if systems else
|
||||
any(not entry_systems(f) for f in files))
|
||||
return [e for e in entries if kept & set(e.get("systems") or [])
|
||||
or (other and not e.get("systems"))]
|
||||
|
||||
|
||||
def _copy_targets(
|
||||
@@ -9375,7 +9452,8 @@ def _prompt_custom_selection(
|
||||
kept_trees = select_data_directories({"data_directories": trees}, kept,
|
||||
narrowed or current is not None,
|
||||
isinstance(current, GamesChoice) and current.required,
|
||||
current.plan if isinstance(current, GamesChoice) else None)
|
||||
current.plan if isinstance(current, GamesChoice) else None,
|
||||
chosen_systems(current))
|
||||
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
|
||||
@@ -9506,7 +9584,8 @@ def _print_menu_head(
|
||||
kept_trees = select_data_directories({"data_directories": trees}, shown,
|
||||
narrowed or current is not None,
|
||||
isinstance(current, GamesChoice) and current.required,
|
||||
current.plan if isinstance(current, GamesChoice) else None)
|
||||
current.plan if isinstance(current, GamesChoice) else None,
|
||||
chosen_systems(current))
|
||||
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())
|
||||
@@ -9692,6 +9771,11 @@ def read_in_place(pairs: list[tuple[dict, Path]]) -> list[str]:
|
||||
return verdicts
|
||||
|
||||
|
||||
def chosen_systems(choice: object) -> list[str] | None:
|
||||
"""The systems a choice names, None for one that names none."""
|
||||
return list(choice[0]) or None if isinstance(choice, tuple) else None
|
||||
|
||||
|
||||
def menu_choice(
|
||||
files: list[dict], omitted: list[dict],
|
||||
installed: tuple[list[dict], list[dict]] | None, current: object, cut: bool,
|
||||
@@ -10317,6 +10401,13 @@ 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:
|
||||
# --platform names the list, not the card: the full screen passes
|
||||
# --platform=retroarch for a muOS card, detection never runs in that
|
||||
# run, and the card's ROMS/ was never read. The card's own files
|
||||
# name its system, as card_app reads them for the app.
|
||||
card = card_app(bios_path)
|
||||
named = card.system if card 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
|
||||
@@ -16000,6 +16091,15 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
f" {_plural(len(folded), 'file')} left out: {bios_path} does not keep "
|
||||
f"case and holds another file under the same name ({shown})"
|
||||
)
|
||||
if any(FAT_REFUSED.search(str(entry.get("dest", ""))) for entry in files) \
|
||||
and not holds_fat_refused_names(bios_path):
|
||||
files, refused = split_fat_refused(files)
|
||||
shown = ", ".join(refused[:3]) + (" ..." if len(refused) > 3 else "")
|
||||
print(
|
||||
f" {_plural(len(refused), 'file')} left out: the file system of "
|
||||
f"{shown_path(bios_path)} cannot hold a name with any of "
|
||||
f"{FAT_REFUSED_SHOWN} ({screen_text(shown)})"
|
||||
)
|
||||
|
||||
game_data_cut = 0
|
||||
if args.no_game_data and not manifest.get("categorized"):
|
||||
@@ -16309,7 +16409,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
)
|
||||
data_dirs = select_data_directories(manifest, files, narrowed,
|
||||
bool(args.required_only),
|
||||
games_plan if games_applied else None)
|
||||
games_plan if games_applied else None,
|
||||
chosen_systems(selection))
|
||||
# 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
|
||||
@@ -16759,6 +16860,10 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
|
||||
failures.append((where, f"{NOT_KEPT} ({reason})"))
|
||||
total_errors += 1
|
||||
|
||||
if os_type == "android" and not args.check and hide_from_media_scanner(bios_path):
|
||||
print(f" Added {NOMEDIA} to {shown_path(bios_path)}: the gallery and music "
|
||||
"apps leave its images, sounds and videos out")
|
||||
|
||||
# Standalone copies
|
||||
if (
|
||||
manifest.get("standalone_copies")
|
||||
@@ -18717,8 +18822,9 @@ def _plan_rows(state: TuiState) -> list[TuiRow]:
|
||||
# The size the pack can give: "Install 6.6 GB" from a pack holding
|
||||
# none of the files.
|
||||
lacking = set(_tui_pack_lacks(state, [event["dest"] for event in state.files]))
|
||||
size = check.get("bytes_needed", 0) - sum(event["size"] for event in state.files
|
||||
if event["dest"] in lacking)
|
||||
size = (_ticked_estimate(state)[1] if _tui_estimating(state)
|
||||
else check.get("bytes_needed", 0) - sum(event["size"] for event in state.files
|
||||
if event["dest"] in lacking))
|
||||
label = (f"{TUI_INSTALL_ROW} {format_size(size)}" if size >= CONFIRM_FROM_BYTES
|
||||
else TUI_INSTALL_ROW)
|
||||
rows = [TuiRow("install", label)]
|
||||
@@ -22258,20 +22364,17 @@ def _view_plan(state: TuiState, width: int, height: int) -> tuple:
|
||||
# The headline counts the systems ticked, as the list just did: at
|
||||
# 40x12 the line below it was dropped, and the plan showed the
|
||||
# total of all of them. Their data files too, as line mode's head.
|
||||
chosen = _ticked_files(state)
|
||||
# One total, the data files in it, the kinds under the folder
|
||||
# adding up to it.
|
||||
lines[0] = _styled(f"About {_plural(len(chosen) + data, 'file')}, "
|
||||
f"{format_size(sum(e['size'] for e in chosen) + (data_size or 0))}"
|
||||
count, size = _ticked_estimate(state)
|
||||
lines[0] = _styled(f"About {_plural(count, 'file')}, {format_size(size)}"
|
||||
f", for {len(state.ticked)} of {len(state.systems)} systems", "title")
|
||||
if _offline_only():
|
||||
# Offline, what the folders copied from hold of the systems
|
||||
# ticked: the plan counted the whole list beside "For your 1 of
|
||||
# 11 systems".
|
||||
took = {event["dest"] for events in _tui_found_elsewhere(state).values()
|
||||
for event in events}
|
||||
held = [event for event in chosen if event["dest"] in took]
|
||||
rest = len(chosen) - len(held) + data
|
||||
held = _ticked_held(state)
|
||||
rest = len(_ticked_files(state)) - len(held) + data
|
||||
# Nothing at hand: the head the whole list's plan says, for the
|
||||
# systems ticked. "About 0 files, 0 B" stood over 20 files the
|
||||
# internet would bring.
|
||||
@@ -22279,8 +22382,8 @@ def _view_plan(state: TuiState, width: int, height: int) -> tuple:
|
||||
f"{agree(rest, 'file needs', 'files need')} the internet, which "
|
||||
f"{offline_named()} does not use, for {len(state.ticked)} of "
|
||||
f"{len(state.systems)} systems", "title") if not held else _styled(
|
||||
f"About {_plural(len(held), 'file')}, "
|
||||
f"{format_size(sum(e['size'] for e in held))}, for {len(state.ticked)} of "
|
||||
f"About {_plural(count, 'file')}, "
|
||||
f"{format_size(size)}, for {len(state.ticked)} of "
|
||||
f"{len(state.systems)} systems"
|
||||
+ (f", {agree(rest, 'needs', 'need')} the internet "
|
||||
f"({offline_named()})" if rest else ""), "title")
|
||||
@@ -22465,7 +22568,10 @@ def _plan_data(state: TuiState, check: dict) -> tuple[int, int | None]:
|
||||
data = check.get("data_missing", 0)
|
||||
return data, check.get("data_bytes") if data else None
|
||||
required = any(flag == "--required-only" for flag, _value in tui_flags(state))
|
||||
trees = select_data_directories(state.manifest or {}, _choice_entries(state), True, required)
|
||||
named = [name for _flag, name in _tui_system_flags(state) if name]
|
||||
known = {row["system"] for row in state.systems}
|
||||
trees = select_data_directories(state.manifest or {}, _choice_entries(state), True, required,
|
||||
systems=named if named and set(named) <= known else None)
|
||||
sizes = [meta.get("size", 0) for entry in trees for meta in (entry.get("files") or {}).values()]
|
||||
return len(sizes), sum(sizes)
|
||||
|
||||
@@ -22525,6 +22631,27 @@ def _ticked_files(state: TuiState) -> list[dict]:
|
||||
or (not event["systems"] and OTHER_SYSTEMS in state.ticked)]
|
||||
|
||||
|
||||
def _ticked_held(state: TuiState) -> list[dict]:
|
||||
"""Offline, the files of the systems ticked that the folders copied
|
||||
from hold: all an install without the internet brings."""
|
||||
took = {event["dest"] for events in _tui_found_elsewhere(state).values()
|
||||
for event in events}
|
||||
return [event for event in _ticked_files(state) if event["dest"] in took]
|
||||
|
||||
|
||||
def _ticked_estimate(state: TuiState) -> tuple[int, int]:
|
||||
"""The files and bytes the systems ticked since the check bring, their
|
||||
data files in them, or offline what the folders copied from hold: the
|
||||
plan's headline and its Install row both say it. One system ticked,
|
||||
the headline said 82.2 MB and Install 6.6 GB, the whole list's size."""
|
||||
if _offline_only():
|
||||
held = _ticked_held(state)
|
||||
return len(held), sum(event["size"] for event in held)
|
||||
chosen = _ticked_files(state)
|
||||
data, data_size = _plan_data(state, state.check or {})
|
||||
return len(chosen) + data, sum(event["size"] for event in chosen) + (data_size or 0)
|
||||
|
||||
|
||||
def _view_systems(state: TuiState, width: int, height: int) -> tuple:
|
||||
rows = tui_rows(state)
|
||||
whole_rows = rows
|
||||
|
||||
Reference in new issue
Block a user