diff --git a/install.ps1 b/install.ps1 index f7957297..33733458 100644 --- a/install.ps1 +++ b/install.ps1 @@ -11,7 +11,7 @@ $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" - $defaultInstallSha256 = "1eb1aed96cab57bf3bc4ef2918bf1ef40cafcb1de157af2e5fcd5a23ae69a443" + $defaultInstallSha256 = "86c4e8b2a67b5721020dd3629212c741a67a3b3180e8f62fb61bba333303a5b6" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 4c2e6006..21e945c3 100644 --- a/install.py +++ b/install.py @@ -3186,6 +3186,20 @@ class Invocation: _INVOCATION = Invocation() +def _on_read_only_mount(path: Path) -> bool: + """Whether the file system holding path, or the folder it would go in, + is mounted read-only (a card Linux remounts read-only after an error).""" + if not hasattr(os, "statvfs"): + return False + probe = path + while not probe.exists() and probe.parent != probe: + probe = probe.parent + try: + return bool(os.statvfs(probe).f_flag & os.ST_RDONLY) + except OSError: + return False + + def _write_advice(path: Path, assumed: bool, plat: str = "") -> str: """What to do about a folder this run could not write. @@ -3193,8 +3207,16 @@ def _write_advice(path: Path, assumed: bool, plat: str = "") -> str: machine it would create that folder on the wrong disk: a platform named but not found here is a card or share mounted elsewhere, named by --dest. Windows has no sudo, and Termux reaches shared storage once - termux-setup-storage has run. + termux-setup-storage has run. A read-only mount refuses root as well, + so sudo is never offered for it. """ + if _on_read_only_mount(path): + again = f"\n {_INVOCATION.command(plat, path)}" if plat else "" + return (f" {path} is on a file system mounted read-only, which no user can " + "write, sudo included. Mount it read-write again (a card or drive " + "the system switched to read-only after an error needs a check " + "first: fsck, or chkdsk on Windows), then run the same command " + f"again.{again}") if assumed: return (f" {path} is where the platform keeps its BIOS on its own system, " "and it was not found here. If this machine prepares its card or " diff --git a/install.sh b/install.sh index e503005b..b61ac077 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="1eb1aed96cab57bf3bc4ef2918bf1ef40cafcb1de157af2e5fcd5a23ae69a443" +DEFAULT_INSTALL_SHA256="86c4e8b2a67b5721020dd3629212c741a67a3b3180e8f62fb61bba333303a5b6" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/tests/test_install.py b/tests/test_install.py index 80774740..602d2b6d 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -2425,6 +2425,23 @@ class SudoAdviceNamesTheFolder(unittest.TestCase): source = (REPO_ROOT / "install.py").read_text(encoding="utf-8") self.assertIn("platform_of={path: plat for plat, path in platforms}", source) + @unittest.skipUnless(hasattr(os, "statvfs"), "statvfs reads mount flags") + def test_a_read_only_mount_is_not_sent_to_sudo(self): + """A card Linux mounted read-only after a dirty unplug was answered + with 'run it again with sudo', which fails the same way.""" + real = os.statvfs(".") + flags = SimpleNamespace(f_flag=real.f_flag | os.ST_RDONLY) + for plat in ("retroarch", ""): + with self.subTest(platform=plat), \ + unittest.mock.patch.object(install.os, "statvfs", return_value=flags): + advice = install._write_advice(Path("/media/card/bios"), False, plat) + self.assertIn("mounted read-only", advice) + self.assertNotIn("Run it again with sudo", advice) + self.assertNotIn("sudo python3", advice) + writable = SimpleNamespace(f_flag=real.f_flag & ~os.ST_RDONLY) + with unittest.mock.patch.object(install.os, "statvfs", return_value=writable): + self.assertIn("sudo", install._write_advice(Path("/media/card/bios"), False, "retroarch")) + class CaseVariants(unittest.TestCase): diff --git a/wiki/troubleshooting.md b/wiki/troubleshooting.md index 4b2ced03..ce099d3b 100644 --- a/wiki/troubleshooting.md +++ b/wiki/troubleshooting.md @@ -272,6 +272,10 @@ The installer needs write access to the target directory. - On Linux/macOS: check directory ownership (`ls -la`) and run with appropriate permissions. Avoid running as root unless the target directory requires it. - On Windows: run PowerShell as Administrator if installing to a protected directory. +- A file system mounted read-only refuses root too, so the installer does not + suggest sudo for it. Linux remounts a card or drive read-only after an error + (an unclean removal, for one): check it (`fsck`, or `chkdsk` on Windows) and + mount it read-write again. A file already in the BIOS folder with the read-only attribute is replaced like any other: on Windows the installer clears the attribute before putting