diff --git a/install.ps1 b/install.ps1 index 656ddcb3..a2992485 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 = "456d3e573f55c206ce3719160afee9b1d02fcfb7b6b53dceb33ebfd5c712a7fa" +$defaultInstallSha256 = "6622a88280f46e21e7fac0adce72973211461cb6dc4af9cd3c5c1be8dd3aa842" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 685017ae..0ec3cfe1 100644 --- a/install.py +++ b/install.py @@ -1062,6 +1062,12 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]: found.append(("retroarch", ra_dir)) break + # The UWP/MSIX package (sideloaded, or Xbox in dev mode) + if not any(name == "retroarch" for name, _ in found): + uwp = _retroarch_uwp_system_dir() + if uwp is not None: + found.append(("retroarch", uwp)) + # Portable RetroArch referenced by LaunchBox lb_root = launchbox_root(os_type) if lb_root and not any(name == "retroarch" for name, _ in found): @@ -1086,6 +1092,39 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]: return found +# Identity Name in pkg/msvc-uwp/RetroArch-msvcUWP/Package.appxmanifest. +RETROARCH_UWP_ID = "1e4cf179-f3c2-404f-b9f3-cb2070a5aad8" + + +def _retroarch_uwp_system_dir() -> Path | None: + """System directory of the RetroArch UWP package, if one is installed. + + The package writes into its LocalState folder: uwp_main.cpp makes it the + home directory, configuration.c reads retroarch.cfg there, and + platform_uwp.c defaults the system directory to ~\\system. A `~` in the + config is therefore LocalState, not the user's profile. + """ + local = os.environ.get("LOCALAPPDATA", "").strip() + if not local: + return None + states = sorted(Path(local, "Packages").glob(f"{RETROARCH_UWP_ID}_*/LocalState")) + if not states: + return None + state = states[0] + cfg = state / "retroarch.cfg" + try: + for line in cfg.read_text(encoding="utf-8", errors="replace").splitlines(): + name, sep, value = line.strip().partition("=") + if sep and name.strip() == "system_directory": + value = value.strip().strip('"').strip("'") + if value.startswith("~"): + return state / value[1:].lstrip("\\/") + break + except OSError: + return state / "system" + return _retroarch_cfg_dir(cfg, "system_directory", state, "system") or state / "system" + + def _bizhawk_unix_firmware() -> Path | None: """BizHawk's Firmware folder on Unix, where its data home says. diff --git a/install.sh b/install.sh index fc4de55b..c0488db0 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="456d3e573f55c206ce3719160afee9b1d02fcfb7b6b53dceb33ebfd5c712a7fa" +DEFAULT_INSTALL_SHA256="6622a88280f46e21e7fac0adce72973211461cb6dc4af9cd3c5c1be8dd3aa842" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/tests/test_install.py b/tests/test_install.py index 832222a9..88bef060 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -2405,5 +2405,33 @@ class BizHawkIsFoundWhereItReads(unittest.TestCase): self.assertIn('.get("emuhawk.exe")', source) + +class RetroArchUwpIsFound(unittest.TestCase): + """The UWP/MSIX RetroArch keeps retroarch.cfg and system/ in its + package's LocalState; no rule looked under %LOCALAPPDATA%\\Packages.""" + + def _state(self) -> Path: + local = Path(_tempdir(self)) + state = local / "Packages" / f"{install.RETROARCH_UWP_ID}_abc123" / "LocalState" + state.mkdir(parents=True) + patcher = unittest.mock.patch.dict(os.environ, {"LOCALAPPDATA": str(local)}) + patcher.start() + self.addCleanup(patcher.stop) + return state + + def test_the_default_is_local_state_system(self): + state = self._state() + self.assertEqual(install._retroarch_uwp_system_dir(), state / "system") + + def test_a_tilde_is_local_state(self): + state = self._state() + (state / "retroarch.cfg").write_text('system_directory = "~\\bios"\n') + self.assertEqual(install._retroarch_uwp_system_dir(), state / "bios") + + def test_nothing_installed(self): + with unittest.mock.patch.dict(os.environ, {"LOCALAPPDATA": _tempdir(self)}): + self.assertIsNone(install._retroarch_uwp_system_dir()) + + if __name__ == "__main__": unittest.main() diff --git a/wiki/installer.md b/wiki/installer.md index 71aa1245..b409de27 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -299,6 +299,7 @@ found on one machine. | `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 | | `%ProgramFiles(x86)%\Steam\steamapps\common\RetroArch\retroarch.cfg` | RetroArch (Steam) | `system_directory` | +| `%LOCALAPPDATA%\Packages\1e4cf179-f3c2-404f-b9f3-cb2070a5aad8_*\LocalState` | RetroArch (UWP/MSIX package) | `system_directory` of the `retroarch.cfg` there, `~` meaning that folder; by default `LocalState\system` | | LaunchBox `Data\Emulators.xml` | RetroArch (portable) | the system directory that entry points at | | LaunchBox `Data\Emulators.xml` naming `EmuHawk.exe` | BizHawk (portable) | `Firmware` beside `EmuHawk.exe` | | `$BIZHAWK_DATA_HOME`, or `$XDG_DATA_HOME/emuhawk-monort-` (Nix package) | BizHawk (Unix) | `Firmware` under it |