From 20829d09ced2699970d7721895f135c3efd08156 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:44:33 +0200 Subject: [PATCH] fix: place launcher links and platform BIOS paths A links record naming a file is where the emulator reads it: ROCKNIX links /storage/roms/bios/nds_bios_arm9.bin into DraStic's system folder (start_drastic.sh:37-41). $biosdir, ${biosPath} and $bios_path are the BIOS directory as RetroPie, EmuDeck and RetroDECK write it, and every place of those platforms was dropped as an unknown folder. A core name the launcher replaces is no longer packed when held: EmuDeck passes Complex_4627v1.03.bin to xemu (emuDeckXemu.sh:60-61), which never opens Complex_4627.bin. --- scripts/launchers.py | 37 +++++++++++++++++++++++++----- tests/test_launcher_layer.py | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/scripts/launchers.py b/scripts/launchers.py index e12b21f5..ae81146a 100644 --- a/scripts/launchers.py +++ b/scripts/launchers.py @@ -22,10 +22,23 @@ from dataclasses import dataclass from common import declared_root, name_match_size_ok, sanitize_pack_path -# The kinds whose names say which file the launcher opens. The others hand -# over a directory, copy a file the platform already names, or let the +# The kinds whose names say which file the launcher opens. A link of a named +# file is one too: ROCKNIX links /storage/roms/bios/nds_bios_arm9.bin into +# DraStic's system folder, so that is the path the emulator reads. The others +# hand over a directory, copy a file the platform already names, or let the # emulator choose. -PLACE_KINDS = ("names_searched", "fixed_name", "core_option") +PLACE_KINDS = ("names_searched", "fixed_name", "core_option", "links") + +# How each launcher extractor spells the BIOS directory in search_dir, as the +# platform's own scripts name it: RetroPie's scriptmodules ($biosdir, +# retropie_launchers.BIOSDIR), EmuDeck's EmuScripts (${biosPath}, +# emudeck_launchers.BIOS) and RetroDECK's framework ($bios_path, +# retrodeck_launchers.BIOS). +BIOS_ROOT_TOKENS = ("$biosdir", "${biosdir}", "${biosPath}", "$bios_path", "${bios_path}") + +# A path spelled from a home or filesystem root names the BIOS directory by +# its base name: /userdata/bios, ~/roms/bios, $HOME/RetroPie/BIOS. +_ROOTED = ("/", "~/", "$HOME/", "$home/", "${HOME}/") _LABELLED = re.compile(r"^([^\s()]+\.[A-Za-z0-9]+) \([^()]*\)$") _PLACEHOLDER = re.compile(r"\{(\w+)\}") @@ -68,19 +81,22 @@ class LauncherPlace: option or a system leaves that default in use elsewhere, and a core option offers a file the user may choose without ever choosing it. """ - return self.kind in ("names_searched", "fixed_name") and not self.conditional + return ( + self.kind in ("names_searched", "fixed_name", "links") + and not self.conditional + ) def _bios_relative(directory: str, base: str) -> str | None: """A launcher directory relative to the BIOS root, None when it is not one.""" directory = directory.strip().rstrip("/") - roots = {base, ""} + roots = {base, "", *BIOS_ROOT_TOKENS} head, _, rest = directory.partition("/") if directory in roots: return "" if head in roots: relative = rest - elif directory.startswith("/") and base and f"/{base}/" in f"{directory}/": + elif directory.startswith(_ROOTED) and base and f"/{base}/" in f"{directory}/": relative = f"{directory}/".split(f"/{base}/", 1)[1].rstrip("/") else: return None @@ -390,9 +406,18 @@ class LauncherLayer: if isinstance(filled, dict): added.append(filled) if place.stands_for_default: + # The emulator opens the launcher's file instead of its + # own name, so a copy under that name is read by nobody: + # EmuDeck passes Complex_4627v1.03.bin to xemu, which then + # never opens Complex_4627.bin. A held file sitting on one + # of the place's own paths is what fills it. + own = {place.fold(p) for p in place.paths} settled.update( id(report) for _candidate, report in matched if not report.get("in_repo") + or place.fold(sanitize_pack_path( + report.get("path") or report.get("name", "") + )) not in own ) return added, settled diff --git a/tests/test_launcher_layer.py b/tests/test_launcher_layer.py index 4231a0c8..bb3069a8 100644 --- a/tests/test_launcher_layer.py +++ b/tests/test_launcher_layer.py @@ -250,6 +250,50 @@ class LauncherLayerTest(unittest.TestCase): self.assertNotIn("core.bin", {u["name"] for u in self._report()}) self.assertNotIn("core.bin", self._pack_names()) + def test_the_default_name_a_launcher_replaces_is_not_packed(self) -> None: + """EmuDeck passes Complex_4627v1.03.bin to xemu, which then never + opens Complex_4627.bin: the pack carried both, one read by nobody.""" + report = {u["path"]: u for u in self._report()} + self.assertIn("sub/fix.bin", report) + self.assertNotIn("core.bin", report) + self.assertNotIn("core.bin", self._pack_names()) + + def test_a_link_of_a_named_file_is_a_place(self) -> None: + """ROCKNIX links /storage/roms/bios/nds_bios_arm9.bin into DraStic's + system folder (start_drastic.sh:37-41): the file goes where the + link reads it, not under the emulator's own folder.""" + self.config["launchers"][2].update( + kind="links", names=["fix.bin"], search_dir="/storage/roms/bios", + ) + places = [p for p in launcher_places(self.config) if p.kind == "links"] + self.assertEqual([p.paths for p in places], [("fix.bin",)]) + self.assertTrue(places[0].stands_for_default) + report = {u["path"]: u for u in self._report()} + self.assertIn("fix.bin", report) + self.assertNotIn("core.bin", report) + + def test_a_link_of_a_directory_is_no_place(self) -> None: + self.config["launchers"][2].update( + kind="links", names=[], search_dir="/userdata/bios/sub", + ) + self.assertFalse([p for p in launcher_places(self.config) if p.kind == "links"]) + + def test_the_platform_spelling_of_the_bios_folder_is_a_root(self) -> None: + """RetroPie names $biosdir, EmuDeck ${biosPath}, RetroDECK $bios_path: + read as no folder at all, every place of those platforms was dropped.""" + for spelling in ("$biosdir", "${biosPath}", "$bios_path", "$HOME/RetroPie/bios"): + self.config["launchers"][2]["search_dir"] = f"{spelling}/sub" + with self.subTest(spelling=spelling): + self.assertEqual(launcher_places(self.config)[2].paths, ("sub/sub/fix.bin",)) + + def test_every_extractor_spelling_of_the_bios_folder_is_known(self) -> None: + import launchers + from scraper import emudeck_launchers, retrodeck_launchers, retropie_launchers + + for token in (emudeck_launchers.BIOS, retrodeck_launchers.BIOS, + retropie_launchers.BIOSDIR): + self.assertIn(token, launchers.BIOS_ROOT_TOKENS) + def test_no_place_is_filled_twice(self) -> None: added = [u["path"] for u in self._report() if u.get("launcher")] self.assertEqual(sorted(added), ["e.bin", "sub/fix.bin"])