feat: line mode says details only with --verbose

By default a run prints what it installs, the BIOS folder, the progress
at a terminal, the result and the next step. The steps, the per-file log
lines, the counts and the reasons a file is left out come back with
--verbose. --check gives the count and the folder, and lists each file
with --verbose. The installer page shows the default run and walks
through the --verbose one.
This commit is contained in:
Abdessamad Derraz committed 2026-11-10 00:21:37 +01:00
1 parent dc5fc93f23
commit 36745abf10
16 files changed
+281 -152

No files matched your search

+1 -1
View File
@@ -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 = "185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7"
$defaultInstallSha256 = "00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e"
# The pin above refuses any changed byte: this cap only stops a body that
# cannot be the installer (a sign-in portal's page) before it is hashed.
$maximumInstallerBytes = 8MB
+150 -83
View File
@@ -494,6 +494,25 @@ class Terminal:
return self.paint(text, "strong")
class _Output:
"""What a run's text says. By default: what will happen, where, the
progress, the result and the next step. --verbose adds every step, list
and reason, the lines a beginner lost the result among. where_said is
set once the BIOS folder was named before the result."""
verbose = False
where_said = False
OUTPUT = _Output()
def detail(*args: Any, **kwargs: Any) -> None:
"""Print a line only --verbose shows."""
if OUTPUT.verbose:
print(*args, **kwargs)
def format_duration(seconds: float) -> str:
"""0:42, 3:05 or 1:02:09."""
seconds = max(0, int(seconds + 0.5))
@@ -521,8 +540,8 @@ class Progress:
"""Bytes and files across the parallel downloads, shown as they move.
On a terminal one line redraws in place: a bar, the bytes, the speed and
the time left. Elsewhere each finished file gets its line, as a log
wants it. Failures are always printed, quiet or not.
the time left. Elsewhere each finished file gets its line with --verbose,
as a log wants it. Failures are always printed, quiet or not.
"""
BAR = 24
@@ -601,7 +620,7 @@ class Progress:
why = f" ({reason})" if reason else ""
line = f" {self.term.bad('FAILED')} {dest}{why}"
SAID_FAILED.add(dest)
elif not self.term.live and not self.term.quiet:
elif not self.term.live and not self.term.quiet and OUTPUT.verbose:
line = f" [{self.files}/{self.total_files}] {dest} {self.term.ok('ok')}"
if line is not None:
self._clear()
@@ -5228,7 +5247,7 @@ def say_saves(plat: str, files: list[dict], omitted: list[dict], bios_path: Path
if printed:
return
for line in save_lines([], omitted, bios_path, display):
print(f" {line}")
detail(f" {line}")
# The digests the line menu's head read of the files in place, by path and
@@ -5701,8 +5720,9 @@ def _kept_lines() -> list[str]:
lines.append(f"Kept {_plural(item['files'], 'file')} already there "
f"({format_size(item['bytes'])}), which the collection's "
f"copies replaced, in {folder}")
lines.append(f" To put them back: {_INVOCATION.launcher()} --restore "
f"{_INVOCATION.word(str(folder))}")
if OUTPUT.verbose:
lines.append(f" To put them back: {_INVOCATION.launcher()} --restore "
f"{_INVOCATION.word(str(folder))}")
return lines
@@ -6634,7 +6654,7 @@ def _report_outcome(
# The command making the same choice ends every result the screen ends
# with it: "Finished with files missing" gave none.
same_lines = ([f"Same choice without questions: {screen_text(same_choice)}"]
if same_choice else [])
if same_choice and OUTPUT.verbose else [])
# A file --offline left out is not a failure, whether the run asked or
# not: without questions it was "14698 files failed", and "Run the same
# command again to retry only those", which cannot fetch them.
@@ -6644,7 +6664,7 @@ def _report_outcome(
if not isinstance(failure[1], OfflineReason)]
errors = max(0, errors - len(left))
offline += len(left)
if transferred and seconds > 0:
if transferred and seconds > 0 and OUTPUT.verbose:
verb = "copied from the pack" if LOCAL.pack is not None else "downloaded"
say(f"\n{format_size(transferred)} {verb} in "
f"{format_duration(seconds)} ({format_size(int(transferred / seconds))}/s).")
@@ -6723,7 +6743,7 @@ def _report_outcome(
# The kinds installed, on every end that installed some, under where
# they came from: as the screen's result says them.
installed_kinds = (f" Installed: {installed_words(kinds[0].bios, kinds[0].games, kinds[1])}."
if kinds and kinds[0].files and downloaded else "")
if kinds and kinds[0].files and downloaded and OUTPUT.verbose else "")
if offline:
remedy = _offline_remedy()
if not downloaded and not errors:
@@ -6829,25 +6849,43 @@ def _report_outcome(
say(f" {sources}")
if installed_kinds:
say(installed_kinds)
say(f"BIOS folder: {_locations(paths)}")
if OUTPUT.verbose or not OUTPUT.where_said:
say(f"BIOS folder: {_locations(paths)}")
for line in kept:
say(line)
if omitted > len(unplaced):
if omitted > len(unplaced) and OUTPUT.verbose:
for line in _omission_lines(omitted - len(unplaced),
[entry for entry in omitted_entries or []
if entry.get("reason") != "no_save_directory"],
Path(paths[0]) if len(paths) == 1 else None):
say(line)
# RomM's scan is the step after the install: the other hints say how to
# install less, a detail.
for line in hints or []:
say(line)
if same_choice:
if OUTPUT.verbose or line == ROMM_SCAN_WORDS:
say(line)
if same_choice and OUTPUT.verbose:
# After the questions, as the screen's result ends: "the same
# command" was one that asked them.
for line in same_lines:
say(line)
say("To check this later, run that command with --check.")
return
say("To check this later, run the same command with --check.")
elif OUTPUT.verbose:
say("To check this later, run the same command with --check.")
say(next_step_words(platform_of or {}, paths))
def next_step_words(platform_of: dict[Path, str], paths: list[Path]) -> str:
"""The step after an install that ended well: a card goes back in its
device, a setup on this machine is started."""
setups = [(platform_of.get(path, ""), path) for path in paths]
if setups and all(plat and on_its_card(plat, path) for plat, path in setups):
return "Next: put the card back in the device and start a game."
names = list(dict.fromkeys(detected_os(plat, path) or display_name(plat)
for plat, path in setups if plat))
if len(names) == 1:
return f"Next: start {names[0]} and play."
return "Next: start your emulators and play."
def _offline_remedy(folders: list[str] | None = None, named: bool = False) -> str:
@@ -6904,8 +6942,10 @@ def _check_fetch_line(fetch: int, beside: str, lacking: int = 0) -> str:
return " Run the same command without --check to copy them into place."
return (f" {agree(fetch, 'file needs', 'files need')} the "
f"internet, which {offline_named()} does not use. {_offline_remedy()}")
return (" Run the same command without --check to fetch them. A wrong copy is "
f"replaced, and the one in place kept in {BACKUP_DIR} beside {beside}.")
# Why nothing is lost goes with the details: the remedy is the line.
return (" Run the same command without --check to fetch them."
+ (f" A wrong copy is replaced, and the one in place kept in {BACKUP_DIR} "
f"beside {beside}." if OUTPUT.verbose else ""))
def _offline_failure(reason: str) -> bool:
@@ -11335,7 +11375,7 @@ def read_games(args: argparse.Namespace, plat: str, bios_path: Path, manifest: d
os_type=os_type):
return None
where = "on the card" if folder is None else f"in {folder}"
print(f"\nReading the games {where}...")
detail(f"\nReading the games {where}...")
try:
plan = plan_games(plat, bios_path, manifest, folder, term, os_type=os_type,
network=network)
@@ -11356,7 +11396,7 @@ def read_games(args: argparse.Namespace, plat: str, bios_path: Path, manifest: d
return None
emit_games(plat, plan, bool(args.required_only))
for line in games_lines(plan, manifest, bool(args.verbose)):
print(fold_line(line if line.startswith(" ") else f" {line}"))
detail(fold_line(line if line.startswith(" ") else f" {line}"))
return plan
@@ -14017,6 +14057,9 @@ def _run(args: argparse.Namespace) -> None:
raise
finally:
LOCAL.close_indexes()
# A caller running the installer in its own process gets the next
# run's text as quiet as any first run.
OUTPUT.verbose = OUTPUT.where_said = False
def _run_text(args: argparse.Namespace) -> None:
@@ -14268,8 +14311,10 @@ def _missing_dest(dest: Path, ask: bool, named: str = "named with --dest",
while not nearest.exists() and nearest.parent != nearest:
nearest = nearest.parent
missing = len(dest.absolute().relative_to(nearest).parts)
print(f" {shown_path(dest)} does not exist. The nearest folder that does: "
f"{shown_path(nearest)}")
# Said where it comes before a question: elsewhere the folder is made.
(print if missing > 1 and ask else detail)(
f" {shown_path(dest)} does not exist. The nearest folder that does: "
f"{shown_path(nearest)}")
if missing > 1 and ask:
answer = _ask(f" Create {_plural(missing, 'folder')} under {shown_path(nearest)}? [y/N] ")
if not answer.strip().lower().startswith("y"):
@@ -15334,7 +15379,9 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
print(f"{where}, so nothing is asked: the run goes on as with --no-input. "
f"Add --no-input to say so, or run it without {undo} to answer.",
file=sys.stderr)
print(f"{term.bold('RetroBIOS')} {term.dim(installer_id())}\n")
OUTPUT.verbose = bool(getattr(args, "verbose", False))
OUTPUT.where_said = False
detail(f"{term.bold('RetroBIOS')} {term.dim(installer_id())}\n")
os_type = detect_os()
if os_type == "wsl":
@@ -15382,7 +15429,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
if args.platform and args.dest:
platforms = [(args.platform, args.dest)]
elif args.platform:
print("Looking for setups here...")
detail("Looking for setups here...")
if not listing:
# The cards mounted here too, as the screen finds them, with or
# without questions: a pack's RetroArch list went to this
@@ -15401,7 +15448,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
platforms = matched
offer = getattr(args, "platform_from_list", False)
for name, path in matched:
print(fold_line(f" Found {found_label(name, path)}"))
detail(fold_line(f" Found {found_label(name, path)}"))
elif listing:
# A listing reads the manifest and writes nothing: no folder is
# needed, and asking BizHawk's for one refused a question asked
@@ -15501,16 +15548,16 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
platforms = [(args.platform, default_dest)]
assumed_dests.add(default_dest)
elif args.dest:
print(f" Using destination: {args.dest}")
detail(f" Using destination: {args.dest}")
plat, args.dest = _platform_for_dest(os_type, args.dest, interactive and not listing)
platforms = [(plat, args.dest)]
else:
print("Looking for setups here...")
detail("Looking for setups here...")
frontends = detect_frontends(os_type)
if "esde" in frontends:
print(" Found ES-DE (BIOS files go to each emulator, not to ES-DE itself)")
detail(" Found ES-DE (BIOS files go to each emulator, not to ES-DE itself)")
if "launchbox" in frontends:
print(" Found LaunchBox")
detail(" Found LaunchBox")
if not listing:
# A card mounted here is found as the screen finds it, in every
# mode: line mode said "No supported platform detected" beside
@@ -15553,7 +15600,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
offer = False
else:
for name, path in platforms:
print(fold_line(f" Found {found_label(name, path)}"))
detail(fold_line(f" Found {found_label(name, path)}"))
if len(platforms) > 1 and not interactive and not listing:
# A run without questions installs for one setup, the one a person
@@ -15674,11 +15721,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
if bios_path not in installed_paths:
installed_paths.append(bios_path)
if LOCAL.manifest is not None:
print(f"\nReading the file index for {plat_name} from {LOCAL.manifest}...")
detail(f"\nReading the file index for {plat_name} from {LOCAL.manifest}...")
elif LOCAL.pack is not None:
print(f"\nReading the file index for {plat_name} from {LOCAL.pack.name}...")
detail(f"\nReading the file index for {plat_name} from {LOCAL.pack.name}...")
else:
print(f"\nFetching file index for {plat_name}...")
detail(f"\nFetching file index for {plat_name}...")
manifest = fetch_manifest(plat_name)
categorized = categorized or bool(manifest.get("categorized"))
files = manifest.get("files", [])
@@ -15696,7 +15743,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
# spelling overwrote the first, and every --check found one
# of them wrong and fetched it again.
shown = ", ".join(folded[:3]) + (" ..." if len(folded) > 3 else "")
print(
detail(
f" {_plural(len(folded), 'file')} left out: {bios_path} does not keep "
f"case and holds another file under the same name ({shown})"
)
@@ -15704,7 +15751,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
and not holds_fat_refused_names(bios_path):
files, refused = split_fat_refused(files)
shown = ", ".join(refused[:3]) + (" ..." if len(refused) > 3 else "")
print(
detail(
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)})"
@@ -15722,7 +15769,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
if args.declared_only:
before = len(files)
files, omitted_files = keep_declared(files, omitted_files)
print(f" The platform's own list: {before} to {len(files)} files "
detail(f" The platform's own list: {before} to {len(files)} files "
"(--declared-only)")
if args.list_targets:
@@ -15761,7 +15808,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
words = (f"Board {board} {where} has no "
f"{PLATFORM_NAMES.get(plat_name, plat_name)} core list: "
"installing the files of every board")
print(f" {words}")
detail(f" {words}")
EVENTS.emit("board", platform=plat_name, board=board,
applied=target == board, message=words)
if target:
@@ -15777,7 +15824,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
# the subset the target needs. Refuse rather than ignore.
_unknown_target(target, plat_name, targets)
elif target_info.get("cores") is None:
print(
detail(
f" Target '{target}' publishes no core list: "
"installing every file"
)
@@ -15786,13 +15833,13 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
files, omitted_files = narrow_to_cores(
manifest, target_info["cores"], files, omitted_files
)
print(f" Filtered {before} to {len(files)} files for target {target}")
detail(f" Filtered {before} to {len(files)} files for target {target}")
found_cores = cores_found.get((plat_name, bios_path), [])
if "system_cores" not in manifest:
found_cores = []
elif found_cores:
print(f" {_plural(len(found_cores), 'core')} installed")
detail(f" {_plural(len(found_cores), 'core')} installed")
if args.installed_cores:
if "system_cores" not in manifest:
print(
@@ -15806,7 +15853,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
files, omitted_files = narrow_to_cores(
manifest, found_cores, files, omitted_files
)
print(f" Narrowed {before} to {len(files)} files to the installed cores")
detail(f" Narrowed {before} to {len(files)} files to the installed cores")
if args.list_emulators:
_print_emulators(plat_name, manifest, *(
@@ -15863,10 +15910,10 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
files, omitted_files = games_plan.apply(files, omitted_files, args.required_only)
unplaced = games_plan.apply([], unplaced, args.required_only)[1]
games_applied = True
print(f" For the games: {_plural(len(files), 'file')} of the list"
+ (f", {GAMES_REQUIRED_WORDS}" if args.required_only else ""))
detail(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. "
detail(" 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
@@ -15934,6 +15981,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
if args.no_game_data else "",
None if games_applied else games_plan,
)
# Its head named the BIOS folder: the result does not again.
OUTPUT.where_said = True
if selection is None and LOCAL.pack is not None:
smaller = " Nothing copied."
elif selection is None:
@@ -15972,7 +16021,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
if len(files) != before and not menu:
# Enter for every system narrows nothing: "Narrowed 23 to 23".
# The menu counted the choice again before Enter.
print(f" Narrowed {before} to {len(files)} files")
detail(f" Narrowed {before} to {len(files)} files")
if (cut and cut[0]) or args.no_game_data:
# Left out at g or by --no-game-data, from what the choice kept:
# g offered the game data of the systems chosen, and the line
@@ -15985,7 +16034,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
_INVOCATION.chosen[plat_name] = (_INVOCATION.chosen.get(plat_name, [])
+ [("--no-game-data", None)])
for line in _left_out_lines(left_out, args.verbose):
print(line)
detail(line)
if unplaced:
total_omitted += len(unplaced)
omitted_seen.extend(unplaced)
@@ -16022,18 +16071,18 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
if not menu:
# An answered menu said these lines right above: the run head
# repeated them under it.
print(f" {_plural(run_counts.files + head_data, 'file')} "
f"({format_size(run_counts.size + head_bytes)})")
detail(f" {_plural(run_counts.files + head_data, 'file')} "
f"({format_size(run_counts.size + head_bytes)})")
categorized_list = bool(manifest.get("categorized"))
kinds = run_counts if categorized_list else tally(
{"size": entry.get("size")} for entry in files)
parts = parts_words(kinds, head_data, head_bytes, game_data_cut)
if parts:
print(f" {parts}.")
detail(f" {parts}.")
defined = kinds_definition(
categorized_list and bool(run_counts.games or game_data_cut), bool(head_data))
if defined:
print(f" {defined}")
detail(f" {defined}")
# A choice that keeps no file says so where "All files up to date"
# was said of it.
empty_choice = "" if run_counts.files or not (selection or game_data_cut) else (
@@ -16051,7 +16100,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
for entry in omitted_files:
reason = entry.get("reason", "unknown")
reasons[reason] = reasons.get(reason, 0) + 1
print(f" {not_installable_words(len(omitted_files), required_omitted, reasons)}")
detail(f" {not_installable_words(len(omitted_files), required_omitted, reasons)}")
total_omitted += len(omitted_files)
# Named for the end of the run: ROCKNIX's own list leaves them out,
# and the end said "the frontend's save directory".
@@ -16071,11 +16120,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
files, library = romm_relayout(files, bios_path, *layout)
if library != bios_path:
installed_paths[installed_paths.index(bios_path)] = library
print(f" Firmware laid out as {'/'.join(layout[0] + ('{platform}',) + layout[1])} "
f"under {library}")
detail(f" Firmware laid out as {'/'.join(layout[0] + ('{platform}',) + layout[1])} "
f"under {library}")
bios_path = library
print("\nChecking existing files...")
detail("\nChecking existing files...")
to_download, up_to_date, mismatched = check_local(
files, bios_path, jobs=args.jobs, term=term
)
@@ -16090,9 +16139,9 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
head_reads_done()
trees_missing = sum(len(state[1]) for _entry, state in data_states)
trees_held = head_data - trees_missing
print(f" {len(up_to_date) + trees_held} in place, "
f"{run_counts.files - present + trees_missing} missing, "
f"{_plural(wrong, 'wrong copy')}")
detail(f" {len(up_to_date) + trees_held} in place, "
f"{run_counts.files - present + trees_missing} missing, "
f"{_plural(wrong, 'wrong copy')}")
# A partial copy left by an interrupted run is useless once the file
# is in place: it goes.
@@ -16157,14 +16206,18 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
# One total with the count above it, the data files among
# them as the screen counts them: "0 in place, 10805 missing"
# stood over "8164 files to download".
# The count and the folder by default, each file with
# --verbose: 20 paths hid the line saying what to do.
print(f"\n {_plural(len(to_download) + trees_missing, 'file')} "
+ ("not in place:" if offline or LOCAL.pack is not None
else "to download:"))
+ ("not in place" if offline or LOCAL.pack is not None
else "to download") + (":" if OUTPUT.verbose else "."))
for line in check_lines(missing, mismatched, files, manifest, bios_path,
args.verbose, up_to_date, bios_index, copies):
print(line)
detail(line)
for line in unreadable_lines(unread, args.verbose):
print(line)
if not OUTPUT.verbose:
print(f" BIOS folder: {shown_path(bios_path)}")
# The data files need the internet too: offline, "8164 files
# need the internet" stood under "10805 files not in place".
trees_fetched = trees_missing if LOCAL.pack is None else 0
@@ -16177,6 +16230,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
print(f"\n {_plural(trees_missing, 'data file')} to fetch")
else:
print(f"\n {EVERYTHING_IN_PLACE}")
print(f" BIOS folder: {shown_path(bios_path)}")
absent = _absent_required(omitted_files, bios_path)
# The collection cannot supply these, so they leave the exit
# code alone, but a user asking why a game will not start needs
@@ -16185,11 +16239,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
absent, "Not in the collection and absent here, needed to start an emulator",
every=args.verbose,
):
print(line)
detail(line)
for entry, (_root, needed, present) in data_states:
if needed:
print(f" {entry['ref']}: {present}/{len(entry['files'])} data files "
f"present, {len(needed)} to fetch")
detail(f" {entry['ref']}: {present}/{len(entry['files'])} data files "
f"present, {len(needed)} to fetch")
check_pending += len(needed)
continue
@@ -16280,15 +16334,28 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
files_pending += len(to_download)
continue
EVENTS.emit("download", platform=plat_name, files=len(to_download), bytes=dl_size)
if not menu:
# What happens and where, the two lines a run without questions
# opens on: the menu's head said them where it was asked.
coming = len(to_download) - len(lacking) + trees_missing
weight = dl_size + sum(data_fetch_bytes(entry, state[1])
for entry, state in data_states)
print(f"\nInstalling {_plural(coming, 'file')} ({format_size(weight)}) for "
f"{detected_os(plat_name, bios_path) or display}"
+ (", what the games need" if games_applied else "") + ".")
print(f"BIOS folder: {shown_path(bios_path)}")
OUTPUT.where_said = True
if unread:
# A file of the user's that could not be read is said in
# every mode: it was expected to be copied.
print()
for line in unreadable_lines(unread, args.verbose):
print(line)
if copies:
print()
detail()
for line in copy_plan_lines(to_download, copies, bios_index, bios_path,
files, args.verbose, False):
print(line)
detail(line)
held = len(fetched) - len(lacking)
rest = (f" and {'copying' if LOCAL.pack is not None else 'downloading'} "
f"{held}" if held else "")
@@ -16303,17 +16370,17 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
# The size beside what it weighs: after "and downloading
# 19" it read as theirs alone.
sized = f" ({format_size(dl_size)})"
print(f"\nCopying {_plural(len(copies), 'file')}{'' if rest else sized} "
f"{origin}{rest}{where}{sized if rest else ''}...")
detail(f"\nCopying {_plural(len(copies), 'file')}{'' if rest else sized} "
f"{origin}{rest}{where}{sized if rest else ''}...")
elif LOCAL.pack is not None:
# The files the pack holds: "Copying 23 files (85.0 KB)" sat
# beside the size of 20.
print(f"\nCopying {_plural(len(to_download) - len(lacking), 'file')} "
f"({format_size(dl_size)}) from {LOCAL.pack.name}...")
detail(f"\nCopying {_plural(len(to_download) - len(lacking), 'file')} "
f"({format_size(dl_size)}) from {LOCAL.pack.name}...")
else:
print(f"\nDownloading {len(to_download)} files ({format_size(dl_size)})...")
print(" Press Ctrl+C to stop. Running the same command again "
"carries on from here.")
detail(f"\nDownloading {len(to_download)} files ({format_size(dl_size)})...")
detail(" Press Ctrl+C to stop. Running the same command again "
"carries on from here.")
# Checked before the folder is created: a run stopped for space
# left an empty BIOS folder behind. The data trees count too,
# written after the files on the same drive.
@@ -16345,7 +16412,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
# of the run installed something.
sys.exit(1 if total_downloaded else 2)
if created:
print(f" Created {shown_path(bios_path)}")
detail(f" Created {shown_path(bios_path)}")
progress = Progress(len(to_download), dl_size, term)
failed, interrupted = download_files(
to_download, bios_path, jobs=args.jobs, verbose=args.verbose,
@@ -16404,7 +16471,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
1 if total_downloaded else 2,
on_its_card(plat_name, bios_path))
if writable:
print("\nData directories:")
detail("\nData directories:")
for entry, state in writable:
# Its archive is drawn as the BIOS files were, and counted
# in the bytes and time the run ends on: 82 MB came in
@@ -16424,9 +16491,9 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
if isinstance(counts["error"], OfflineReason):
# Left out as the BIOS files above are: counted missing,
# not as one failure per tree.
print(f" {entry['ref']}: {counts['present']} already in place, "
f"{agree(counts['absent'], 'needs', 'need')} the internet "
f"({offline_named()})")
detail(f" {entry['ref']}: {counts['present']} already in place, "
f"{agree(counts['absent'], 'needs', 'need')} the internet "
f"({offline_named()})")
total_offline += counts["absent"]
files_pending += counts["absent"]
continue
@@ -16442,7 +16509,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
f"{LOCAL.pack.name}" if LOCAL.pack is not None else
f", {counts['changed'] + counts['absent']} no longer in "
"the source (the pack has them)")
print(line)
detail(line)
if counts["error"] in (NOT_WRITABLE, DRIVE_FULL):
if counts["error"] == DRIVE_FULL:
print(f" {entry['ref']}: {DRIVE_FULL} ({shown_path(bios_path)})",
@@ -16461,8 +16528,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
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")
detail(f" Added {NOMEDIA} to {shown_path(bios_path)}: the gallery and music "
"apps leave its images, sounds and videos out")
# Standalone copies
if (
@@ -16490,11 +16557,11 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
manifest, os_type, launchbox_bios_dirs(lb_root) if lb_root else None
)
if folders:
print("\nStandalone emulator folders found here:")
detail("\nStandalone emulator folders found here:")
for folder in folders:
print(f" {folder}")
print(" --standalone-copies also copies the BIOS files and keys "
"they read into them.")
detail(f" {folder}")
detail(" --standalone-copies also copies the BIOS files and keys "
"they read into them.")
sys.stdout = real_stdout
if not listing:
+1 -1
View File
@@ -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="185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7"
DEFAULT_INSTALL_SHA256="00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e"
# The pin above refuses any changed byte: this cap only stops a body that
# cannot be the installer (a sign-in portal's page) before it is hashed.
MAX_INSTALLER_BYTES=8388608
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 1132111,
"sha256": "185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7",
"size": 1135666,
"sha256": "00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 1185736,
"sha256": "cb245dc33b894a9720ae3734bf55512c2b0fca99e0bd33e8c12a72738085f5cc"
"size": 1189291,
"sha256": "c56ce912e436173542905c89c1aee7de025f93bea5df130ab51e589c071f6369"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 1132111,
"sha256": "185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7",
"size": 1135666,
"sha256": "00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 1132111,
"sha256": "185fa0951fa9a7cce617547c72770819ef4df08ebb3b29857126713083179fe7",
"size": 1135666,
"sha256": "00376e0108c51fc9dbd27470080028be84251b1aa8d7bdfe28db3a71b2eae29e",
"executable": false
},
{
+15 -9
View File
@@ -444,14 +444,14 @@ class Sandbox:
with zipfile.ZipFile(system / "PSX BIOS.zip", "w") as archive:
archive.writestr("extra.bin", data["pcsx/extra.bin"])
# A check that flags a file ends on 3, as documented.
return self.run("retroarch", "--check", "--manifest", str(listed), home=home,
codes=(3,))
return self.run("retroarch", "--check", "--verbose", "--manifest", str(listed),
home=home, codes=(3,))
old = home / "old"
old.mkdir()
for dest in ("scph5500.bin", "scph5501.bin"):
(old / dest).write_bytes(data[dest])
return self.run("retroarch", "--yes", "--manifest", str(listed), "--from", str(old),
home=home, landed=True)
return self.run("retroarch", "--yes", "--verbose", "--manifest", str(listed),
"--from", str(old), home=home, landed=True)
def run(self, platform: str, *argv: str, landed: bool = False,
held: bool = False, home: Path | None = None, codes: tuple = (0,)) -> str:
@@ -551,7 +551,7 @@ BULK_SYSTEM = "sony-playstation"
def _bulk_line(box: Sandbox | _LazySandbox) -> str:
run = box.run("retroarch", "--dry-run", "--system", BULK_SYSTEM)
run = box.run("retroarch", "--dry-run", "--verbose", "--system", BULK_SYSTEM)
found = [line for line in run.splitlines() if re.match(r" \S+ pulls ", line)]
if len(found) != 1:
raise SampleFailed(f"a RetroArch run with --system {BULK_SYSTEM} names no core "
@@ -560,11 +560,17 @@ def _bulk_line(box: Sandbox | _LazySandbox) -> str:
def sample_writers(box: Sandbox) -> dict[str, Callable[[str], str]]:
"""The run samples, each from the run that prints it."""
install_run = lambda: box.run(WALKTHROUGH, "--yes", "--jobs", "1", landed=True) # noqa: E731
check_run = lambda: box.run(WALKTHROUGH, "--check", held=True) # noqa: E731
dry_run = lambda: box.run("retroarch", "--dry-run") # noqa: E731
"""The run samples, each from the run that prints it: the walk through a
run reads its --verbose lines, the default sample the lines it hides them
from."""
default_run = lambda: box.run(WALKTHROUGH, "--yes", "--jobs", "1", landed=True) # noqa: E731
install_run = lambda: box.run(WALKTHROUGH, "--yes", "--jobs", "1", "--verbose", # noqa: E731
landed=True)
check_run = lambda: box.run(WALKTHROUGH, "--check", "--verbose", held=True) # noqa: E731
dry_run = lambda: box.run("retroarch", "--dry-run", "--verbose") # noqa: E731
return {
"run-default": lambda _body: _fenced(
_lines_from(default_run(), "Installing ", "Next: ")),
"dry-run": lambda _body: (
f"A RetroArch dry run into an empty folder, with the list of "
f"{list_date('retroarch')}, printed this:\n\n"
+1 -1
View File
@@ -201,7 +201,7 @@ class InstallerSamplesSayWhatTheCodePrints(unittest.TestCase):
at = page.index("<!-- generated: run-download -->")
before = page[page.rindex("\n\n", 0, at - 2):at]
self.assertIn("At a terminal one progress line", before)
self.assertIn("A pipe or a log gets one line per file", before)
self.assertIn("a pipe or a log gets one\nline per file", before)
def test_the_generator_pages_name_the_run_samples_and_step_7(self):
tools = _page("tools.md")
+29 -15
View File
@@ -331,11 +331,16 @@ class TestRunOutcome(unittest.TestCase):
return buf.getvalue(), code
def test_success_names_the_location_and_the_way_to_check(self):
install.OUTPUT.verbose = install.OUTPUT.where_said = False
out, code = self._report(downloaded=12, up_to_date=3)
self.assertEqual(code, 0)
self.assertIn("12 files installed", out)
self.assertIn("/userdata/bios", out)
self.assertIn("--check", out)
self.assertIn("Next: ", out)
self.assertNotIn("--check", out)
self.addCleanup(setattr, install.OUTPUT, "verbose", False)
install.OUTPUT.verbose = True
self.assertIn("--check", self._report(downloaded=12, up_to_date=3)[0])
def test_total_failure_does_not_say_done_and_exits_two(self):
out, code = self._report(errors=7)
@@ -523,7 +528,12 @@ class AMissingDestIsSaid(unittest.TestCase):
def test_one_missing_level_or_no_terminal_is_said_not_asked(self):
root = Path(_tempdir(self))
# A "no" that was asked would stop the run with 2.
# A "no" that was asked would stop the run with 2. Said with
# --verbose: the folder is made, and the run names it.
install.OUTPUT.verbose = False
self.assertEqual(self._call(root / "bios", True, "n"), ("", None))
self.addCleanup(setattr, install.OUTPUT, "verbose", False)
install.OUTPUT.verbose = True
out, code = self._call(root / "bios", True, "n")
self.assertIsNone(code)
self.assertIn("does not exist", out)
@@ -1512,7 +1522,7 @@ class InstallerStaysOnPython38AndTheStdlib(unittest.TestCase):
check=False,
)
self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr)
self.assertIn("to download:", proc.stdout)
self.assertIn("to download.", proc.stdout)
def test_no_syntax_a_312_parser_lets_through(self):
"""Parenthesized context managers (3.9) and f-strings reusing their
@@ -2157,7 +2167,7 @@ class TestLaunchboxDetectionPowershell(unittest.TestCase):
proc = subprocess.run(
[
"pwsh", "-NoProfile", "-File",
str(REPO_ROOT / "install.ps1"), "--standalone-copies",
str(REPO_ROOT / "install.ps1"), "--standalone-copies", "--verbose",
],
env=env,
capture_output=True,
@@ -4163,7 +4173,7 @@ class DataDirectoriesAreInstalled(unittest.TestCase):
"files": {"Sys/big.bin": {"sha1": hashlib.sha1(member).hexdigest(),
"size": len(member)}}}]
(repo.root / "install" / "retroarch.json").write_text(json.dumps(listed))
proc = repo.run("--no-input")
proc = repo.run("--no-input", "--verbose")
bios = sum(len(payload) for payload, *_ in FakeRepository.PAYLOADS.values())
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn(f"{install.format_size(bios + archive)} downloaded in", proc.stdout)
@@ -4447,7 +4457,7 @@ class ACheckNamesWhatTheCollectionLacks(unittest.TestCase):
}
proc = subprocess.run(
[sys.executable, "-I", "install.py", "--platform", "recalbox",
"--no-input", "--dest", str(home / "bios"), "--check"],
"--no-input", "--dest", str(home / "bios"), "--check", "--verbose"],
cwd=REPO_ROOT, env=env, capture_output=True, text=True, timeout=300,
check=False,
)
@@ -4969,6 +4979,9 @@ class TheInstallerSaysWhatItFound(unittest.TestCase):
(dest / "lynxboot.img").write_bytes(b"corrupt")
proc = self._run("--dest", str(dest), "--system", "atari-lynx", "--check")
self.assertEqual(proc.returncode, 3)
self.assertIn("1 file to download.", proc.stdout)
self.assertNotIn("lynxboot.img", proc.stdout)
proc = self._run("--dest", str(dest), "--system", "atari-lynx", "--check", "--verbose")
self.assertIn("wrong copy: lynxboot.img", proc.stdout)
self.assertIn("without --check", proc.stdout)
@@ -4988,7 +5001,7 @@ class TheInstallerSaysWhatItFound(unittest.TestCase):
def test_a_piped_log_keeps_its_order(self):
proc = subprocess.run(
[sys.executable, "install.py", "--platform", "retroarch", "--no-input",
"--target", "no-such-target", "--dest", str(self.home / "s")],
"--verbose", "--target", "no-such-target", "--dest", str(self.home / "s")],
cwd=REPO_ROOT, env=self.env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, timeout=300, check=False,
)
@@ -5653,7 +5666,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun):
dest.mkdir()
(dest / "scph5501.bin").write_bytes(b"junk")
proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(dest),
"--check", "--no-input")
"--check", "--no-input", "--verbose")
self.assertEqual(proc.returncode, 3, proc.stderr)
listing = proc.stdout.split("to download:\n", 1)[1].splitlines()
self.assertEqual(listing[0].strip(), "wrong copy: scph5501.bin")
@@ -5685,7 +5698,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun):
"bios/Other/ppsspp/PPSSPP/ppge_atlas.meta"):
shutil.copy(REPO_ROOT / repo_path, dest / "PPSSPP")
proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(dest),
"--system", "sony-psp", "--check", "--no-input")
"--system", "sony-psp", "--check", "--no-input", "--verbose")
self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr)
self.assertNotIn("Every file on the list is in place", proc.stdout)
found = re.search(r"(\d+) in place, (\d+) missing", proc.stdout)
@@ -5699,6 +5712,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun):
dest.mkdir()
proc = _install_run({**self.env, "RETROBIOS_INSTALL_OFFLINE": "1"}, "--platform",
"retroarch", "--dest", str(dest), "--check", "--no-input",
"--verbose",
"--manifest", str(REPO_ROOT / "install" / "retroarch.json"))
self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr)
missing = re.search(r"0 in place, (\d+) missing", proc.stdout).group(1)
@@ -5711,7 +5725,7 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun):
(cores / "handy_libretro.so").write_bytes(b"")
proc = _install_run(self.env, "--platform", "retroarch", "--dest",
str(self.home / "s"), "--cores-dir", str(cores),
"--dry-run", "--no-input")
"--dry-run", "--no-input", "--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("to the installed cores", proc.stdout)
@@ -5912,7 +5926,7 @@ class StandaloneFoldersAreNamedWhenThereAreAny(unittest.TestCase, _LoopbackRun):
def _run(self) -> str:
proc = _install_run(self.env, "--platform", "retroarch", "--dest",
str(self.home / "system"), "--system", "atari-lynx",
"--no-input")
"--no-input", "--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
return proc.stdout
@@ -6661,7 +6675,7 @@ class RomMFirmwareInstallsUnderItsTemplate(unittest.TestCase, _LoopbackRun):
" firmware: \"{platform}/bios\"\n")
proc = _install_run(self.env, "--platform", "romm", "--dest",
str(base / "library" / "bios"), "--system", "atari-lynx",
"--no-input")
"--no-input", "--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertTrue((base / "library" / "lynx" / "bios" / "lynxboot.img").is_file())
self.assertFalse((base / "library" / "bios").exists())
@@ -6810,7 +6824,7 @@ class EventsDescribeTheRun(unittest.TestCase, _SyntheticTree):
SKIPPED = [{"reason": "not_found", "label": "not found", "files": 1, "required": 1}]
def test_a_check_run(self):
proc, events = self._run("--check")
proc, events = self._run("--check", "--verbose")
self.assertEqual(proc.returncode, 3)
installer = events[0]["installer"]
self.assertEqual(events, self._head("check", installer) + [
@@ -7448,7 +7462,7 @@ class KeptFilesEndToEnd(unittest.TestCase, _SyntheticTree):
def test_install_then_restore(self):
proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(self.bios),
"--no-input", "--jobs", "1")
"--no-input", "--jobs", "1", "--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertEqual((self.bios / "b.bin").read_bytes(), self.FILES["bios/A/b.bin"])
runs = list((self.home / install.BACKUP_DIR).iterdir())
@@ -7614,7 +7628,7 @@ class TheListVersionIsSaidBeforeAndAfter(unittest.TestCase):
"may show some files as not matching it.")
def test_line_mode_says_it_before_the_install_and_with_the_result(self):
proc = self.run_installer()
proc = self.run_installer("--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertEqual(proc.stdout.count(self.SAID), 2, proc.stdout)
self.assertLess(proc.stdout.index(self.SAID), proc.stdout.index("Checking existing"))
+2 -2
View File
@@ -483,7 +483,7 @@ class TheOneLinerReadsInstalledCores(unittest.TestCase):
def test_only_what_the_installed_cores_read(self):
with _CoreRepository() as repo:
repo.install_core("pcsx_rearmed_neon")
proc = repo.run("--installed-cores")
proc = repo.run("--installed-cores", "--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("1 core installed", proc.stdout)
self.assertEqual(
@@ -562,7 +562,7 @@ class TheInterfaceFindsTheCoresLineModeFinds(unittest.TestCase):
found = []
for given in ((), ("--dest", str(repo.dest))):
command = [sys.executable, str(REPO_ROOT / "install.py"), "--platform",
"retroarch", *given, "--installed-cores", "--check"]
"retroarch", *given, "--installed-cores", "--check", "--verbose"]
proc = subprocess.run(command, env=env, capture_output=True, text=True,
timeout=60, check=False, stdin=subprocess.DEVNULL)
self.assertNotIn("no installed core found", proc.stderr)
+14 -3
View File
@@ -308,6 +308,15 @@ class ProgressOutput(unittest.TestCase):
return stream, term
def test_a_log_gets_one_line_per_file_and_failures_say_why(self):
# A file that went well is a line of --verbose, a failure of every run.
install.OUTPUT.verbose = False
stream, term = self._term()
progress = install.Progress(2, 10, term)
progress.done("a.bin", True)
progress.done("b.bin", False, "HTTP 404")
self.assertEqual(stream.getvalue(), " FAILED b.bin (HTTP 404)\n")
self.addCleanup(setattr, install.OUTPUT, "verbose", False)
install.OUTPUT.verbose = True
stream, term = self._term()
progress = install.Progress(2, 10, term)
progress.done("a.bin", True)
@@ -329,6 +338,8 @@ class ProgressOutput(unittest.TestCase):
["0:00", "0:42", "3:05", "1:02:09"])
def test_the_summary_names_each_failure(self):
self.addCleanup(setattr, install.OUTPUT, "verbose", False)
install.OUTPUT.verbose = True
buf = io.StringIO()
with contextlib.redirect_stdout(buf), self.assertRaises(SystemExit):
install._report_outcome(
@@ -544,7 +555,7 @@ class AFolderThisUserCannotEnter(unittest.TestCase):
with FakeRepository() as repo:
locked = self._locked(repo)
try:
proc = repo.run("--check")
proc = repo.run("--check", "--verbose")
finally:
os.chmod(locked, 0o755)
self.assertNotIn("Traceback", proc.stderr)
@@ -671,7 +682,7 @@ class AHomeThisUserCannotOpen(unittest.TestCase):
("detected", [])):
runs[name] = subprocess.run(
[sys.executable, str(REPO_ROOT / "install.py"), "--check",
"--no-input", *extra],
"--no-input", "--verbose", *extra],
env=env, capture_output=True, text=True, timeout=60,
stdin=subprocess.DEVNULL)
finally:
@@ -864,7 +875,7 @@ class APartlyClosedHome(unittest.TestCase):
("events", ["--events"])):
runs[name] = subprocess.run(
[sys.executable, str(REPO_ROOT / "install.py"), "--check",
"--no-input", *extra],
"--no-input", "--verbose", *extra],
env=env, capture_output=True, text=True, timeout=60,
stdin=subprocess.DEVNULL)
finally:
+2 -2
View File
@@ -42,7 +42,7 @@ class AndroidMediaScanner(unittest.TestCase):
return self.repo, proc
def test_an_android_install_writes_one(self):
repo, proc = self._run("android")
repo, proc = self._run("android", "--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
marker = repo.dest / ".nomedia"
self.assertTrue(marker.is_file())
@@ -106,7 +106,7 @@ class NamesAFatCardCannotHold(unittest.TestCase):
"files": files}))
out = io.StringIO()
argv = ["install.py", "--platform", "retrodeck", "--dest", str(dest),
"--manifest", str(listed), "--from", str(old), "--no-input"]
"--manifest", str(listed), "--from", str(old), "--no-input", "--verbose"]
with mock.patch.object(install, "holds_fat_refused_names", return_value=holds), \
mock.patch.object(install, "detect_os", return_value="linux"), \
mock.patch("sys.argv", argv), contextlib.redirect_stdout(out), \
+4 -4
View File
@@ -219,8 +219,8 @@ class CopyFromAnOldCard(FromFolder):
def test_check_names_what_the_folder_would_give_and_writes_nothing(self):
self.keep("BIOS/SCPH5501.BIN", US)
self.keep("gba_bios.bin", GBA)
proc = self.run_installer("--check", "--manifest", str(self.list), "--from",
str(self.old))
proc = self.run_installer("--check", "--verbose", "--manifest", str(self.list),
"--from", str(self.old))
self.assertEqual(proc.returncode, 3, proc.stdout + proc.stderr)
self.assertIn(f"{self.old} holds 2 of these files: a run without --check "
"copies them.", proc.stdout)
@@ -376,7 +376,7 @@ class MisplacedInTheBiosFolder(Scratch):
def test_a_nested_bios_folder_is_fixed_and_left_in_place(self):
for name, data in PAYLOAD.items():
self.place(f"system/{name}", data)
proc = self.install()
proc = self.install("--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
for name, data in PAYLOAD.items():
self.assertEqual((self.dest / name).read_bytes(), data)
@@ -387,7 +387,7 @@ class MisplacedInTheBiosFolder(Scratch):
def test_a_pack_extracted_into_a_folder_of_its_own_is_fixed(self):
for name, data in PAYLOAD.items():
self.place(f"RetroArch_BIOS_Pack/{name}", data)
proc = self.install()
proc = self.install("--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("one folder too deep: it is at RetroArch_BIOS_Pack/scph5501.bin",
proc.stdout)
+3 -2
View File
@@ -833,7 +833,7 @@ class CommandLine(unittest.TestCase):
return [json.loads(line) for line in proc.stdout.splitlines() if line.startswith("{")]
def test_check_narrows_to_what_the_games_need(self):
proc = run_cli(self.card, "--for-games", "--check", "--events")
proc = run_cli(self.card, "--for-games", "--check", "--events", "--verbose")
events = self.events(proc)
games = next(e for e in events if e["event"] == "games")
self.assertEqual(games["games"], 2)
@@ -973,7 +973,8 @@ class ARunThatAsksNothing(unittest.TestCase):
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)
self.assertIn("what the games need", said)
self.assertIn("--all-files installs the whole list", self.install("--yes", "--verbose")[1])
def test_all_files_installs_the_whole_list(self):
everything, said = self.install("--all-files", "--yes")
+8 -4
View File
@@ -461,7 +461,7 @@ class ABoardWithoutTheNetwork(Scratch):
(self.root / "targets" / "retroarch.json").write_text(json.dumps(BOARDS))
self.complete()
proc = self.run_installer("--offline", "--check", "--manifest", str(self.list),
"--target", "psx-board", "--no-input")
"--target", "psx-board", "--no-input", "--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("for target psx-board", proc.stdout)
@@ -500,7 +500,7 @@ class FromPackInstallsOffline(Scratch):
for p in self.dest.rglob("*") if p.is_file()}
def test_the_pack_installs_with_no_network(self):
proc = self.run_installer("--from-pack", str(self.pack))
proc = self.run_installer("--from-pack", str(self.pack), "--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertEqual(self.installed(), PAYLOAD)
self.assertIn("Copying 4 files", proc.stdout)
@@ -571,7 +571,8 @@ class FromPackInstallsOffline(Scratch):
for rel, data in tree.items()},
}]
self.list.write_text(json.dumps(listed))
proc = self.run_installer("--from-pack", str(self.pack), "--manifest", str(self.list))
proc = self.run_installer("--from-pack", str(self.pack), "--manifest", str(self.list),
"--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("dolphin-sys: 2 installed", proc.stdout)
for rel, data in tree.items():
@@ -708,6 +709,8 @@ class TheSummaryNamesTheBulk(unittest.TestCase):
files, {"system_cores": {"3do": ["mame", "opera"]}}, True, True), [])
def test_the_end_summary_prints_the_line(self):
self.addCleanup(setattr, install.OUTPUT, "verbose", False)
install.OUTPUT.verbose = True
out = io.StringIO()
with contextlib.redirect_stdout(out):
install._report_outcome([Path("/x")], 3, 0, 0, 0, False,
@@ -950,7 +953,8 @@ class OfflineLeavesFilesMissing(Scratch):
}]
self.list.write_text(json.dumps(listed))
self.complete()
proc = self.run_installer("--offline", "--manifest", str(self.list), "--no-input")
proc = self.run_installer("--offline", "--manifest", str(self.list), "--no-input",
"--verbose")
# Its files are missing, as the BIOS files --offline leaves out are,
# with or without questions.
self.assertIn("bluemsx: 0 already in place, 1 needs the internet (--offline)",
+5 -5
View File
@@ -594,7 +594,7 @@ class FakeRepository:
class TheOneLinerNarrows(unittest.TestCase):
def test_options_install_only_the_choice(self):
with FakeRepository() as repo:
proc = repo.run("--system", "sony-playstation", "--region", "us")
proc = repo.run("--system", "sony-playstation", "--region", "us", "--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("Narrowed 5 to 2 files", proc.stdout)
self.assertEqual(repo.installed(), ["pcsx/extra.bin", "scph5501.bin"])
@@ -1606,14 +1606,14 @@ class ThePlatformsOwnListAlone(unittest.TestCase):
def test_install_check_and_dry_run_apply_it(self):
with FakeRepository() as repo:
proc = repo.run("--declared-only")
proc = repo.run("--declared-only", "--verbose")
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("5 to 3 files", proc.stdout)
self.assertEqual(repo.installed(),
["gba_bios.bin", "scph5500.bin", "scph5501.bin"])
for mode in ("--check", "--dry-run"):
with self.subTest(mode=mode), FakeRepository() as repo:
proc = repo.run("--declared-only", mode)
proc = repo.run("--declared-only", mode, "--verbose")
self.assertIn("5 to 3 files", proc.stdout, proc.stderr)
self.assertIn("0 in place, 3 missing", proc.stdout)
self.assertEqual(repo.installed(), [])
@@ -1633,7 +1633,7 @@ class ThePlatformsOwnListAlone(unittest.TestCase):
def test_a_local_file_list_applies_it(self):
with FakeRepository() as repo:
proc = repo.run("--declared-only", "--dry-run", "--manifest",
proc = repo.run("--declared-only", "--dry-run", "--verbose", "--manifest",
str(repo.root / "install" / "retroarch.json"))
self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
self.assertIn("5 to 3 files", proc.stdout)
@@ -1778,7 +1778,7 @@ class SomethingElseAtADestination(unittest.TestCase):
def test_each_says_why_in_the_check_and_the_install(self):
with FakeRepository() as repo:
self.prepare(repo)
proc = repo.run("--check")
proc = repo.run("--check", "--verbose")
self.assertEqual(proc.returncode, 3, proc.stderr)
self.assertNotIn("Traceback", proc.stderr)
self.assertIn(" wrong copy: scph5500.bin\n a folder sits where this "
+15 -6
View File
@@ -7017,6 +7017,8 @@ def line_decision(args: argparse.Namespace, answer) -> tuple[list, str]:
def stop(_args, platforms):
raise _Decided(platforms)
# --verbose's text, which names what detection found.
args.verbose = True
install.LOCAL.reset()
with mock.patch.object(install, "app_refusal", side_effect=stop), \
mock.patch.object(install, "detect_os", return_value="linux"), \
@@ -7798,7 +7800,7 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings,
(backup / "gba_bios.bin").write_bytes(b"gba")
(backup / "doom" / "doom1.wad").write_bytes(b"wad" * 10)
args = _args(manifest=listing, dest=self.here, offline=True, declared_only=False,
no_input=True)
no_input=True, verbose=True)
setattr(args, "from", [backup])
real = io.StringIO()
install.LOCAL.reset()
@@ -7965,7 +7967,7 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings,
old = self.root / "OLD"
old.mkdir()
(old / "gba_bios.bin").write_bytes(payload)
args = _args(manifest=listing, declared_only=False, offline=True)
args = _args(manifest=listing, declared_only=False, offline=True, verbose=True)
setattr(args, "from", [old])
real = io.StringIO()
install.LOCAL.reset()
@@ -8047,7 +8049,10 @@ class LineModeAndScreenDecideAlike(_LineModeFindings, _OutsideTextFindings,
def line_run(self, args: argparse.Namespace, answers: list, mounts: list = (),
found: list = ()) -> tuple[str, list]:
"""(text, prompts) of line mode at a terminal for args, the answers
given in turn: the run stops at the first question left unanswered."""
given in turn: the run stops at the first question left unanswered.
Its text is --verbose's: these findings read the lines the default
leaves out."""
args.verbose = True
given, asked = iter(answers), []
def answer(prompt: str) -> str:
@@ -12619,7 +12624,7 @@ class ChoiceStateInBothModes(unittest.TestCase):
self.assertNotIn("to fetch them", text)
self.assertIn("to copy 1 of them from RetroArch_BIOS_Pack.zip", text)
options = ("--platform", "retroarch", "--manifest", str(listing), "--dest", str(dest),
"--yes")
"--yes", "--verbose")
proc = install_run(*options, "--from-pack", str(pack))
text = proc.stdout + proc.stderr
self.assertIn("Copying 1 file (1 B) from RetroArch_BIOS_Pack.zip...", text)
@@ -12774,7 +12779,7 @@ class ChoiceStateInBothModes(unittest.TestCase):
files = [self.entry("gba_bios.bin", b"gba", ["nintendo-gba"])]
listing = self.listing("retroarch", files)
proc = install_run("--platform", "retroarch", "--manifest", str(listing),
"--dest", str(self.here), "--check", "--offline")
"--dest", str(self.here), "--check", "--offline", "--verbose")
self.assertIn("0 in place, 1 missing, 0 wrong copies", proc.stdout)
self.assertNotIn("present (", proc.stdout)
source = (REPO_ROOT / "install.py").read_text(encoding="utf-8")
@@ -13232,6 +13237,8 @@ class OneVocabularyOneCount(unittest.TestCase):
return listing, dest, old
def run_line(self, listing: Path, dest: Path, answers: list, **given) -> str:
# --verbose's text: the counts compared are lines the default leaves out.
given.setdefault("verbose", True)
from_ = given.pop("from_", None)
args = _args(manifest=listing, dest=dest, declared_only=False, **given)
if from_:
@@ -13626,7 +13633,9 @@ class ProvenCardsAndCounts(unittest.TestCase):
def line_from(self, args: argparse.Namespace, answers: list,
folders: list[Path] = ()) -> tuple[str, list]:
"""(text, prompts) of line mode at a terminal, the result included,
the folders --from names read first, as main() reads them."""
the folders --from names read first, as main() reads them: --verbose's
text."""
args.verbose = True
given, asked = iter(answers), []
def answer(prompt: str) -> str:
+23 -6
View File
@@ -449,6 +449,23 @@ Batocera install into an empty folder, from the list of 2026-11-06.
The figures change with each release.
<!-- /generated: run-date -->
By default a run says five things: what it installs, where, a progress line
at a terminal, the result and the next step.
<!-- generated: run-default -->
```
Installing 18175 files (9.2 GB) for Batocera.
BIOS folder: /userdata/bios
Done. 18175 files installed.
Next: start Batocera and play.
```
<!-- /generated: run-default -->
With `--verbose` (`-v`) it says each step, the lists and the reasons too.
The lines below are those of the same run with `--verbose`.
### Before the run
The size depends on the platform and changes with each release. The run prints
@@ -473,8 +490,8 @@ Looking for setups here...
<!-- /generated: run-first-lines -->
A line above them, `RetroBIOS` and a code, identifies the installer: the
code is the start of the SHA-256 the command checked before running it. The
path is the folder it will write to.
code is the start of the SHA-256 the command checked before running it.
`--version` prints it too. The path is the folder it will write to.
### The file index and the download
@@ -501,8 +518,8 @@ is already there, and only the rest is downloaded. A download of 1 GB or more
is confirmed first, the size and the folder in the question.
The second phase downloads what is missing. At a terminal one progress line
moves with the count and the size. A pipe or a log gets one line per file,
as the log of the run below:
moves with the count and the size. With `--verbose` a pipe or a log gets one
line per file, as the log of the run below:
<!-- generated: run-download -->
@@ -537,8 +554,8 @@ To check this later, run the same command with --check.
<!-- /generated: run-end -->
When most of a download of 100 MB or more comes from one system, or from
one core of a single system, the end of the run says so and what narrowing
would save, once that saving is over half.
one core of a single system, the end of a `--verbose` run says so and what
narrowing would save, once that saving is over half.
<!-- generated: bulk-hint -->