From 2ecbea56b54a1f267e5c4f098026eb8be50bf44d Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 03:44:17 +0100 Subject: [PATCH] fix: skip data trees a read-only folder refuses A BIOS folder this user cannot write no longer tries each data tree's archive and blames its URL with a raw errno. The tree is counted failed for the same cause as the BIOS files, so the summary keeps its one line and its advice. --- install.ps1 | 2 +- install.py | 25 ++++++++++++++++++++++--- install.sh | 2 +- install/launchers/index.json | 16 ++++++++-------- tests/test_install_offline.py | 27 +++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 13 deletions(-) diff --git a/install.ps1 b/install.ps1 index 425d1554c..a96745599 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 = "78ef177611168c1c7699cafa8982f901aaaf5342b6c55d6e158ba18fb4cc0ae2" + $defaultInstallSha256 = "c8cf4b65474279c30c2e1781028d3e05f195e26dd759a98a37db3477b6dd3014" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index ab13fccdf..fe18bfa61 100644 --- a/install.py +++ b/install.py @@ -7198,7 +7198,12 @@ def install_data_directory(entry: dict, bios_path: Path, # Kept apart, so the advice is the offline one, not the network's. counts["error"] = OfflineReason(str(exc)) except (urllib.error.URLError, OSError, ValueError, tarfile.TarError) as exc: - counts["error"] = str(getattr(exc, "reason", None) or exc) + # The tree's folder refused, not the source: said as the BIOS files + # say it, never as an archive that could not be read. + counts["error"] = (NOT_WRITABLE if isinstance(exc, OSError) + and not isinstance(exc, urllib.error.URLError) + and _write_refused(exc) and not _folder_writable(root) + else str(getattr(exc, "reason", None) or exc)) except _MEMBER_ERRORS as exc: # A damaged, encrypted or unsupported member, in words. counts["error"] = unreadable_words(exc) @@ -15838,8 +15843,19 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: # PPSSPP's assets. Fetched from their source archive, each file # checked against the hash the list records. if data_dirs and not total_interrupted: - print("\nData directories:") + # A tree whose folder this user cannot write is not fetched: the + # BIOS files above already said the cause and its remedy, and + # each archive was named as unreadable. + writable = [] for entry, state in data_states: + if state[1] and not _folder_writable(state[0]): + failures.append((entry["ref"], NOT_WRITABLE)) + total_errors += 1 + else: + writable.append((entry, state)) + if writable: + print("\nData directories:") + for entry, state in writable: counts = install_data_directory(entry, bios_path, state) total_downloaded += counts["installed"] data_landed += counts["installed"] @@ -15866,7 +15882,10 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: f", {counts['changed'] + counts['absent']} no longer in " "the source (the pack has them)") print(line) - if counts["error"]: + if counts["error"] == NOT_WRITABLE: + failures.append((entry["ref"], NOT_WRITABLE)) + total_errors += 1 + elif counts["error"]: print(f" {entry['ref']}: could not read " f"{LOCAL.pack.name if LOCAL.pack is not None else entry['url']}: " f"{counts['error']}", file=sys.stderr) diff --git a/install.sh b/install.sh index 2acffc1b5..556c6aec4 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="78ef177611168c1c7699cafa8982f901aaaf5342b6c55d6e158ba18fb4cc0ae2" +DEFAULT_INSTALL_SHA256="c8cf4b65474279c30c2e1781028d3e05f195e26dd759a98a37db3477b6dd3014" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/install/launchers/index.json b/install/launchers/index.json index 56277c1c3..3cba41420 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": 1100388, - "sha256": "78ef177611168c1c7699cafa8982f901aaaf5342b6c55d6e158ba18fb4cc0ae2", + "size": 1101457, + "sha256": "c8cf4b65474279c30c2e1781028d3e05f195e26dd759a98a37db3477b6dd3014", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 1154015, - "sha256": "03bcbdf8385808576afb0899860e5f11af37a9aee709b72481064e7a0ee79afa" + "size": 1155084, + "sha256": "41b72bf029de9d855312e472cc9a10046f71692a55e02c4a959efdf4201505ad" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 1100388, - "sha256": "78ef177611168c1c7699cafa8982f901aaaf5342b6c55d6e158ba18fb4cc0ae2", + "size": 1101457, + "sha256": "c8cf4b65474279c30c2e1781028d3e05f195e26dd759a98a37db3477b6dd3014", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 1100388, - "sha256": "78ef177611168c1c7699cafa8982f901aaaf5342b6c55d6e158ba18fb4cc0ae2", + "size": 1101457, + "sha256": "c8cf4b65474279c30c2e1781028d3e05f195e26dd759a98a37db3477b6dd3014", "executable": false }, { diff --git a/tests/test_install_offline.py b/tests/test_install_offline.py index dd6849439..b46ea29be 100644 --- a/tests/test_install_offline.py +++ b/tests/test_install_offline.py @@ -951,6 +951,33 @@ class OfflineLeavesFilesMissing(Scratch): self.assertNotIn("could not read", proc.stderr) self.assertNotIn("no longer in the source", proc.stdout) + @unittest.skipIf(os.name == "nt" or os.geteuid() == 0, + "a read-only folder is refused by POSIX permissions") + def test_a_tree_in_a_folder_not_writable_is_not_fetched(self): + """A read-only card blamed each data archive's URL with a raw + errno, and that second cause split the one-line summary.""" + listed = manifest() + listed["data_directories"] = [{ + "ref": "bluemsx", "dest": "Machines", "type": "zip", + "url": "https://example.invalid/blueMSX.zip", "systems": ["nintendo-gba"], + "files": {"a.txt": {"sha1": _sha1(b"a"), "size": 1}}, + }, { + "ref": "fbneo-cheats", "dest": "fbneo/cheats", "type": "zip", + "url": "https://example.invalid/cheats.zip", "systems": ["nintendo-gba"], + "files": {"b.txt": {"sha1": _sha1(b"b"), "size": 1}}, + }] + self.list.write_text(json.dumps(listed)) + self.complete() + self.dest.chmod(0o555) + self.addCleanup(self.dest.chmod, 0o755) + proc = self.run_installer("--manifest", str(self.list), "--no-input") + out = proc.stdout + proc.stderr + self.assertEqual(proc.returncode, 2, out) + self.assertNotIn("could not read", out) + self.assertNotIn("Errno", out) + self.assertNotIn("Data directories:", out) + self.assertIn("failed: 2 files (folder not writable), first bluemsx", out) + class OneOfflineVocabulary(Scratch): """--offline said one