diff --git a/install.ps1 b/install.ps1 index d53d32866..d60007075 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 = "e818db12a40eb26da591c605351349983861dc35e7cdb98993bb12ee1922d91b" + $defaultInstallSha256 = "aa9407fbbe2d7a7b2ac78407272b96b1d9befb177eea64a5a084c73a0100dee9" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 4c66501ff..d5824b476 100644 --- a/install.py +++ b/install.py @@ -13636,6 +13636,30 @@ def _missing_dest(dest: Path, ask: bool, named: str = "named with --dest", return True +def _check_cores_dir(folder: Path) -> None: + """Refuse a --cores-dir that cannot be listed, saying why: a typo + (core/ for cores/) or a closed folder read as an empty one, and the + run said "none in" and asked for the --cores-dir it was given.""" + try: + with os.scandir(folder): + return + except FileNotFoundError: + why = "does not exist" + except NotADirectoryError: + why = "is not a folder" + except PermissionError as exc: + closed = closed_folder(folder) + why = (f"cannot be opened by this user ({exc.strerror or exc})" + if closed is None or closed[0] == folder + else f"cannot be opened: {closed_folder_words(folder, closed)}") + except OSError as exc: + why = f"could not be read ({exc.strerror or exc})" + print(f"Error: --cores-dir {screen_text(str(folder))} {why}.", file=sys.stderr) + print(" Name the folder RetroArch loads cores from (libretro_directory in " + "retroarch.cfg, cores/ beside RetroArch by default).", file=sys.stderr) + sys.exit(1) + + def _no_core_found(plat: str, bios_path: Path, searched: list[Path]) -> None: """Refuse --installed-cores with no core, and say how to point at them. @@ -14967,6 +14991,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: cores_found: dict[tuple[str, Path], list[str]] = {} for plat_name, bios_path in platforms: if plat_name in CORE_PLATFORMS: + if args.cores_dir: + _check_cores_dir(args.cores_dir) searched = [args.cores_dir] if args.cores_dir else core_dirs(plat_name, bios_path) cores_found[(plat_name, bios_path)] = installed_cores(searched) elif args.installed_cores: diff --git a/install.sh b/install.sh index e62e835c6..841e60b9d 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="e818db12a40eb26da591c605351349983861dc35e7cdb98993bb12ee1922d91b" +DEFAULT_INSTALL_SHA256="aa9407fbbe2d7a7b2ac78407272b96b1d9befb177eea64a5a084c73a0100dee9" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/install/launchers/index.json b/install/launchers/index.json index 371e49d0c..2b470c65b 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": 1090483, - "sha256": "e818db12a40eb26da591c605351349983861dc35e7cdb98993bb12ee1922d91b", + "size": 1091637, + "sha256": "aa9407fbbe2d7a7b2ac78407272b96b1d9befb177eea64a5a084c73a0100dee9", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 1144110, - "sha256": "5d9347427326038bf925f778d724f87cdafca03e50017123af3cd0eb9939cb7c" + "size": 1145264, + "sha256": "3d17d78050a0fe0181712084cb7bfbc258be08853ffcb7da897628e8a9e9f545" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 1090483, - "sha256": "e818db12a40eb26da591c605351349983861dc35e7cdb98993bb12ee1922d91b", + "size": 1091637, + "sha256": "aa9407fbbe2d7a7b2ac78407272b96b1d9befb177eea64a5a084c73a0100dee9", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 1090483, - "sha256": "e818db12a40eb26da591c605351349983861dc35e7cdb98993bb12ee1922d91b", + "size": 1091637, + "sha256": "aa9407fbbe2d7a7b2ac78407272b96b1d9befb177eea64a5a084c73a0100dee9", "executable": false }, { diff --git a/tests/test_install.py b/tests/test_install.py index 079d4d45b..f928fb395 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -4668,6 +4668,37 @@ class RunsAgainstTheRealLists(unittest.TestCase, _LoopbackRun): self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) self.assertIn("to the installed cores", proc.stdout) + def test_a_cores_folder_that_cannot_be_listed_says_why(self): + """A typo (core/ for cores/) or a closed folder read as an empty + one: the run said "none in" and asked for the --cores-dir given.""" + missing = self.home / "core" + a_file = self.home / "cores.txt" + a_file.write_text("") + for folder, why in ((missing, "does not exist"), (a_file, "is not a folder")): + with self.subTest(why=why): + proc = _install_run(self.env, "--platform", "retroarch", "--dest", + str(self.home / "s"), "--cores-dir", str(folder), + "--dry-run", "--no-input") + self.assertEqual(proc.returncode, 1) + self.assertIn(f"--cores-dir {folder} {why}", proc.stderr) + self.assertNotIn("none in", proc.stderr) + + @unittest.skipIf(os.name == "nt" or os.geteuid() == 0, + "a folder without read permission is POSIX") + def test_a_closed_cores_folder_says_so(self): + cores = self.home / "closed" + cores.mkdir() + (cores / "handy_libretro.so").write_bytes(b"") + os.chmod(cores, 0o000) + try: + proc = _install_run(self.env, "--platform", "retroarch", "--dest", + str(self.home / "s"), "--cores-dir", str(cores), + "--dry-run", "--no-input") + finally: + os.chmod(cores, 0o755) + self.assertEqual(proc.returncode, 1) + self.assertIn(f"--cores-dir {cores} cannot be opened by this user", proc.stderr) + def test_no_core_found_says_how_to_go_on(self): proc = _install_run(self.env, "--platform", "retroarch", "--dest", str(self.home / "s"), "--installed-cores", "--dry-run", diff --git a/wiki/installer.md b/wiki/installer.md index 0b59e4444..cb4dbbdd9 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -961,7 +961,7 @@ what the options name, everything by default. | `--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, Lakka and KNULLI the board is read from the system (`batocera.arch`, `recalbox.arch`, `HW_DEVICE`, `LIBREELEC_DEVICE` or `LIBREELEC_PROJECT`, `knulli.arch`) and applied when no `--target` is given, from a pack alone too when it carries the core list per board. On a computer, the board a Batocera card names on its BATOCERA partition (`boot/batocera.board`) is applied the same way, in line mode and on the full screen | | `--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) | -| `--cores-dir PATH` | The folder RetroArch loads its cores from, for `--installed-cores` when it is not found. Implies `--installed-cores` | +| `--cores-dir PATH` | The folder RetroArch loads its cores from, for `--installed-cores` when it is not found. Implies `--installed-cores`. A folder that does not exist or cannot be opened is refused with the reason (code 1) | | `--save-dir PATH` | The frontend's save directory (RetroArch's `savefile_directory`), for the files some cores read there, when the run cannot find it. The core's own folder is still added below it when the frontend sorts saves by core | | `--system NAMES` | Install only these systems. Comma-separated or repeated. `--list-systems` prints the names, `other` included, and the title a system is printed with is taken too. A short name the platform does not use (`gba`, `psx`) is refused with the ids it may stand for, never taken in their place. So is an id the list files under another one: RetroArch keeps `neogeo.zip` under `arcade`, which `--system snk-neogeo` suggests | | `--core NAMES` | Keep the platform's own list and only the extra files these cores load. `--list-cores` prints the names |