fix: say when --dest does not exist

A mistyped --dest was created with every missing parent and reported done. The run now names the missing folder and the nearest one that exists, asks at a terminal before creating more than one level, and prints the folder it created. The free-space check now runs before the folder is made and ends with 2, nothing installed.
This commit is contained in:
Abdessamad Derraz committed 2026-10-21 10:01:34 +02:00
1 parent 6c4f056f08
commit ff8a390ce6
5 files changed
+92 -11

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "108b168f54fdc795322152b655fcca3793968cc8e4629287e64f2bd51fa13aa7"
$defaultInstallSha256 = "6e7005671e99ab965e815ba96bb6c6c0542667012d9b42bd139d2e3e0f06d40c"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+39 -5
View File
@@ -3515,11 +3515,13 @@ def _print_plan(
print("\n Nothing was written. Run without --dry-run to install.")
def _check_free_space(bios_path: Path, needed: int, plat: str = "") -> None:
def _check_free_space(bios_path: Path, needed: int, plat: str = "", code: int = 2) -> None:
"""Stop before writing when the volume cannot hold the download.
Reported against the nearest existing ancestor, since the destination
itself may have just been created on a volume that is already full.
may not exist yet. The run ends with code: 2 when nothing was installed,
the status a script reads as "nothing installed", 1 after an earlier
platform of the run installed files.
"""
probe = bios_path
while not probe.exists() and probe != probe.parent:
@@ -3536,13 +3538,13 @@ def _check_free_space(bios_path: Path, needed: int, plat: str = "") -> None:
if not plat:
print(f" Free up {format_size(needed - free)}, or use --dest to install "
"to another drive.", file=sys.stderr)
sys.exit(1)
sys.exit(code)
# --dest alone used to mean RetroArch's list: the platform is named, so
# a Batocera install moved to a USB drive stays Batocera's.
print(f" Free up {format_size(needed - free)}, or install to another drive:\n"
f" {_INVOCATION.command(plat, '<folder on another drive>')}",
file=sys.stderr)
sys.exit(1)
sys.exit(code)
def download_files(
@@ -4611,6 +4613,30 @@ def main() -> None:
sys.stdout = real_stdout
def _missing_dest(dest: Path, ask: bool) -> None:
"""Say that a --dest folder does not exist before anything goes there.
A typo (~/.config/retroarh/system) was created with every missing
parent, filled, and reported done in a folder no platform reads, and
--check said only "0/7 present". One missing level is a BIOS folder not
made yet and is created. Several make a path that may be mistyped:
at a terminal the run asks first, and the answer defaults to no.
"""
if dest.exists():
return
nearest = dest.absolute()
while not nearest.exists() and nearest.parent != nearest:
nearest = nearest.parent
missing = len(dest.absolute().relative_to(nearest).parts)
print(f" {dest} does not exist. The nearest folder that does: {nearest}")
if missing > 1 and ask:
answer = _ask(f" Create {_plural(missing, 'folder')} under {nearest}? [y/N] ")
if not answer.strip().lower().startswith("y"):
print(" Nothing installed. Check the folder named with --dest, then run "
"the command again.")
sys.exit(2)
def _no_core_found(plat: str, bios_path: Path, searched: list[Path], dest_given: bool) -> None:
"""Refuse --installed-cores with no core, and say how to point at them.
@@ -4767,6 +4793,9 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
if len(platforms) > 1 and not listing and interactive:
platforms = _prompt_platform_choice(platforms)
if args.dest is not None and not listing:
_missing_dest(args.dest, interactive and not (args.check or args.dry_run or args.yes))
total_downloaded = 0
total_up_to_date = 0
total_errors = 0
@@ -5108,6 +5137,10 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
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.")
# Checked before the folder is created: a run stopped for space
# left an empty BIOS folder behind.
_check_free_space(bios_path, dl_size, plat_name, 1 if total_downloaded else 2)
created = not bios_path.exists()
try:
bios_path.mkdir(parents=True, exist_ok=True)
except OSError as exc:
@@ -5120,7 +5153,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
# as when it exists read-only; 1 once an earlier platform
# of the run installed something.
sys.exit(1 if total_downloaded else 2)
_check_free_space(bios_path, dl_size, plat_name)
if created:
print(f" Created {bios_path}")
progress = Progress(len(to_download), dl_size, term)
failed, interrupted = download_files(
to_download, bios_path, jobs=args.jobs, verbose=args.verbose,
+1 -1
View File
@@ -18,7 +18,7 @@ esac
TEMP_INSTALLER=""
TEMP_DIRECTORY=""
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
DEFAULT_INSTALL_SHA256="108b168f54fdc795322152b655fcca3793968cc8e4629287e64f2bd51fa13aa7"
DEFAULT_INSTALL_SHA256="6e7005671e99ab965e815ba96bb6c6c0542667012d9b42bd139d2e3e0f06d40c"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+49 -2
View File
@@ -376,8 +376,10 @@ class TestFreeSpaceGuard(unittest.TestCase):
self.assertEqual((out, code), ("", 0))
def test_short_of_space_names_both_numbers_and_stops(self):
"""Nothing was installed: code 2, as the exit table says, not the 1
of a partial install."""
out, code = self._run(free=1_000, needed=5_000)
self.assertEqual(code, 1)
self.assertEqual(code, 2)
self.assertIn("Needed", out)
self.assertIn("Free", out)
self.assertIn("--dest", out)
@@ -385,7 +387,52 @@ class TestFreeSpaceGuard(unittest.TestCase):
def test_probe_climbs_to_an_existing_ancestor(self):
missing = self.root / "not" / "created" / "yet"
out, code = self._run(free=1, needed=2, path=missing)
self.assertEqual(code, 1)
self.assertEqual(code, 2)
def test_the_space_check_comes_before_the_folder(self):
source = (REPO_ROOT / "install.py").read_text(encoding="utf-8")
check = source.index("_check_free_space(bios_path, dl_size")
self.assertLess(check, source.index("bios_path.mkdir(parents=True, exist_ok=True)"))
class AMissingDestIsSaid(unittest.TestCase):
"""--dest ~/.config/retroarh/system (a typo) was created with every
missing parent and reported done, and --check on it said only
"0/7 present"."""
def _call(self, dest: Path, ask: bool, answer: str = "") -> tuple[str, object]:
out = io.StringIO()
code = None
with contextlib.redirect_stdout(out), \
unittest.mock.patch("builtins.input", return_value=answer):
try:
install._missing_dest(dest, ask)
except SystemExit as exc:
code = exc.code
return out.getvalue(), code
def test_an_existing_folder_says_nothing(self):
root = Path(_tempdir(self))
self.assertEqual(self._call(root, True), ("", None))
def test_a_deep_missing_path_is_asked_and_no_is_the_default(self):
root = Path(_tempdir(self))
dest = root / "retroarh" / "system"
out, code = self._call(dest, True, "")
self.assertEqual(code, 2)
self.assertIn(f"The nearest folder that does: {root}", out)
self.assertFalse((root / "retroarh").exists())
self.assertEqual(self._call(dest, True, "y")[1], None)
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.
out, code = self._call(root / "bios", True, "n")
self.assertIsNone(code)
self.assertIn("does not exist", out)
out, code = self._call(root / "a" / "b", False, "n")
self.assertIsNone(code)
self.assertIn("does not exist", out)
class AFolderThatRefusesWritesIsNamed(unittest.TestCase):
+2 -2
View File
@@ -228,7 +228,7 @@ what the options name, everything by default.
| Option | Effect |
|--------|--------|
| `--platform NAME` | Install for this platform instead of the detected one. Unknown names are refused with the available list |
| `--dest PATH` | Destination directory, overriding detection. Without `--platform`, the file list is the platform detected here: at a terminal the installer asks, Enter taking the one found, and otherwise it uses the one found, RetroArch's when nothing is found, and refuses when several are. The run prints whose list it is |
| `--dest PATH` | Destination directory, overriding detection. Without `--platform`, the file list is the platform detected here: at a terminal the installer asks, Enter taking the one found, and otherwise it uses the one found, RetroArch's when nothing is found, and refuses when several are. The run prints whose list it is. A folder that does not exist is named with the nearest one that does, then created. When more than its last level is missing (a mistyped path), a run at a terminal asks first, and the answer defaults to no |
| `--target NAME` | Keep only the files the cores of that hardware target need, and the systems they emulate. An unknown target is refused rather than ignored, since carrying on would install everything. On Batocera, Recalbox, ROCKNIX and Lakka the board is read from the system (`batocera.arch`, `recalbox.arch`, `HW_DEVICE`, `LIBREELEC_DEVICE` or `LIBREELEC_PROJECT`) and applied when no `--target` is given |
| `--every-board` | Install the files of every board instead of the one the system names, for a card that moves between devices |
| `--installed-cores` | Keep only what the cores already installed read (RetroArch, Lakka, RetroPie). See [Installed cores](#installed-cores) |
@@ -538,7 +538,7 @@ fields, the installer refuses to narrow rather than install everything.
|------|---------|
| `0` | everything asked for is installed or already up to date |
| `1` | a failure: the one-liner could not download or verify `install.py`, or found no Python 3.8 (nothing was run), an unusable base URL, an unknown platform or target, `--installed-cores` with no core found, a manifest that fails validation, a fetch that never succeeded, no platform to install for in a non-interactive run, some files failed while others installed, or `--check` found files to fetch |
| `2` | nothing was installed: every file the run tried failed (a BIOS folder that cannot be written, for one). Also an option the installer refused before any download: an unknown option, `--jobs` outside 1 to 32, `--target` with `--every-board`. The message then starts with `install.py: error:` |
| `2` | nothing was installed: every file the run tried failed (a BIOS folder that cannot be written, for one), the drive has too little free space for the download (checked before any folder is created), or the question about a `--dest` folder that does not exist was answered no. Also an option the installer refused before any download: an unknown option, `--jobs` outside 1 to 32, `--target` with `--every-board`. The message then starts with `install.py: error:` |
| `130` | stopped with Ctrl+C, or with Ctrl+C or Ctrl+D at a question. Run the same command again to carry on |
| `141` | the output was piped to a reader that closed early (`--list-platforms \| head`) |