diff --git a/install.ps1 b/install.ps1 index 55444d30e..087b4755c 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 = "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 diff --git a/install.py b/install.py index a3ad31f83..5d50497f8 100644 --- a/install.py +++ b/install.py @@ -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: diff --git a/install.sh b/install.sh index c3482d606..36aaf3311 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="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 diff --git a/install/launchers/index.json b/install/launchers/index.json index 3cda2e006..c056dc24c 100644 --- a/install/launchers/index.json +++ b/install/launchers/index.json @@ -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 }, { diff --git a/scripts/generate_installer_doc.py b/scripts/generate_installer_doc.py index 8414de2e0..7baf85733 100644 --- a/scripts/generate_installer_doc.py +++ b/scripts/generate_installer_doc.py @@ -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" diff --git a/tests/test_doc_pages.py b/tests/test_doc_pages.py index 0b58a1f26..bcfda2ef0 100644 --- a/tests/test_doc_pages.py +++ b/tests/test_doc_pages.py @@ -201,7 +201,7 @@ class InstallerSamplesSayWhatTheCodePrints(unittest.TestCase): at = page.index("") 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") diff --git a/tests/test_install.py b/tests/test_install.py index 0d25cae85..eba661cfa 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -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")) diff --git a/tests/test_install_cores.py b/tests/test_install_cores.py index 7594890fe..f33f24cb0 100644 --- a/tests/test_install_cores.py +++ b/tests/test_install_cores.py @@ -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) diff --git a/tests/test_install_experience.py b/tests/test_install_experience.py index 6d7ca9a83..6ffcbb1b0 100644 --- a/tests/test_install_experience.py +++ b/tests/test_install_experience.py @@ -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: diff --git a/tests/test_install_filesystems.py b/tests/test_install_filesystems.py index 4eb66519b..8392dc2e4 100644 --- a/tests/test_install_filesystems.py +++ b/tests/test_install_filesystems.py @@ -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), \ diff --git a/tests/test_install_from.py b/tests/test_install_from.py index 2b36b408e..f36d9125d 100644 --- a/tests/test_install_from.py +++ b/tests/test_install_from.py @@ -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) diff --git a/tests/test_install_games.py b/tests/test_install_games.py index ab1507fbe..cf45f3059 100644 --- a/tests/test_install_games.py +++ b/tests/test_install_games.py @@ -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") diff --git a/tests/test_install_offline.py b/tests/test_install_offline.py index c9226b185..5994f1a4d 100644 --- a/tests/test_install_offline.py +++ b/tests/test_install_offline.py @@ -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)", diff --git a/tests/test_install_selection.py b/tests/test_install_selection.py index 852699169..a6e37379f 100644 --- a/tests/test_install_selection.py +++ b/tests/test_install_selection.py @@ -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 " diff --git a/tests/test_install_tui.py b/tests/test_install_tui.py index 9c42e9650..785e379f8 100644 --- a/tests/test_install_tui.py +++ b/tests/test_install_tui.py @@ -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: diff --git a/wiki/installer.md b/wiki/installer.md index 42c69341a..5ce100f68 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -449,6 +449,23 @@ Batocera install into an empty folder, from the list of 2026-11-06. The figures change with each release. +By default a run says five things: what it installs, where, a progress line +at a terminal, the result and the next step. + + + +``` +Installing 18175 files (9.2 GB) for Batocera. +BIOS folder: /userdata/bios + +Done. 18175 files installed. +Next: start Batocera and play. +``` + + +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... 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: @@ -537,8 +554,8 @@ To check this later, run the same command with --check. 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.