From 153ba479f61a2c333f1350bdd313d12083b5e1c5 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:58:32 +0200 Subject: [PATCH] fix: assume no default where detection was refused The Flatpak RetroArch's folder at 000 hid the retroarch.cfg naming ~/Games/bios, and the run offered 6.6 GB to ~/.config/retroarch/system. A closed folder under HOME or above the default now leaves no default: --dest is asked for. Python 3.14's quiet Path.exists() refusals are told from absence by os.stat, and an assumed folder reads as the one a frontend usually reads. --- install.ps1 | 2 +- install.py | 52 ++++++++++----- install.sh | 2 +- tests/fixtures/tui/folder-120x40.txt | 2 +- tests/fixtures/tui/folder-40x12.txt | 4 +- tests/fixtures/tui/folder-40x15.txt | 4 +- tests/fixtures/tui/folder-80x24.txt | 2 +- tests/fixtures/tui/route-evmapy.txt | 10 +-- tests/fixtures/tui/route-gptokeyb.txt | 10 +-- tests/fixtures/tui/route-gptokeyb_muxterm.txt | 10 +-- tests/fixtures/tui/route-keys.txt | 10 +-- tests/fixtures/tui/route-miyoo.txt | 10 +-- tests/fixtures/tui/route-muxterm.txt | 10 +-- tests/test_install_experience.py | 64 ++++++++++++++++++- 14 files changed, 137 insertions(+), 55 deletions(-) diff --git a/install.ps1 b/install.ps1 index 88e16f93..24449bae 100644 --- a/install.ps1 +++ b/install.ps1 @@ -11,7 +11,7 @@ $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" - $defaultInstallSha256 = "bb0a0a88e16a6fb7aa93ec0a2b4cb5f1d667faacd50830aae9ce763e60d5d112" + $defaultInstallSha256 = "be9d37048e800072d6d5d2a40afaa5a46d5b062577f83c0fa633fc15159de66a" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index deaba930..35dc4518 100644 --- a/install.py +++ b/install.py @@ -4254,10 +4254,20 @@ def _probed(probe: Callable[[], bool], path: Path) -> bool: """probe() for path, False where the place cannot be looked at. A refusal met while looking for setups is said once (_note_refused).""" try: - return probe() + found = probe() except OSError as exc: _note_refused(path, exc) return False + if not found: + # Python 3.14's Path.exists() answers False where earlier ones + # raised: a refusal is told from absence by the stat it hides. + try: + os.stat(path) + except PermissionError as exc: + _note_refused(path, exc) + except OSError: + pass + return found def _exists(path: Path) -> bool: @@ -4284,7 +4294,7 @@ def closed_folder(path: Path) -> tuple[Path, str] | None: first line said where. """ try: - path.exists() + os.stat(path) return None except OSError as exc: failure = exc @@ -6269,17 +6279,27 @@ def _default_dest(os_type: str, platform_name: str) -> Path | None: C:\\userdata\\bios, reported done. """ if os_type == "android" and platform_name == "retroarch": - return _android_storage_root() / "RetroArch" / "system" - if platform_name in PORTABLE_ONLY: + default = _android_storage_root() / "RetroArch" / "system" + elif platform_name in PORTABLE_ONLY or (os_type == "windows" and platform_name + not in _os_default_dests(os_type)): return None - own = _os_default_dests(os_type).get(platform_name) - if own is not None: - return own - if os_type == "windows": + else: + default = (_os_default_dests(os_type).get(platform_name) + or (None if platform_name == "retrobat" else DEFAULT_DESTS.get(platform_name))) + if default is not None and _refused_near(default): + # The Flatpak's own folder at 000 hid the retroarch.cfg naming + # ~/Games/bios, and 6.6 GB were offered to a folder it does not read. return None - if platform_name == "retrobat": - return None - return DEFAULT_DESTS.get(platform_name) + return default + + +def _refused_near(default: Path) -> bool: + """Whether detection met a closed folder under HOME, where configs that + could name another folder live, or above the default itself.""" + home = Path.home() + place = default.absolute() + return any(home in (folder, *folder.parents) or folder in (place, *place.parents) + for folder, _why in _DETECTION_REFUSED) def _manual_usage_hint(os_type: str, listing: str = "") -> list[str]: @@ -9219,10 +9239,12 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: else: default_dest = _default_dest(os_type, args.platform) if default_dest is None: + where = ("where this user can look, and its default folder may not be " + "the one it reads" if _DETECTION_REFUSED + else "and no folder on this system is one it reads") print( - f" Platform '{args.platform}' not found, and no folder on this " - f"system is one it reads. Name it with --dest, for example: " - f"--platform {args.platform} --dest ", + f" Platform '{args.platform}' not found {where}. Name it with " + f"--dest, for example: --platform {args.platform} --dest ", file=sys.stderr, ) sys.exit(1) @@ -11986,7 +12008,7 @@ def _view_folder(state: TuiState, width: int, height: int) -> tuple: "root folder, the one holding .system."] elif state.folder_default is not None: title = "Which folder?" - before = [f"{name} reads its BIOS files from:", str(state.folder_default)] + before = [f"{name} usually reads its BIOS files from:", str(state.folder_default)] else: title = "Which folder?" before = [f"No usual {name} folder here: browse for it or type it."] diff --git a/install.sh b/install.sh index 2ac14608..99e21a37 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="bb0a0a88e16a6fb7aa93ec0a2b4cb5f1d667faacd50830aae9ce763e60d5d112" +DEFAULT_INSTALL_SHA256="be9d37048e800072d6d5d2a40afaa5a46d5b062577f83c0fa633fc15159de66a" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/tests/fixtures/tui/folder-120x40.txt b/tests/fixtures/tui/folder-120x40.txt index 1aefeaee..8a4d2501 100644 --- a/tests/fixtures/tui/folder-120x40.txt +++ b/tests/fixtures/tui/folder-120x40.txt @@ -1,6 +1,6 @@ RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files from: /home/user/.config/retroarch/system > Use this folder diff --git a/tests/fixtures/tui/folder-40x12.txt b/tests/fixtures/tui/folder-40x12.txt index e560d2ce..fb144e50 100644 --- a/tests/fixtures/tui/folder-40x12.txt +++ b/tests/fixtures/tui/folder-40x12.txt @@ -1,6 +1,7 @@ RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files + from: /home/user/.config/retroarch/system > Use this folder @@ -8,5 +9,4 @@ Type a path - A Use this folder B Back ? Help diff --git a/tests/fixtures/tui/folder-40x15.txt b/tests/fixtures/tui/folder-40x15.txt index 360bf2a8..5ff74269 100644 --- a/tests/fixtures/tui/folder-40x15.txt +++ b/tests/fixtures/tui/folder-40x15.txt @@ -1,6 +1,7 @@ RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files + from: /home/user/.config/retroarch/system > Use this folder @@ -11,5 +12,4 @@ - A Use this folder B Back ? Help diff --git a/tests/fixtures/tui/folder-80x24.txt b/tests/fixtures/tui/folder-80x24.txt index 7ed7c112..cb4a819b 100644 --- a/tests/fixtures/tui/folder-80x24.txt +++ b/tests/fixtures/tui/folder-80x24.txt @@ -1,6 +1,6 @@ RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files from: /home/user/.config/retroarch/system > Use this folder diff --git a/tests/fixtures/tui/route-evmapy.txt b/tests/fixtures/tui/route-evmapy.txt index ef4a7dee..179fa02f 100644 --- a/tests/fixtures/tui/route-evmapy.txt +++ b/tests/fixtures/tui/route-evmapy.txt @@ -109,7 +109,8 @@ === folder 40x12 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files + from: /home/user/.config/retroarch/system > Use this folder @@ -117,12 +118,12 @@ Type a path - A Use this folder B Back === folder 40x15 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files + from: /home/user/.config/retroarch/system > Use this folder @@ -133,12 +134,11 @@ - A Use this folder B Back === folder 80x24 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files from: /home/user/.config/retroarch/system > Use this folder diff --git a/tests/fixtures/tui/route-gptokeyb.txt b/tests/fixtures/tui/route-gptokeyb.txt index 43ada349..94646027 100644 --- a/tests/fixtures/tui/route-gptokeyb.txt +++ b/tests/fixtures/tui/route-gptokeyb.txt @@ -109,7 +109,8 @@ === folder 40x12 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files + from: /home/user/.config/retroarch/system > Use this folder @@ -117,12 +118,12 @@ Type a path - A Use this folder B Back === folder 40x15 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files + from: /home/user/.config/retroarch/system > Use this folder @@ -133,12 +134,11 @@ - A Use this folder B Back === folder 80x24 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files from: /home/user/.config/retroarch/system > Use this folder diff --git a/tests/fixtures/tui/route-gptokeyb_muxterm.txt b/tests/fixtures/tui/route-gptokeyb_muxterm.txt index 43ada349..94646027 100644 --- a/tests/fixtures/tui/route-gptokeyb_muxterm.txt +++ b/tests/fixtures/tui/route-gptokeyb_muxterm.txt @@ -109,7 +109,8 @@ === folder 40x12 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files + from: /home/user/.config/retroarch/system > Use this folder @@ -117,12 +118,12 @@ Type a path - A Use this folder B Back === folder 40x15 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files + from: /home/user/.config/retroarch/system > Use this folder @@ -133,12 +134,11 @@ - A Use this folder B Back === folder 80x24 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files from: /home/user/.config/retroarch/system > Use this folder diff --git a/tests/fixtures/tui/route-keys.txt b/tests/fixtures/tui/route-keys.txt index 9c2e5336..f2bed0ac 100644 --- a/tests/fixtures/tui/route-keys.txt +++ b/tests/fixtures/tui/route-keys.txt @@ -109,7 +109,8 @@ === folder 40x12 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files + from: /home/user/.config/retroarch/system > Use this folder @@ -117,12 +118,12 @@ Type a path - Enter Use this folder Esc Back === folder 40x15 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files + from: /home/user/.config/retroarch/system > Use this folder @@ -133,12 +134,11 @@ - Enter Use this folder Esc Back === folder 80x24 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files from: /home/user/.config/retroarch/system > Use this folder diff --git a/tests/fixtures/tui/route-miyoo.txt b/tests/fixtures/tui/route-miyoo.txt index 259b7828..99103df1 100644 --- a/tests/fixtures/tui/route-miyoo.txt +++ b/tests/fixtures/tui/route-miyoo.txt @@ -109,7 +109,8 @@ === folder 40x12 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files + from: /home/user/.config/retroarch/system > Use this folder @@ -117,12 +118,12 @@ Type a path - A Use this folder R2 Back === folder 40x15 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files + from: /home/user/.config/retroarch/system > Use this folder @@ -133,12 +134,11 @@ - A Use this folder R2 Back === folder 80x24 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files from: /home/user/.config/retroarch/system > Use this folder diff --git a/tests/fixtures/tui/route-muxterm.txt b/tests/fixtures/tui/route-muxterm.txt index f23f8568..7c360043 100644 --- a/tests/fixtures/tui/route-muxterm.txt +++ b/tests/fixtures/tui/route-muxterm.txt @@ -109,7 +109,8 @@ === folder 40x12 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files + from: /home/user/.config/retroarch/system > Use this folder @@ -117,12 +118,12 @@ Type a path - Start Use this folder B Back === folder 40x15 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files + from: /home/user/.config/retroarch/system > Use this folder @@ -133,12 +134,11 @@ - Start Use this folder B Back === folder 80x24 RetroBIOS: Which folder? - RetroArch reads its BIOS files from: + RetroArch usually reads its BIOS files from: /home/user/.config/retroarch/system > Use this folder diff --git a/tests/test_install_experience.py b/tests/test_install_experience.py index 97d98039..4cba5a56 100644 --- a/tests/test_install_experience.py +++ b/tests/test_install_experience.py @@ -13,6 +13,7 @@ import http.server import io import json import os +import shutil import subprocess import sys import tempfile @@ -686,8 +687,67 @@ class AHomeThisUserCannotOpen(unittest.TestCase): self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr) said = " ".join(proc.stderr.split()) self.assertNotIn("the one detected here", said) - self.assertIn("than the one taken as RetroArch's default, RetroArch not being " - "found here (name another with --dest).", said) + # HOME closed, the default is not assumed at all: --dest is asked for. + self.assertIn("Platform 'retroarch' not found where this user can look", said) + self.assertNotIn("using default path", proc.stdout + proc.stderr) + + +class TheFrontendsOwnFolderClosed(unittest.TestCase): + """The Flatpak RetroArch's folder at 000: its retroarch.cfg names + ~/Games/bios, and the run took ~/.config/retroarch/system, a folder it + does not read, and offered 6.6 GB there. A folder detection could not + look in leaves no default to assume.""" + + @unittest.skipIf(os.name == "nt" or os.geteuid() == 0, + "a folder without search permission is POSIX") + def test_no_default_is_assumed(self): + with FakeRepository() as repo: + home = repo.root / "home" + flatpak = home / ".var" / "app" / "org.libretro.RetroArch" + cfg = flatpak / "config" / "retroarch" / "retroarch.cfg" + cfg.parent.mkdir(parents=True) + cfg.write_text(f'system_directory = "{home / "Games" / "bios"}"\n') + env = {**repo.env(), "HOME": str(home), "RETROBIOS_OS": "linux"} + os.chmod(flatpak, 0o000) + try: + proc = subprocess.run( + [sys.executable, str(REPO_ROOT / "install.py"), "--platform", "retroarch", + "--dry-run", "--no-input"], + env=env, capture_output=True, text=True, timeout=60, + stdin=subprocess.DEVNULL) + finally: + os.chmod(flatpak, 0o755) + self.assertNotIn("Traceback", proc.stderr) + self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr) + self.assertNotIn("using default path", proc.stdout + proc.stderr) + self.assertIn(f"this user cannot open {flatpak}", proc.stderr) + self.assertIn("--dest", proc.stderr) + + +class ARefusalTheProbeDoesNotRaise(unittest.TestCase): + """Python 3.14's Path.exists() answers False on a folder this user + cannot enter instead of raising: every closed-folder line went silent + and the run took the default again.""" + + @unittest.skipIf(os.name == "nt" or os.geteuid() == 0, + "a folder without search permission is POSIX") + def test_a_quiet_false_is_still_told_from_absence(self): + base = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp")) + self.addCleanup(shutil.rmtree, base, True) + closed = base / "closed" + closed.mkdir() + os.chmod(closed, 0o000) + self.addCleanup(os.chmod, closed, 0o755) + target = closed / "retroarch.cfg" + install._DETECTION_REFUSED.clear() + self.addCleanup(install._DETECTION_REFUSED.clear) + quiet = mock.patch.object(install.Path, "exists", lambda self: False) + with quiet, mock.patch.object(install._SETUP_SEARCH, "active", True, create=True), \ + contextlib.redirect_stderr(io.StringIO()): + self.assertFalse(install._exists(target)) + self.assertFalse(install._exists(base / "absent")) + self.assertEqual(install.closed_folder(target), (closed, "Permission denied")) + self.assertEqual([folder for folder, _why in install._DETECTION_REFUSED], [closed]) class APartlyClosedHome(unittest.TestCase):