feat: detect android in the installer

This commit is contained in:
Abdessamad Derraz committed 2026-09-05 07:00:12 +02:00
1 parent 96920785ad
commit f864613f11
5 files changed
+207 -9

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@ param(
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "7dbb5c48a834d31a470b1952a9b8659b4c1f49f8d37c709a4032a56abe495f27"
$defaultInstallSha256 = "b5749a3aea8679657a6d48f04ef18879f2fe04cc55d1c1a6510fd6b5d74068e1"
$maximumInstallerBytes = 2MB
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
$temporary = $null
+79 -5
View File
@@ -104,19 +104,39 @@ DEFAULT_DESTS = {
"retropie": Path.home() / "RetroPie" / "BIOS",
}
# The three RetroArch packages published for Android, newest ABI first.
ANDROID_RETROARCH_PACKAGES = (
"com.retroarch.aarch64",
"com.retroarch",
"com.retroarch.ra32",
)
def _is_android() -> bool:
"""Android reports itself as Linux, so the runtime is named instead.
Termux inherits the two variables the Android runtime exports, and
/system/build.prop covers a shell that was started without them.
"""
if os.environ.get("ANDROID_ROOT") and os.environ.get("ANDROID_DATA"):
return True
return Path("/system/build.prop").exists()
def detect_os() -> str:
"""Return normalized OS identifier.
RETROBIOS_OS names the platform outright (linux, wsl, windows, darwin),
for a run that has to behave like another host: the PowerShell wrapper
tests drive a Windows layout on a Linux runner.
RETROBIOS_OS names the platform outright (linux, wsl, windows, darwin,
android), for a run that has to behave like another host: the PowerShell
wrapper tests drive a Windows layout on a Linux runner.
"""
forced = os.environ.get("RETROBIOS_OS", "").strip().lower()
if forced in ("linux", "wsl", "windows", "darwin"):
if forced in ("linux", "wsl", "windows", "darwin", "android"):
return forced
system = platform.system().lower()
if system == "linux":
if _is_android():
return "android"
proc_version = Path("/proc/version")
if proc_version.exists():
try:
@@ -326,6 +346,40 @@ def _detect_embedded() -> list[tuple[str, Path]]:
return found
def _android_storage_root() -> Path:
"""Shared storage root, the parent RetroArch builds its defaults from."""
value = os.environ.get("EXTERNAL_STORAGE", "").strip()
return Path(value) if value else Path("/storage/emulated/0")
def _detect_android() -> list[tuple[str, Path]]:
"""Find the RetroArch system directory on Android.
The config is read from the app external files directory first, then from
the internal one (platform_unix.c:1327-1395). A system_directory left at
default resolves against <shared storage>/RetroArch, which is also where
the directory sits when no config has been written yet
(platform_unix.c:2836-2851).
"""
found: list[tuple[str, Path]] = []
storage = _android_storage_root()
default_root = storage / "RetroArch"
for package in ANDROID_RETROARCH_PACKAGES:
for cfg in (
storage / "Android" / "data" / package / "files" / "retroarch.cfg",
Path("/data/data") / package / "files" / "retroarch.cfg",
):
system_dir = _parse_retroarch_system_dir(cfg, default_root)
if system_dir:
found.append(("retroarch", system_dir))
return found
if default_root.exists():
found.append(("retroarch", default_root / "system"))
return found
def _lnk_target(lnk_path: Path, exe_name: str) -> Path | None:
"""Read the target path of a Windows shortcut.
@@ -530,6 +584,9 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
"""Detect installed emulator platforms and their BIOS directories."""
found: list[tuple[str, Path]] = []
if os_type == "android":
found.extend(_detect_android())
if os_type in ("linux", "wsl"):
found.extend(_detect_embedded())
@@ -1227,6 +1284,17 @@ def _prompt_manual_platform(os_type: str) -> list[tuple[str, Path]]:
return [(plat, dest)]
def _default_dest(os_type: str, platform_name: str) -> Path:
"""Fall back path for a platform named on the command line but not found.
A home directory is meaningless for RetroArch on Android, where Termux
owns the home and the frontend reads from shared storage.
"""
if os_type == "android" and platform_name == "retroarch":
return _android_storage_root() / "RetroArch" / "system"
return DEFAULT_DESTS.get(platform_name, Path.home() / "bios")
def _manual_usage_hint(os_type: str) -> list[str]:
"""How to name the platform and directory when nothing is detected."""
if os_type == "windows":
@@ -1236,6 +1304,12 @@ def _manual_usage_hint(os_type: str) -> list[str]:
"https://raw.githubusercontent.com/Abdess/retrobios/main/install.ps1))) "
"--platform retroarch --dest 'C:\\RetroArch\\system'",
]
if os_type == "android":
return [
" Use --platform and --dest to specify manually, for example:",
" python3 install.py --platform retroarch "
"--dest /storage/emulated/0/RetroArch/system",
]
return [
" Use --platform and --dest to specify manually, for example:",
" python3 install.py --platform retroarch --dest ~/.config/retroarch/system",
@@ -1352,7 +1426,7 @@ def main() -> None:
if matched:
platforms = matched
else:
default_dest = DEFAULT_DESTS.get(args.platform, Path.home() / "bios")
default_dest = _default_dest(os_type, args.platform)
print(
f" Platform '{args.platform}' not detected, "
f"using default path: {default_dest}"
+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="7dbb5c48a834d31a470b1952a9b8659b4c1f49f8d37c709a4032a56abe495f27"
DEFAULT_INSTALL_SHA256="b5749a3aea8679657a6d48f04ef18879f2fe04cc55d1c1a6510fd6b5d74068e1"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+103
View File
@@ -214,6 +214,109 @@ class TestEmbeddedDetection(unittest.TestCase):
self.assertEqual(self._detect_with(set()), [])
class TestAndroidDetection(unittest.TestCase):
"""Android reports itself as Linux, and RetroArch is the frontend there."""
def setUp(self):
self.saved = {
key: os.environ.get(key)
for key in ("ANDROID_ROOT", "ANDROID_DATA", "EXTERNAL_STORAGE", "RETROBIOS_OS")
}
for key in self.saved:
os.environ.pop(key, None)
def tearDown(self):
for key, value in self.saved.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
def test_runtime_variables_name_android(self):
os.environ["ANDROID_ROOT"] = "/system"
os.environ["ANDROID_DATA"] = "/data"
self.assertTrue(install._is_android())
def test_plain_linux_is_not_android(self):
self.assertFalse(
install._is_android() and not Path("/system/build.prop").exists()
)
def test_forced_os_accepts_android(self):
os.environ["RETROBIOS_OS"] = "android"
self.assertEqual(install.detect_os(), "android")
def test_default_directory_found_without_a_config(self):
storage = Path(tempfile.mkdtemp())
(storage / "RetroArch" / "system").mkdir(parents=True)
os.environ["EXTERNAL_STORAGE"] = str(storage)
self.assertEqual(
install._detect_android(),
[("retroarch", storage / "RetroArch" / "system")],
)
shutil.rmtree(storage)
def test_config_default_resolves_against_the_retroarch_root(self):
storage = Path(tempfile.mkdtemp())
files = storage / "Android" / "data" / "com.retroarch.aarch64" / "files"
files.mkdir(parents=True)
(files / "retroarch.cfg").write_text('system_directory = "default"\n')
os.environ["EXTERNAL_STORAGE"] = str(storage)
self.assertEqual(
install._detect_android(),
[("retroarch", storage / "RetroArch" / "system")],
)
shutil.rmtree(storage)
def test_custom_system_directory_wins(self):
storage = Path(tempfile.mkdtemp())
files = storage / "Android" / "data" / "com.retroarch" / "files"
files.mkdir(parents=True)
custom = storage / "Emulation" / "bios"
(files / "retroarch.cfg").write_text(f'system_directory = "{custom}"\n')
os.environ["EXTERNAL_STORAGE"] = str(storage)
self.assertEqual(install._detect_android(), [("retroarch", custom)])
shutil.rmtree(storage)
def test_nothing_found_on_a_bare_device(self):
storage = Path(tempfile.mkdtemp())
os.environ["EXTERNAL_STORAGE"] = str(storage)
self.assertEqual(install._detect_android(), [])
shutil.rmtree(storage)
def test_detect_platforms_routes_android(self):
storage = Path(tempfile.mkdtemp())
(storage / "RetroArch" / "system").mkdir(parents=True)
os.environ["EXTERNAL_STORAGE"] = str(storage)
self.assertEqual(
install.detect_platforms("android"),
[("retroarch", storage / "RetroArch" / "system")],
)
shutil.rmtree(storage)
def test_manual_hint_names_the_android_path(self):
hint = "\n".join(install._manual_usage_hint("android"))
self.assertIn("/storage/emulated/0/RetroArch/system", hint)
def test_named_platform_falls_back_to_shared_storage(self):
storage = Path(tempfile.mkdtemp())
os.environ["EXTERNAL_STORAGE"] = str(storage)
self.assertEqual(
install._default_dest("android", "retroarch"),
storage / "RetroArch" / "system",
)
self.assertEqual(
install._default_dest("linux", "batocera"), Path("/userdata/bios")
)
shutil.rmtree(storage)
def test_packages_match_the_published_flavors(self):
self.assertEqual(
install.ANDROID_RETROARCH_PACKAGES,
("com.retroarch.aarch64", "com.retroarch", "com.retroarch.ra32"),
)
class TestLaunchboxDetection(unittest.TestCase):
"""LaunchBox references its emulators in Data/Emulators.xml."""
+23 -2
View File
@@ -217,8 +217,29 @@ redirect the write somewhere else again.
## Android
There is no Android detection. Termux reports itself as Linux, so nothing is
found and the platform has to be named along with where it writes:
Android reports itself as Linux, so the runtime is named from its own
variables instead: `ANDROID_ROOT` and `ANDROID_DATA`, which Termux inherits,
with `/system/build.prop` covering a shell started without them.
Detection then looks for RetroArch, the only Android frontend keeping a shared
BIOS directory. `retroarch.cfg` is read from the app external files directory
and then from the internal one, the order the frontend itself follows
(`platform_unix.c:1327-1395`), and a `system_directory` left at `default`
resolves against `<shared storage>/RetroArch`, where the directory also sits
when no config has been written yet (`platform_unix.c:2836-2851`).
Under scoped storage those two 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:
```bash
pkg install python
termux-setup-storage
curl -fsSL https://raw.githubusercontent.com/Abdess/retrobios/main/install.sh | sh
```
`EXTERNAL_STORAGE` overrides the storage root for a device that mounts it
elsewhere. A custom BIOS directory is still named directly:
```bash
python install.py --platform retroarch --dest /storage/emulated/0/RetroArch/system