diff --git a/install.ps1 b/install.ps1 index c4407102..84fc014f 100644 --- a/install.ps1 +++ b/install.ps1 @@ -11,7 +11,7 @@ $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" - $defaultInstallSha256 = "956eba5f9d01138abda5550973810ccdc1138daa54c9065bf3f04cc0ba051468" + $defaultInstallSha256 = "6ae8bdd7bbe9a0c3e6cb2d92947bb4e135e38d6446d206ae30b988a045eaa536" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index abf3af2e..2494f206 100644 --- a/install.py +++ b/install.py @@ -1199,6 +1199,46 @@ def _detect_android() -> list[tuple[str, Path]]: return found +# Android emulators that keep their BIOS outside RetroArch's folder: +# (name, package, folder, files read there). DraStic keeps it in its private +# data (GammaOS Next 14 bb28e71d, gammaos/setup.sh:345-363 extracts it to +# /data/data/com.dsemu.drastic/files/DraStic/system), which no other app +# writes without root. Flycast reads /data/ (flycast a9bd1c74, +# shell/android-studio/flycast/src/main/jni/src/Android.cpp:143-159), its +# home being Android/data/com.flycast.emulator/files, which Android 11 and +# later keep from other apps. +ANDROID_APP_DATA = Path("/data/data") +_ANDROID_APP_FOLDERS = ( + ("DraStic", "com.dsemu.drastic", "/data/data/com.dsemu.drastic/files/DraStic/system", + ("nds_bios_arm7.bin", "nds_bios_arm9.bin", "nds_firmware.bin")), + ("Flycast", "com.flycast.emulator", + "$STORAGE/Android/data/com.flycast.emulator/files/data", + ("dc_boot.bin", "dc_nvmem.bin")), +) + + +def android_unreachable_folders() -> list[str]: + """A line for each Android emulator installed here whose BIOS folder + this shell cannot write, said instead of skipping it in silence. + + An app's private folder can be tested for existence by any app, so the + emulator is known to be installed even where its folder is out of reach. + """ + lines = [] + storage = str(_android_storage_root()) + for name, package, folder, files in _ANDROID_APP_FOLDERS: + target = Path(folder.replace("$STORAGE", storage, 1)) + if folder.startswith("/data/data/"): + target = ANDROID_APP_DATA / Path(folder).relative_to("/data/data") + if not (ANDROID_APP_DATA / package).exists() and not target.exists(): + continue + if target.is_dir() and os.access(target, os.W_OK): + continue + lines.append(f"{name} is installed, but this shell cannot write its BIOS folder " + f"{target} without root: {', '.join(files)} are not copied there.") + return lines + + def _lnk_target(lnk_path: Path, exe_name: str) -> Path | None: """Read the target path of a Windows shortcut. @@ -1539,6 +1579,8 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]: if os_type == "android": found.extend(_detect_android()) + for line in android_unreachable_folders(): + print(line, file=sys.stderr) if os_type in ("linux", "wsl"): found.extend(_detect_embedded()) diff --git a/install.sh b/install.sh index c67e9b2e..bfea4cda 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="956eba5f9d01138abda5550973810ccdc1138daa54c9065bf3f04cc0ba051468" +DEFAULT_INSTALL_SHA256="6ae8bdd7bbe9a0c3e6cb2d92947bb4e135e38d6446d206ae30b988a045eaa536" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/platforms/_registry.yml b/platforms/_registry.yml index 84d0d7ce..a9c3ba60 100644 --- a/platforms/_registry.yml +++ b/platforms/_registry.yml @@ -158,6 +158,10 @@ _standalone_copies: &standalone_copies # shell/apple/emulator-osx/emulator-osx/osx-main.mm:83-101 (flycast 6bb0fc3e). darwin: - $HOME/Library/Application Support/Flycast/data + # /data/ (shell/android-studio/flycast/src/main/jni/src/Android.cpp: + # 143-159, flycast a9bd1c74), writable from Termux up to Android 10. + android: + - $EXTERNAL_STORAGE/Android/data/com.flycast.emulator/files/data - file: dc/dc_nvmem.bin targets: linux: @@ -168,6 +172,10 @@ _standalone_copies: &standalone_copies # shell/apple/emulator-osx/emulator-osx/osx-main.mm:83-101 (flycast 6bb0fc3e). darwin: - $HOME/Library/Application Support/Flycast/data + # /data/ (shell/android-studio/flycast/src/main/jni/src/Android.cpp: + # 143-159, flycast a9bd1c74), writable from Termux up to Android 10. + android: + - $EXTERNAL_STORAGE/Android/data/com.flycast.emulator/files/data platforms: retroarch: diff --git a/tests/test_install.py b/tests/test_install.py index c7d4e708..301358a4 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -2724,6 +2724,57 @@ class AndroidCitationsNameTheRelease(unittest.TestCase): self.assertNotIn("2836-2851", text) +class AndroidEmulatorsOutOfReachAreSaid(unittest.TestCase): + """DraStic keeps its BIOS in its private data and Flycast in + Android/data, which Termux cannot write on Android 11 and later: both + were skipped without a word.""" + + def _lines(self, make) -> list[str]: + root = Path(_tempdir(self)) + app_data, storage = root / "data", root / "storage" + app_data.mkdir() + make(app_data, storage) + with unittest.mock.patch.object(install, "ANDROID_APP_DATA", app_data), \ + unittest.mock.patch.dict(os.environ, {"EXTERNAL_STORAGE": str(storage)}): + return install.android_unreachable_folders() + + def test_nothing_installed_says_nothing(self): + self.assertEqual(self._lines(lambda data, storage: None), []) + + def test_an_installed_drastic_is_named(self): + lines = self._lines(lambda data, storage: (data / "com.dsemu.drastic").mkdir()) + self.assertEqual(len(lines), 1) + self.assertIn("DraStic is installed", lines[0]) + self.assertIn("nds_bios_arm7.bin", lines[0]) + + def test_flycast_out_of_reach_is_named(self): + lines = self._lines(lambda data, storage: (data / "com.flycast.emulator").mkdir()) + self.assertEqual(len(lines), 1) + self.assertIn("Flycast is installed", lines[0]) + self.assertIn("dc_boot.bin", lines[0]) + + def test_a_writable_flycast_folder_is_a_copy_target(self): + def make(data, storage): + (data / "com.flycast.emulator").mkdir() + (storage / "Android/data/com.flycast.emulator/files/data").mkdir(parents=True) + self.assertEqual(self._lines(make), []) + import yaml # noqa: PLC0415 + + registry = yaml.safe_load((REPO_ROOT / "platforms" / "_registry.yml").read_text()) + copies = {entry.get("file"): entry for entry in registry["_standalone_copies"]} + self.assertEqual(copies["dc/dc_boot.bin"]["targets"]["android"], + ["$EXTERNAL_STORAGE/Android/data/com.flycast.emulator/files/data"]) + + def test_detection_prints_them(self): + err = io.StringIO() + with unittest.mock.patch.object(install, "_detect_android", return_value=[]), \ + unittest.mock.patch.object(install, "android_unreachable_folders", + return_value=["DraStic is installed, but"]), \ + contextlib.redirect_stderr(err): + install.detect_platforms("android") + self.assertIn("DraStic is installed", err.getvalue()) + + class TheBoardTheSystemNamesIsTheTarget(unittest.TestCase): """Batocera, Recalbox and ROCKNIX each write the board they were built for, but the installer read none of them, so a Batocera RPi4 installed diff --git a/wiki/installer.md b/wiki/installer.md index cd7161e4..6dd0037e 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -504,7 +504,16 @@ left at `default` resolves against `/RetroArch`, where the directory also sits when no config has been written yet (`platform_unix.c:1670-1697`). -Under scoped storage those two config paths are unreadable from Termux on +Two Android emulators keep their BIOS elsewhere. DraStic reads +`/data/data/com.dsemu.drastic/files/DraStic/system`, private to the app, and +Flycast reads `Android/data/com.flycast.emulator/files/data` in shared +storage, which Android 11 and later keep from other apps. No app can write +the first without root, and Termux cannot write the second on Android 11 and +later. When either emulator is installed and its folder is out of reach, the +run names it and the files that are not copied. Where the Flycast folder is +writable (Android 10 and earlier), it is a standalone copy target. + +Under scoped storage the two RetroArch config paths are unreadable from Termux on Android 11 and later, so on current devices the shared directory is what answers. Running `termux-setup-storage` once is what makes it reachable: