chore: merge integ configs root and switch folders

The configs root place joins roms and emulator folders in the split
handed directories, and RetroBat's portable folder table keeps both
sides. Batocera, KNULLI and RetroBat records regenerated.
This commit is contained in:
Abdessamad Derraz committed 2026-11-09 20:16:41 +01:00
commit e93ff86bf1
27 files changed
+893 -206

No files matched your search

+32 -5
View File
@@ -1150,7 +1150,7 @@ class OuterRootsAreUntrusted(unittest.TestCase):
def test_unsafe_from_bios_is_refused(self):
for value in (*self.UNSAFE, "../../../x", "../x/..", "x", "./x", "/x", "..x", None):
with self.subTest(from_bios=value), self.assertRaises(ValueError):
install._check_roms_directory({"roms_directory": {**self.ROMS, "from_bios": value}})
install._check_relative_directories({"roms_directory": {**self.ROMS, "from_bios": value}})
def test_the_relations_the_rom_folders_files_use_are_accepted(self):
bios = Path("/card/system/bios")
@@ -1161,13 +1161,13 @@ class OuterRootsAreUntrusted(unittest.TestCase):
}
for value, root in expected.items():
with self.subTest(from_bios=value):
install._check_roms_directory({"roms_directory": {**self.ROMS, "from_bios": value}})
self.assertEqual(install._roms_root(bios, value), root)
install._check_relative_directories({"roms_directory": {**self.ROMS, "from_bios": value}})
self.assertEqual(install._relative_root(bios, value), root)
self.assertEqual(install.MAX_ROMS_UPS, 2)
def test_a_list_without_a_roms_root_leaves_the_files_out(self):
files = [{"dest": "pico-8/pico8_64", "root": "roms", "cores": ["x"]}]
kept, left = install.place_roms_entries(files, Path("/card/roms/bios"), {}, "x", say=False)
kept, left = install.place_relative_entries(files, Path("/card/roms/bios"), {}, "x", say=False)
self.assertEqual((kept, [o["reason"] for o in left]), ([], ["no_roms_directory"]))
def test_unsafe_emulator_folders_are_refused(self):
@@ -1192,7 +1192,7 @@ class OuterRootsAreUntrusted(unittest.TestCase):
with unittest.mock.patch.object(install.os, "name", host):
for value in ("../C:x", "..\\x", "../x:", "../Roms:x"):
with self.subTest(host=host, from_bios=value), self.assertRaises(ValueError):
install._check_roms_directory(
install._check_relative_directories(
{"roms_directory": {**self.ROMS, "from_bios": value}})
for value in ("C:x/{key}", "x:/{key}", "emu:x/{key}"):
layout = {**self.EMULATORS, "searched": [value], "default": value}
@@ -7721,5 +7721,32 @@ class TheListVersionIsSaidBeforeAndAfter(unittest.TestCase):
self.assertNotIn("This list was built for", proc.stdout)
class APackageHintNamesThePackageCommand(unittest.TestCase):
"""Run from the retrobios package, a hint said `python3 install.py`,
a file the user does not have."""
def test_the_environment_names_the_command(self):
for prefix, command in (
("/home/me/.cache/uv/archive-v0/UwL4i3xOlCEYSTnw", "uvx retrobios"),
("C:\\Users\\me\\AppData\\Local\\uv\\cache\\archive-v0\\x1", "uvx retrobios"),
("/home/me/.cache/pipx/539b8243c11ee7c", "pipx run retrobios"),
("/home/me/.local/pipx/.cache/539b8243c11ee7c", "pipx run retrobios"),
("/home/me/.local/share/pipx/venvs/retrobios", "retrobios"),
("/home/me/.local/share/uv/tools/retrobios", "retrobios"),
("/home/me/venv", "retrobios"),
):
with self.subTest(prefix=prefix):
self.assertEqual(install.package_command(prefix), command)
def test_the_launcher_is_the_package_command(self):
hint = install.Invocation()
with unittest.mock.patch.dict(os.environ, {"RETROBIOS_BOOTSTRAP": "pypi"}), \
unittest.mock.patch.object(install.sys, "prefix", "/home/me/.cache/uv/archive-v0/a"):
self.assertEqual(hint.launcher(), "uvx retrobios")
self.assertEqual(hint.launcher(elevated=True), "sudo uvx retrobios")
self.assertTrue(hint.command("retroarch", "DIR").startswith(
"uvx retrobios --platform retroarch --dest DIR"))
if __name__ == "__main__":
unittest.main()
+135 -31
View File
@@ -21,7 +21,6 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from launchers import handed_directories, standalone_bindings # noqa: E402
from scraper import knulli_launchers as kl # noqa: E402
from scraper import retrobat_launchers as rl # noqa: E402
from scraper.batocera_launchers import ( # noqa: E402
GENERATORS,
@@ -128,35 +127,27 @@ class AFolderTheGeneratorEnters(unittest.TestCase):
self.assertEqual(found, {"": ("outside", "")})
class ADataFolderTheEnvironmentSets(unittest.TestCase):
SUYU = f"{kl.GENERATORS}/suyu/suyuGenerator.py"
TEXT = textwrap.dedent("""\
from ...batoceraPaths import CACHE, CONFIGS, SAVES
class SuyuGenerator:
def generate(self, system, rom):
commandArray = ["/usr/bin/suyu", "-f", "-g", rom]
return Command.Command(array=commandArray, env={
"XDG_CONFIG_HOME": CONFIGS,
"XDG_DATA_HOME": SAVES / "switch",
})
""")
def test_the_data_folder_is_a_record_and_binds_outside(self):
files = {f"{kl.CONFIGGEN}/batoceraPaths.py": PATHS,
f"{kl.GENERATORS}/importer.py": IMPORTER_TEXT, self.SUYU: self.TEXT}
(record,) = records(kl.data_homes(kl.configgen(files.__getitem__)))
self.assertEqual(
(record["emulator"], record["search_dir"], record["passes_to"], record["source_ref"]),
("suyu", "/userdata/saves/switch", "environment XDG_DATA_HOME", f"{self.SUYU}:8"),
)
config = {"base_destination": "bios", "standalone_cores": ["suyu"], "launchers": [record]}
class AVariableADeclaredFolderIsSpelledFrom(unittest.TestCase):
def test_a_data_folder_outside_the_tree_binds_outside(self):
record = {"emulator": "emu", "build": "standalone", "kind": "passes_path",
"names": [], "search_dir": "/userdata/saves/switch",
"passes_to": "environment XDG_DATA_HOME", "source_ref": "x.py:1"}
config = {"base_destination": "bios", "standalone_cores": ["emu"], "launchers": [record]}
found = standalone_bindings(
{"suyu"}, ["$XDG_DATA_HOME/suyu", "SuyuPath::KeysDir = keys"],
handed_directories(config),
{"emu"}, ["$XDG_DATA_HOME/emu"], handed_directories(config)
)
self.assertEqual(found, {"": ("outside", "")})
def test_a_data_folder_in_the_tree_binds_below_it(self):
record = {"emulator": "emu", "build": "standalone", "kind": "passes_path",
"names": [], "search_dir": "/userdata/bios/data",
"passes_to": "environment XDG_DATA_HOME", "source_ref": "x.py:1"}
config = {"base_destination": "bios", "standalone_cores": ["emu"], "launchers": [record]}
found = standalone_bindings(
{"emu"}, ["$XDG_DATA_HOME/emu"], handed_directories(config)
)
self.assertEqual(found, {"": ("", "data/emu")})
class APortableUserFolder(unittest.TestCase):
CITRON = """
@@ -185,12 +176,28 @@ class APortableUserFolder(unittest.TestCase):
}
"""
RYUJINX = """
public override System.Diagnostics.ProcessStartInfo Generate(string system, string emulator, string core, string rom, string playersControllers, ScreenResolution resolution)
{
string path = AppConfig.GetFullPath("ryujinx");
string portablePath = Path.Combine(AppConfig.GetFullPath("saves"), "switch", "ryujinx", "portable");
string setupPath = Path.Combine(path, "portable");
}
"""
def sources(self) -> dict[str, rl.Source]:
def source(ref: str, text: str) -> rl.Source:
return rl.Source(ref, textwrap.dedent(text).lstrip("\n"))
def yuzu_like(exe: str) -> str:
return self.CITRON.replace("citron.exe", exe)
return {
f"{rl.GENERATORS}/Citron.Generator.cs": rl.Source(
"c", textwrap.dedent(self.CITRON).lstrip("\n")),
f"{rl.GENERATORS}/ShadPS4.Generator.cs": rl.Source(
"s", textwrap.dedent(self.SHADPS4).lstrip("\n")),
f"{rl.GENERATORS}/Citron.Generator.cs": source("c", self.CITRON),
f"{rl.GENERATORS}/Eden.Generator.cs": source("e", yuzu_like("eden.exe")),
f"{rl.GENERATORS}/Ryujinx.Generator.cs": source("r", self.RYUJINX),
f"{rl.GENERATORS}/ShadPS4.Generator.cs": source("s", self.SHADPS4),
f"{rl.GENERATORS}/Yuzu.Generator.cs": source("y", yuzu_like("yuzu.exe")),
}
def test_each_generator_gives_its_user_folder(self):
@@ -198,7 +205,15 @@ class APortableUserFolder(unittest.TestCase):
self.assertEqual(
[(r["emulator"], r["search_dir"], r["source_ref"]) for r in found],
[("citron", '<AppConfig.GetFullPath("citron")>/user', "c:3, 7-8"),
("shadps4", '<AppConfig.GetFullPath("shadps4")>/user', "s:3, 8-10")],
("eden", '<AppConfig.GetFullPath("eden")>/user', "e:3, 7-8"),
("ryujinx", '<AppConfig.GetFullPath("ryujinx")>/portable', "r:3-5"),
("shadps4", '<AppConfig.GetFullPath("shadps4")>/user', "s:3, 8-10"),
("yuzu", '<AppConfig.GetFullPath("yuzu")>/user', "y:3, 7-8")],
)
ryujinx = found[2]
self.assertEqual(ryujinx["passes_to"], "DefaultPortableDir, the portable folder beside Ryujinx.exe")
self.assertEqual(
ryujinx["depends_on_option"], "unless saves/switch/ryujinx/portable exists, passed with -r"
)
def test_the_user_folder_binds_below_the_emulator_folder(self):
@@ -213,5 +228,94 @@ class APortableUserFolder(unittest.TestCase):
self.assertEqual(found, {"": ("emulators", "citron/user")})
class ADefaultTheBuildLacksIsNotRun(unittest.TestCase):
"""KNULLI's configgen-defaults.yml picks suyu for the Switch, which its
es_systems.yml does not offer and no package builds."""
ES = textwrap.dedent("""\
switch:
emulators:
citron:
citron: { requireAnyOf: [BR2_PACKAGE_CITRON] }
voxatron:
emulators: {}
""")
DEFAULTS = textwrap.dedent("""\
switch:
emulator: suyu
core: suyu
voxatron:
emulator: lexaloffle
core: vox_official
kodi:
emulator: kodi
core: kodi
""")
LAUNCHERS = [{"emulator": "lexaloffle", "kind": "fixed_name", "passes_to": "exec",
"search_dir": "/userdata/bios"}]
def test_only_what_a_package_or_the_user_supplies_is_run(self):
from scraper import batocera_scraper as bs
paths = ["package/batocera/emulators/citron/Config.in", "package/kodi/Config.in"]
built = bs.packages_and_programs(bs.package_names(paths), self.LAUNCHERS)
cores, standalone, _dual = bs.configgen_cores(self.DEFAULTS, self.ES, set(), built)
self.assertEqual(sorted(cores), ["citron", "kodi", "vox_official"])
self.assertIn("lexaloffle", standalone)
self.assertNotIn("suyu", standalone)
by_system = bs.es_systems_by_system(self.ES, self.DEFAULTS, set(), built)
self.assertEqual(by_system["switch"], ["citron"])
# Without the packages, every default counts, as before.
self.assertIn("suyu", bs.configgen_cores(self.DEFAULTS, self.ES, set())[0])
def test_the_real_knulli_list_names_no_suyu(self):
import yaml
config = yaml.safe_load((REPO_ROOT / "platforms" / "knulli.yml").read_text())
self.assertNotIn("suyu", config["cores"])
self.assertNotIn("suyu", config.get("standalone_cores") or [])
class AUserFolderInTheConfigsFolder(unittest.TestCase):
"""Batocera starts shadPS4 from /userdata/system/configs/shadps4, a folder
of the configs root its batoceraPaths builds (../system/configs)."""
def test_the_sys_modules_go_below_the_configs_root(self):
from common import load_emulator_profiles, load_platform_config, platform_layouts
from common import standalone_set
profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["shadps4"]
for platform in ("batocera", "knulli"):
config = load_platform_config(platform, str(REPO_ROOT / "platforms"))
((view, _standalone),) = platform_layouts("shadps4", profile, standalone_set(config))
with self.subTest(platform=platform):
self.assertEqual(
{(f.get("root"), f["standalone_path"].rsplit("/", 1)[0]) for f in view["files"]},
{("configs", "shadps4/user/sys_modules"), ("configs", "shadps4/user/fonts/font")},
)
def test_the_installer_follows_from_bios_to_the_configs_folder(self):
import install
manifest = {"configs_directory": {"bios_folder": "bios", "from_bios": "../system/configs"}}
install._check_relative_directories(manifest)
files = [{"dest": "shadps4/user/sys_modules/libSceNgs2.sprx", "root": "configs"}]
bios = Path("/userdata/bios")
kept, left = install.place_relative_entries(files, bios, manifest, "x", say=False)
self.assertEqual((len(kept), left), (1, []))
self.assertEqual(
install._entry_place(bios, kept[0])[0], Path("/userdata/system/configs")
)
kept, left = install.place_relative_entries(
files, Path("/elsewhere/firmware"), manifest, "x", say=False)
self.assertEqual((kept, [o["reason"] for o in left]), ([], ["no_configs_directory"]))
for value in ("../C:x", "../../../x", "../system/../x"):
with self.subTest(from_bios=value), self.assertRaises(ValueError):
install._check_relative_directories(
{"configs_directory": {"bios_folder": "bios", "from_bios": value}})
if __name__ == "__main__":
unittest.main()
+7 -7
View File
@@ -383,10 +383,10 @@ class AProgramRunFromItsFolder(unittest.TestCase):
import install
manifest = self.manifest("romsplat")
install._check_roms_directory(manifest)
install._check_relative_directories(manifest)
files = list(manifest["files"])
card = self.root / "card" / "roms" / "bios"
kept, left = install.place_roms_entries(files, card, manifest, "romsplat", say=False)
kept, left = install.place_relative_entries(files, card, manifest, "romsplat", say=False)
self.assertEqual((len(kept), left), (2, []))
self.assertEqual(
install._entry_path(card, kept[0]),
@@ -394,22 +394,22 @@ class AProgramRunFromItsFolder(unittest.TestCase):
if install._entry_path(card, kept[0]).is_absolute() else card.parent / kept[0]["dest"],
)
other = self.root / "elsewhere"
kept, left = install.place_roms_entries(files, other, manifest, "romsplat", say=False)
kept, left = install.place_relative_entries(files, other, manifest, "romsplat", say=False)
self.assertEqual(kept, [])
self.assertEqual({o["reason"] for o in left}, {"no_roms_directory"})
with self.assertRaises(ValueError):
install._check_roms_directory({"files": files})
install._check_relative_directories({"files": files})
# A roms root beside the BIOS folder (Batocera's ../roms).
beside = {**manifest, "roms_directory": {"bios_folder": "bios", "from_bios": "../roms"}}
install._check_roms_directory(beside)
install._check_relative_directories(beside)
disk = self.root / "userdata" / "bios"
install.place_roms_entries(files, disk, beside, "romsplat", say=False)
install.place_relative_entries(files, disk, beside, "romsplat", say=False)
self.assertEqual(install._entry_place(disk, kept_entry := files[0])[0],
self.root / "userdata" / "roms")
self.assertEqual(kept_entry["root"], "roms")
for climbing in ("roms", "../roms/..", "/storage/roms"):
with self.subTest(from_bios=climbing), self.assertRaises(ValueError):
install._check_roms_directory(
install._check_relative_directories(
{**manifest, "roms_directory": {"bios_folder": "bios", "from_bios": climbing}}
)
+18 -2
View File
@@ -186,8 +186,10 @@ class TheInstalledCommandIsTheInstaller(Built):
def _same_answers(self, python: str, name: str) -> None:
command = self._installed(python, name)
# `retrobios` and `python -m retrobios` both run install.py.
# `retrobios` and `python -m retrobios` both run install.py, whose
# examples then name the package command.
launchers = ([str(command)], [str(command.with_name("python")), "-m", "retrobios"])
as_package = {**os.environ, "RETROBIOS_BOOTSTRAP": "pypi"}
for launcher in launchers:
for args in (["--help"], ["--version"], ["--no-such-option"]):
with self.subTest(python=name, launcher=launcher[-1], args=args):
@@ -195,7 +197,7 @@ class TheInstalledCommandIsTheInstaller(Built):
text=True, check=False, cwd=self.scratch)
script = subprocess.run([python, str(REPO_ROOT / "install.py"), *args],
capture_output=True, text=True, check=False,
cwd=self.scratch)
cwd=self.scratch, env=as_package)
self.assertEqual(
(packaged.returncode, packaged.stdout, packaged.stderr),
(script.returncode, script.stdout, script.stderr))
@@ -203,6 +205,20 @@ class TheInstalledCommandIsTheInstaller(Built):
def test_this_python(self):
self._same_answers(sys.executable, "venv-current")
def test_a_hint_gives_back_the_package_command(self):
"""Nothing detected, the example said `python3 install.py`."""
command = self._installed(sys.executable, "venv-hint")
home = self.scratch / "empty-home"
home.mkdir()
env = {**os.environ, "HOME": str(home), "USERPROFILE": str(home),
"RETROBIOS_INSTALL_OFFLINE": "1"}
env.pop("RETROBIOS_BOOTSTRAP", None)
run = subprocess.run([str(command), "--yes"], capture_output=True, text=True,
check=False, cwd=self.scratch, env=env)
out = run.stdout + run.stderr
self.assertIn("retrobios --platform retroarch --dest", out)
self.assertNotIn("install.py --platform", out)
def test_python_38(self):
python = _python38()
if python is None: