diff --git a/install.ps1 b/install.ps1 index 233fa84f..17921a9d 100644 --- a/install.ps1 +++ b/install.ps1 @@ -8,7 +8,7 @@ param( $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -$defaultInstallSha256 = "26f2b2fc61c828de3eceec4cc6ba90d10801b3c97a88531adf9b8307a3866e2e" +$defaultInstallSha256 = "300fcfd077c970a6807b2414b19cbae87b330664f3f7092d8275f5da2fe6e022" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 91692084..af1a72dd 100644 --- a/install.py +++ b/install.py @@ -2765,11 +2765,21 @@ def _report_outcome( print("Run the same command again to carry on from here. Nothing is lost.") sys.exit(130) - refused = any(reason == NOT_WRITABLE for _dest, reason in failures or []) + # A standalone copy fails by absolute path, in an emulator's folder + # rather than the BIOS folder: that folder is where the write went. + elsewhere = sorted({Path(dest).parent for dest, _reason in failures or [] + if Path(dest).is_absolute()}) + refused = any(reason == NOT_WRITABLE for dest, reason in failures or [] + if not Path(dest).is_absolute()) advice = [ _write_advice(path, path in (assumed or set()), (platform_of or {}).get(path, "")) for path in paths ] if refused else [] + advice += [_write_advice(folder, False) for folder in sorted({ + Path(dest).parent for dest, reason in failures or [] + if Path(dest).is_absolute() and reason == NOT_WRITABLE + })] + paths = paths + [folder for folder in elsewhere if folder not in paths] if errors and not downloaded: print(f"\nNothing was installed. {_plural(errors, 'file')} failed.") @@ -3110,6 +3120,7 @@ def select_data_directories(manifest: dict, files: list[dict], narrowed: bool) - def do_standalone_copies( manifest: dict, bios_path: Path, os_type: str, extra_dirs: dict[str, Path] | None = None, + failures: list[tuple[str, str]] | None = None, ) -> tuple[int, int, int]: """Copy BIOS files to standalone emulator directories. @@ -3126,6 +3137,8 @@ def do_standalone_copies( Returns (copied, skipped, failed): skipped is a target directory that does not exist, failed a copy the system refused (permission, disk full, read-only mount), which used to be counted as "dir not found". + Each failure is also appended to failures, by absolute path, for the + summary to name. """ from fnmatch import fnmatch @@ -3200,6 +3213,10 @@ def do_standalone_copies( except OSError as exc: print(f" ERROR: {dest}: {exc.strerror or exc}") failed += 1 + if failures is not None: + reason = NOT_WRITABLE if _write_refused(exc) else str( + exc.strerror or exc) + failures.append((str(dest), reason)) return copied, skipped, failed @@ -4135,7 +4152,7 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: lb_root = launchbox_root(os_type) extra_dirs = launchbox_bios_dirs(lb_root) if lb_root else None copied, skipped, copy_failed = do_standalone_copies( - manifest, bios_path, os_type, extra_dirs + manifest, bios_path, os_type, extra_dirs, failures ) if copied or skipped or copy_failed: print( diff --git a/install.sh b/install.sh index 0f038281..d47910d6 100755 --- a/install.sh +++ b/install.sh @@ -18,7 +18,7 @@ esac TEMP_INSTALLER="" TEMP_DIRECTORY="" DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -DEFAULT_INSTALL_SHA256="26f2b2fc61c828de3eceec4cc6ba90d10801b3c97a88531adf9b8307a3866e2e" +DEFAULT_INSTALL_SHA256="300fcfd077c970a6807b2414b19cbae87b330664f3f7092d8275f5da2fe6e022" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/tests/test_install.py b/tests/test_install.py index 1c685f64..4ab9f361 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -2852,6 +2852,43 @@ class ACheckNamesWhatTheCollectionLacks(unittest.TestCase): +class ARefusedStandaloneCopyIsNamed(unittest.TestCase): + """A standalone copy refused by a read-only emulator folder counted as + an error with no failures entry: the summary named no file, pointed at + the BIOS folder and gave no advice.""" + + def test_the_file_and_its_folder_reach_the_summary(self): + import contextlib # noqa: PLC0415 + + if os.name == "nt" or os.geteuid() == 0: + self.skipTest("a read-only folder needs POSIX modes and a non-root user") + bios = Path(_tempdir(self)) + (bios / "scph5501.bin").write_bytes(b"x") + target = Path(_tempdir(self)) / "pcsx2-bios" + target.mkdir() + target.chmod(0o500) + self.addCleanup(target.chmod, 0o700) + manifest = {"standalone_copies": [ + {"file": "scph5501.bin", "emulator": "pcsx2", + "targets": {"linux": [str(target)]}}, + ]} + failures: list = [] + with contextlib.redirect_stdout(io.StringIO()): + _copied, _skipped, failed = install.do_standalone_copies( + manifest, bios, "linux", None, failures) + self.assertEqual(failed, 1) + self.assertEqual(failures, [(str(target / "scph5501.bin"), install.NOT_WRITABLE)]) + + out = io.StringIO() + with contextlib.redirect_stdout(out), self.assertRaises(SystemExit): + install._report_outcome([bios], 0, 1, failed, 0, False, failures=failures) + text = out.getvalue() + self.assertIn(str(target / "scph5501.bin"), text) + self.assertIn(str(target), text.split("Location")[1]) + self.assertIn("cannot write there", text) + + + class ThePagesQuoteTheBootstrapsOwnWords(unittest.TestCase): """The installer page and the FAQ quoted "install.py SHA-256 mismatch", a message the bootstraps no longer print."""