diff --git a/install.ps1 b/install.ps1 index 79c59f28a..cd200b066 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 = "450cee7625f098d5796452b02ab9c8bffdeaaef89180d2254bbe07ce354bc359" + $defaultInstallSha256 = "5bce9f0f81e4f1c7302ca78fa5242f26acc06523f8718378ee97042f2421ff78" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 082994522..5f372b33f 100644 --- a/install.py +++ b/install.py @@ -1401,6 +1401,49 @@ def _macos_retroarch_base() -> Path: return Path.home() / "Documents" / "RetroArch" +def _retroarch_appimage_dirs(home: Path) -> list[Path]: + """The system folder of each RetroArch AppImage kept with a portable + folder, up to two folders below home. + + The buildbot's Linux build unpacks RetroArch-Linux-x86_64.AppImage beside + RetroArch-Linux-x86_64.AppImage.home/.config/retroarch. The AppImage + runtime sets HOME to .home and XDG_CONFIG_HOME to + .config when those folders exist (type2-runtime fedfad51, + src/runtime/runtime.c:1449-1462), so this RetroArch never reads + ~/.config/retroarch. The archive holds no retroarch.cfg: until the first + run writes one, the system folder is the base folder's (frontend_unix_get_env + in platform_unix.c). + """ + images: list[Path] = [] + for depth in ("", "*/", "*/*/"): + for suffix in (".home", ".config"): + try: + hits = sorted(home.glob(f"{depth}*.AppImage{suffix}")) + except OSError: + continue + for hit in hits: + image = hit.with_name(hit.name[:-len(suffix)]) + if image not in images and _is_file(image): + images.append(image) + dirs: list[Path] = [] + for image in images: + portable_home = image.with_name(image.name + ".home") + portable_config = image.with_name(image.name + ".config") + if _is_dir(portable_config): + base = portable_config / "retroarch" + elif _is_dir(portable_home) and "XDG_CONFIG_HOME" not in os.environ: + base = portable_home / ".config" / "retroarch" + else: + continue + tilde = portable_home if _is_dir(portable_home) else home + system = _parse_retroarch_system_dir(base / "retroarch.cfg", base, tilde) + if system is None and _is_dir(base): + system = base / "system" + if system is not None and system not in dirs: + dirs.append(system) + return dirs + + def _unix_retroarch_base() -> Path: """RetroArch's base folder on Linux: $XDG_CONFIG_HOME/retroarch when the variable is set, else ~/.config/retroarch (fill_pathname_application_data @@ -2839,6 +2882,8 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]: found.append(("retroarch", ra_dir)) break + found.extend(("retroarch", ra_dir) for ra_dir in _retroarch_appimage_dirs(home)) + if os_type == "darwin": # open_default_config_file reads /RetroArch/config # (configuration.c). A value left at default is the documents folder, diff --git a/install.sh b/install.sh index d6e4b78ba..979021d69 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="450cee7625f098d5796452b02ab9c8bffdeaaef89180d2254bbe07ce354bc359" +DEFAULT_INSTALL_SHA256="5bce9f0f81e4f1c7302ca78fa5242f26acc06523f8718378ee97042f2421ff78" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/install/launchers/index.json b/install/launchers/index.json index 9fd32e302..4cbfbf06e 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": 1115328, - "sha256": "450cee7625f098d5796452b02ab9c8bffdeaaef89180d2254bbe07ce354bc359", + "size": 1117350, + "sha256": "5bce9f0f81e4f1c7302ca78fa5242f26acc06523f8718378ee97042f2421ff78", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 1168955, - "sha256": "1f52731fda2beb3ff9e7aaf7f3d9333853e66b99240b24e4f6b0026368755cb4" + "size": 1170977, + "sha256": "cfb61e90dafef4fceadd8750412fe91a98b5d8cbf9574c819c09bddb76486e78" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 1115328, - "sha256": "450cee7625f098d5796452b02ab9c8bffdeaaef89180d2254bbe07ce354bc359", + "size": 1117350, + "sha256": "5bce9f0f81e4f1c7302ca78fa5242f26acc06523f8718378ee97042f2421ff78", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 1115328, - "sha256": "450cee7625f098d5796452b02ab9c8bffdeaaef89180d2254bbe07ce354bc359", + "size": 1117350, + "sha256": "5bce9f0f81e4f1c7302ca78fa5242f26acc06523f8718378ee97042f2421ff78", "executable": false }, { diff --git a/tests/test_install.py b/tests/test_install.py index 4cec55ae6..e1aa93888 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -4542,6 +4542,53 @@ class WindowsSeenFromWsl(unittest.TestCase): str(self.root)) +class RetroArchAppImageIsPortable(unittest.TestCase): + """The buildbot's Linux RetroArch is an AppImage beside a .home folder + the AppImage runtime makes its HOME: the run found nothing and offered + ~/.config/retroarch/system, a folder this RetroArch never reads.""" + + def _detect(self, home: Path, env: dict | None = None) -> list: + cleared = {"HOME": str(home), "XDG_CONFIG_HOME": ""} + with unittest.mock.patch.dict(os.environ, {**cleared, **(env or {})}), \ + unittest.mock.patch.object(Path, "home", return_value=home): + os.environ.pop("XDG_CONFIG_HOME") + return install.detect_platforms("linux") + + def _unpack(self, home: Path) -> Path: + folder = home / "Downloads" / "RetroArch-Linux-x86_64" + base = folder / "RetroArch-Linux-x86_64.AppImage.home" / ".config" / "retroarch" + (base / "system").mkdir(parents=True) + (folder / "RetroArch-Linux-x86_64.AppImage").write_bytes(b"\x7fELF") + return base + + def test_the_unpacked_archive_is_found(self): + home = Path(_tempdir(self)) + base = self._unpack(home) + self.assertIn(("retroarch", base / "system"), self._detect(home)) + + def test_its_config_is_read_with_its_own_home(self): + home = Path(_tempdir(self)) + base = self._unpack(home) + (base / "retroarch.cfg").write_text('system_directory = "~/bios"\n') + portable = base.parent.parent + self.assertIn(("retroarch", portable / "bios"), self._detect(home)) + + def test_a_config_folder_beside_it_wins(self): + home = Path(_tempdir(self)) + base = self._unpack(home) + config = base.parent.parent.with_name("RetroArch-Linux-x86_64.AppImage.config") + (config / "retroarch").mkdir(parents=True) + found = self._detect(home) + self.assertIn(("retroarch", config / "retroarch" / "system"), found) + self.assertNotIn(("retroarch", base / "system"), found) + + def test_a_folder_without_its_appimage_is_left(self): + home = Path(_tempdir(self)) + base = self._unpack(home) + (base.parent.parent.with_name("RetroArch-Linux-x86_64.AppImage")).unlink() + self.assertNotIn(("retroarch", base / "system"), self._detect(home)) + + class SteamRetroArchOnMacIsPortable(unittest.TestCase): """platform_darwin.m makes the Steam build portable (HAVE_STEAM) with no portable.txt: a default system_directory is Application Support's, and diff --git a/wiki/installer.md b/wiki/installer.md index b89874475..7ed1725ed 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -1177,6 +1177,7 @@ usual folders of a setup that has one on each side: RetroArch's in this Linux | `~/.var/app/org.libretro.RetroArch/config/retroarch/retroarch.cfg` | RetroArch (Flatpak) | `system_directory` | | `~/snap/retroarch/current/.config/retroarch/retroarch.cfg` | RetroArch (Snap) | `system_directory` | | `~/.config/retroarch/retroarch.cfg` | RetroArch (native) | `system_directory` | +| `.AppImage.home/.config/retroarch/retroarch.cfg` (or `.AppImage.config/retroarch/`) beside an AppImage in `~`, or one or two folders below it | RetroArch (AppImage from the buildbot, whose portable folder becomes its home) | `system_directory`. Without a config, `system` in that `retroarch` folder | | `~/Library/Application Support/RetroArch/config/retroarch.cfg` | RetroArch (macOS) | `system_directory`. At default, `~/Documents/RetroArch/system` (portable, Steam build included: `Application Support/RetroArch/system`) | | `retroarch.cfg` beside `retroarch.exe` (`C:\RetroArch-Win64`, `C:\RetroArch`, `Program Files\RetroArch`) | RetroArch (Windows) | `system_directory`. At default or without a config, `system` beside the exe | | `%APPDATA%\RetroArch\retroarch.cfg` | RetroArch (Windows) | `system_directory`, read when the exe folder has no config |