feat: name Android BIOS folders out of reach

DraStic keeps its BIOS in its private data and Flycast in Android/data,
which Termux cannot write on Android 11 and later. An installed one is now
named with the files not copied, and a writable Flycast folder is a
standalone copy target.
This commit is contained in:
Abdessamad Derraz committed 2026-10-22 03:38:04 +02:00
1 parent 05d8f944a9
commit 43bb0c9b29
6 files changed
+113 -3

No files matched your search

+1 -1
View File
@@ -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 }
+42
View File
@@ -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 <home>/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())
+1 -1
View File
@@ -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() {
+8
View File
@@ -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
# <home>/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
# <home>/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:
+51
View File
@@ -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
+10 -1
View File
@@ -504,7 +504,16 @@ left at `default` resolves against `<shared storage>/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: