chore: merge launcher placement

This commit is contained in:
Abdessamad Derraz committed 2026-11-09 18:23:46 +01:00
commit 56ed37c3f0
32 files changed
+1289 -160

No files matched your search

+368
View File
@@ -0,0 +1,368 @@
"""A platform's launcher moves where an emulator's file lands.
A profile's ``path`` is where the emulator's code reads a file. UAE4All2
reads kickstarts/<name> below its working directory, and RetroPie starts it
in its install directory, whose kickstarts/ it links to BIOS/amiga. The
pack, the manifest and verify all put the file in amiga/ on that platform,
and a platform with no such record keeps the profile's layout.
"""
from __future__ import annotations
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
import hashlib
import io
import shutil
import sys
import tempfile
import unittest
import zipfile
from contextlib import redirect_stdout
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from common import ( # noqa: E402
_emulator_profiles_cache,
load_emulator_profiles,
load_platform_config,
platform_layouts,
standalone_set,
)
from generate_pack import generate_manifest, generate_pack # noqa: E402
from verify import verify_platform # noqa: E402
WORKING_DIRECTORY = {
"emulator": "kick", "build": "standalone", "kind": "passes_path", "names": [],
"search_dir": "$md_inst", "passes_to": "working directory of $romdir/amiga/+Start.sh",
"source_ref": "kick.sh:90",
}
LINK = {
"emulator": "kick", "build": "standalone", "kind": "links", "names": [],
"search_dir": "$biosdir/amiga", "passes_to": "$md_inst/kickstarts is a symlink to it",
"source_ref": "kick.sh:79",
}
class ALinkMovesTheDestination(unittest.TestCase):
def setUp(self) -> None:
_emulator_profiles_cache.clear()
(REPO_ROOT / "tmp").mkdir(exist_ok=True)
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, self.root, True)
bios = self.root / "bios" / "Commodore" / "Amiga"
bios.mkdir(parents=True)
files, by_md5, by_name = {}, {}, {}
for name, data in (("kick13.rom", b"KICKSTART 1.3"), ("kick.rom", b"CUSTOM KICKSTART")):
(bios / name).write_bytes(data)
sha1 = hashlib.sha1(data).hexdigest()
md5 = hashlib.md5(data).hexdigest()
files[sha1] = {"path": str(bios / name), "name": name, "sha1": sha1,
"md5": md5, "crc32": "", "size": len(data)}
by_md5[md5] = sha1
by_name[name] = [sha1]
self.db = {
"files": files,
"indexes": {"by_md5": by_md5, "by_name": by_name, "by_crc32": {},
"by_path_suffix": {}},
}
kick13 = by_name["kick13.rom"][0]
self.platforms = self.root / "platforms"
self.emulators = self.root / "emulators"
self.platforms.mkdir()
self.emulators.mkdir()
for name, launchers in (("linkplat", [WORKING_DIRECTORY, LINK]), ("plainplat", [])):
(self.platforms / f"{name}.yml").write_text(yaml.safe_dump({
"platform": name, "version": "1", "verification_mode": "existence",
"base_destination": "BIOS", "cores": ["kick"], "standalone_cores": ["kick"],
"launchers": launchers,
"systems": {"commodore-amiga": {"files": []}},
}))
(self.platforms / "_registry.yml").write_text(yaml.safe_dump({"platforms": {
"linkplat": {"config": "linkplat.yml"}, "plainplat": {"config": "plainplat.yml"},
}}))
(self.emulators / "kick.yml").write_text(yaml.safe_dump({
"emulator": "Kick", "type": "standalone", "cores": ["kick"],
"systems": ["commodore-amiga"],
"standalone_directories": ["getcwd"],
"files": [
{"name": "kick13.rom", "path": "kickstarts/kick13.rom",
"sha1": kick13, "required": False},
# Read in the working directory itself, which the platform
# links nowhere in the BIOS tree.
{"name": "kick.rom", "path": "kick.rom", "required": False},
],
}))
def pack_names(self, platform: str, name: str = "kick13.rom") -> list[str]:
with redirect_stdout(io.StringIO()):
zip_path = generate_pack(
platform, str(self.platforms), self.db, str(self.root / "bios"),
str(self.root / "out" / platform), emulators_dir=str(self.emulators),
offline=True,
)
with zipfile.ZipFile(zip_path) as zf:
return [n for n in zf.namelist() if n.rsplit("/", 1)[-1] == name]
def manifest(self, platform: str) -> dict:
with redirect_stdout(io.StringIO()):
return generate_manifest(
platform, str(self.platforms), self.db, str(self.root / "bios"),
str(self.platforms / "_registry.yml"),
emulators_dir=str(self.emulators), offline=True,
)
def manifest_dests(self, platform: str, name: str = "kick13.rom") -> list[str]:
return [f["dest"] for f in self.manifest(platform)["files"]
if f["dest"].rsplit("/", 1)[-1] == name]
def verify_entries(self, platform: str, name: str = "kick13.rom") -> list[dict]:
config = load_platform_config(platform, str(self.platforms))
result = verify_platform(config, self.db, str(self.emulators))
return [u for u in result["undeclared_files"] if u["name"] == name]
def verify_paths(self, platform: str) -> list[str]:
return [u["path"] for u in self.verify_entries(platform)]
def test_the_link_moves_it_in_pack_manifest_and_verify(self) -> None:
self.assertEqual(self.pack_names("linkplat"), ["amiga/kick13.rom"])
self.assertEqual(self.manifest_dests("linkplat"), ["amiga/kick13.rom"])
self.assertEqual(self.verify_paths("linkplat"), ["amiga/kick13.rom"])
def test_a_platform_without_the_record_is_unchanged(self) -> None:
self.assertEqual(self.pack_names("plainplat"), ["kickstarts/kick13.rom"])
self.assertEqual(self.manifest_dests("plainplat"), ["kickstarts/kick13.rom"])
self.assertEqual(self.verify_paths("plainplat"), ["kickstarts/kick13.rom"])
def test_a_file_outside_every_binding_is_named_never_placed(self) -> None:
self.assertEqual(self.pack_names("linkplat", "kick.rom"), [])
manifest = self.manifest("linkplat")
self.assertEqual(self.manifest_dests("linkplat", "kick.rom"), [])
omitted = [o for o in manifest["omitted_files"] if o["name"] == "kick.rom"]
self.assertEqual(len(omitted), 1)
self.assertEqual(
(omitted[0]["dest"], omitted[0]["root"], omitted[0]["reason"], omitted[0]["cores"]),
("kick.rom", "emulator", "outside_bios_folder", ["kick"]),
)
(entry,) = self.verify_entries("linkplat", "kick.rom")
self.assertFalse(entry["in_pack"])
self.assertIn("outside the BIOS tree", entry["omitted"])
def test_a_root_file_of_another_core_is_not_claimed_by_it(self) -> None:
# Another core reads kick.rom at the BIOS root: kick's own kick.rom,
# in its working directory, is a different place.
for name in ("linkplat", "plainplat"):
path = self.platforms / f"{name}.yml"
data = yaml.safe_load(path.read_text())
data["cores"] = ["kick", "other"]
path.write_text(yaml.safe_dump(data))
(self.emulators / "other.yml").write_text(yaml.safe_dump({
"emulator": "Other", "type": "libretro", "cores": ["other"],
"systems": ["commodore-amiga"],
"files": [{"name": "kick.rom", "path": "kick.rom", "required": False}],
}))
_emulator_profiles_cache.clear()
manifest = self.manifest("linkplat")
(held,) = [f for f in manifest["files"] if f["dest"] == "kick.rom"]
self.assertEqual(held["cores"], ["other"])
omitted = [o for o in manifest["omitted_files"] if o["name"] == "kick.rom"]
self.assertEqual([(o["root"], o["cores"]) for o in omitted], [("emulator", ["kick"])])
def test_the_installer_accepts_the_omission(self) -> None:
import install
manifest = self.manifest("linkplat")
omitted = next(o for o in manifest["omitted_files"] if o["name"] == "kick.rom")
install._check_omission(omitted, omitted["dest"])
# Two emulators reading kick.rom in their own folders read two files.
self.assertNotEqual(
install._entry_key(omitted), install._entry_key({**omitted, "cores": ["other"]})
)
self.assertNotEqual(install._entry_key(omitted), install._entry_key({"dest": "kick.rom"}))
self.assertEqual(install.OMISSION_REASONS[omitted["reason"]], "read outside the BIOS folder")
with self.assertRaises(ValueError):
install._check_root({"root": "emulator"}, "kick.rom")
install._check_root(omitted, "kick.rom", omitted=True)
def test_without_relocation_the_root_file_stays_at_the_root(self) -> None:
self.assertEqual(self.pack_names("plainplat", "kick.rom"), ["kick.rom"])
def test_the_link_alone_binds_nothing(self) -> None:
# Without the working directory the link's $md_inst is not the
# directory the profile's paths are relative to.
config = {"standalone_cores": ["kick"], "launchers": [LINK]}
profile = load_emulator_profiles(str(self.emulators))["kick"]
(view, standalone), = platform_layouts("kick", profile, standalone_set(config))
self.assertTrue(standalone)
self.assertNotIn("standalone_path", view["files"][0])
class UAE4AllOnRetroPie(unittest.TestCase):
"""The real profile against the real launcher records."""
def test_kickstarts_land_in_amiga(self) -> None:
profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["uae4all"]
config = load_platform_config("retropie", str(REPO_ROOT / "platforms"))
(view, standalone), = platform_layouts("uae4all", profile, standalone_set(config))
self.assertTrue(standalone)
for entry in view["files"]:
if entry["path"].startswith("kickstarts/"):
self.assertEqual(
entry["standalone_path"], "amiga/" + entry["path"].removeprefix("kickstarts/")
)
else:
# kick.rom and rom.key in the working directory, $md_inst
self.assertEqual(entry.get("unplaced"), "outside_bios_folder", entry["path"])
PROGRAM = b"PROGRAM BINARY"
PROGRAM_DATA = b"PROGRAM DATA"
class AProgramRunFromItsFolder(unittest.TestCase):
"""A program that reads its files beside its executable.
The profile gives the layout of the program's archive (prog/prog,
prog/prog.dat) and declares the executable directory. A platform that
runs prog/prog from its BIOS folder keeps that layout, one that runs it
from roms/prog places the program folder below its roms folder, and one
that runs it from an emulator folder names its files as not placeable.
"""
def setUp(self) -> None:
_emulator_profiles_cache.clear()
(REPO_ROOT / "tmp").mkdir(exist_ok=True)
self.root = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp"))
self.addCleanup(shutil.rmtree, self.root, True)
bios = self.root / "bios" / "Maker" / "Prog"
bios.mkdir(parents=True)
files, by_md5, by_name = {}, {}, {}
for name, data in (("prog", PROGRAM), ("prog.dat", PROGRAM_DATA)):
(bios / name).write_bytes(data)
sha1 = hashlib.sha1(data).hexdigest()
md5 = hashlib.md5(data).hexdigest()
files[sha1] = {"path": str(bios / name), "name": name, "sha1": sha1,
"md5": md5, "crc32": "", "size": len(data)}
by_md5[md5] = sha1
by_name[name] = [sha1]
self.db = {"files": files, "indexes": {
"by_md5": by_md5, "by_name": by_name, "by_crc32": {}, "by_path_suffix": {}}}
self.platforms = self.root / "platforms"
self.emulators = self.root / "emulators"
self.platforms.mkdir()
self.emulators.mkdir()
run = {"emulator": "prog", "build": "standalone", "kind": "fixed_name",
"passes_to": "exec", "source_ref": "start_prog.sh:9"}
launchers = {
"biosplat": [{**run, "names": ["prog/prog"], "search_dir": "/userdata/bios"}],
"romsplat": [{**run, "names": ["prog"], "search_dir": "/storage/roms/prog"}],
"exeplat": [{**run, "names": ["prog"], "search_dir": "<emulator folder>"}],
"plainplat": [],
}
for name, records in launchers.items():
(self.platforms / f"{name}.yml").write_text(yaml.safe_dump({
"platform": name, "version": "1", "verification_mode": "existence",
"base_destination": "bios", "cores": ["prog"], "standalone_cores": ["prog"],
"launchers": records,
"systems": {"prog-system": {"files": []}},
}))
(self.platforms / "_registry.yml").write_text(yaml.safe_dump({"platforms": {
**{name: {"config": f"{name}.yml"} for name in launchers},
"romsplat": {"config": "romsplat.yml",
"roms_directory": {"path": "/storage/roms", "bios_folder": "bios"}},
}}))
(self.emulators / "prog.yml").write_text(yaml.safe_dump({
"emulator": "Prog", "type": "standalone", "cores": ["prog"],
"systems": ["prog-system"],
"standalone_directories": ["executable directory"],
"files": [
{"name": "prog", "path": "prog/prog", "sha1": by_name["prog"][0],
"required": True},
{"name": "prog.dat", "path": "prog/prog.dat",
"sha1": by_name["prog.dat"][0], "required": True},
],
}))
def pack(self, platform: str) -> list[str]:
with redirect_stdout(io.StringIO()):
zip_path = generate_pack(
platform, str(self.platforms), self.db, str(self.root / "bios"),
str(self.root / "out" / platform), emulators_dir=str(self.emulators),
offline=True,
)
with zipfile.ZipFile(zip_path) as zf:
return sorted(n for n in zf.namelist() if n.split("/")[-1].startswith("prog"))
def manifest(self, platform: str) -> dict:
with redirect_stdout(io.StringIO()):
return generate_manifest(
platform, str(self.platforms), self.db, str(self.root / "bios"),
str(self.platforms / "_registry.yml"),
emulators_dir=str(self.emulators), offline=True,
)
def verify(self, platform: str) -> list[dict]:
config = load_platform_config(platform, str(self.platforms))
result = verify_platform(config, self.db, str(self.emulators))
return [u for u in result["undeclared_files"] if u["name"].startswith("prog")]
def test_run_from_the_bios_folder_keeps_the_layout(self) -> None:
for platform in ("biosplat", "plainplat"):
with self.subTest(platform=platform):
self.assertEqual(self.pack(platform), ["prog/prog", "prog/prog.dat"])
self.assertEqual(
sorted(f["dest"] for f in self.manifest(platform)["files"]),
["prog/prog", "prog/prog.dat"],
)
def test_run_from_the_roms_folder_is_installed_there(self) -> None:
self.assertEqual(self.pack("romsplat"), [])
manifest = self.manifest("romsplat")
self.assertEqual(
sorted((f["dest"], f.get("root")) for f in manifest["files"]),
[("prog/prog", "roms"), ("prog/prog.dat", "roms")],
)
self.assertEqual(manifest["roms_directory"], {"bios_folder": "bios"})
entries = self.verify("romsplat")
self.assertEqual(sorted(u["path"] for u in entries), ["prog/prog", "prog/prog.dat"])
self.assertTrue(all(u["in_pack"] is False for u in entries))
def test_run_from_an_emulator_folder_is_named(self) -> None:
self.assertEqual(self.pack("exeplat"), [])
manifest = self.manifest("exeplat")
self.assertEqual([f["dest"] for f in manifest["files"]], [])
self.assertEqual(
sorted((o["dest"], o["root"], o["reason"]) for o in manifest["omitted_files"]),
[("prog/prog", "emulator", "outside_bios_folder"),
("prog/prog.dat", "emulator", "outside_bios_folder")],
)
def test_the_installer_finds_the_roms_folder_above_the_bios_folder(self) -> None:
import install
manifest = self.manifest("romsplat")
install._check_roms_directory(manifest)
files = list(manifest["files"])
card = self.root / "card" / "roms" / "bios"
kept, left = install.place_roms_entries(files, card, manifest, "romsplat", say=False)
self.assertEqual((len(kept), left), (2, []))
self.assertEqual(
install._entry_path(card, kept[0]),
(card.parent / kept[0]["dest"]).resolve()
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)
self.assertEqual(kept, [])
self.assertEqual({o["reason"] for o in left}, {"no_roms_directory"})
with self.assertRaises(ValueError):
install._check_roms_directory({"files": files})
if __name__ == "__main__":
unittest.main()
+30
View File
@@ -1145,6 +1145,36 @@ class FolderArguments(unittest.TestCase):
)
PICO8 = """
public override System.Diagnostics.ProcessStartInfo Generate(string system, string emulator, string core, string rom, string playersControllers, ScreenResolution resolution)
{
string path = AppConfig.GetFullPath("pico8");
string exe = Path.Combine(path, "pico8.exe");
if (!File.Exists(exe))
return null;
return new ProcessStartInfo()
{
FileName = exe,
WorkingDirectory = path,
Arguments = args,
};
}
"""
class ProgramsRunFromEmulatorFolders(unittest.TestCase):
def test_pico8_runs_its_binary_from_its_emulator_folder(self):
(record,) = rl.pico8(_sources(**{f"{G}/Pico8.Generator.cs": PICO8}))
self.assertEqual(
(record["emulator"], record["kind"], record["names"], record["search_dir"],
record["passes_to"], record["source_ref"]),
("pico8", "fixed_name", ["pico8.exe"], '<AppConfig.GetFullPath("pico8")>', "exec",
f"{G}/Pico8.Generator.cs:3-7, 11-12"),
)
class EveryBiosReferenceIsRead(unittest.TestCase):
def test_unread_reference_stops_the_scrape(self):
extra = EKA2L1 + (
+34
View File
@@ -293,6 +293,28 @@ DRASTIC = module("drastic", """
""", 36)
UAE4ALL = module("uae4all", """
function configure_uae4all() {
mkRomDir "amiga"
moveConfigDir "$md_inst/kickstarts" "$biosdir/amiga"
rm -f "$romdir/amiga/+Start UAE4All.sh"
if [[ "$md_mode" == "install" ]]; then
cat > "$romdir/amiga/+Start UAE4All.sh" << _EOF_
#!/bin/bash
pushd "$md_inst"
./uae4all
popd
_EOF_
fi
addEmulator 1 "$md_id" "amiga" "bash $romdir/amiga/+Start\\ UAE4All.sh"
addSystem "amiga"
}
""", 60)
class WorkingDirectory(unittest.TestCase):
def test_drastic_reads_below_the_folder_its_script_enters(self):
found = records({f"{EMULATORS}/drastic.sh": DRASTIC}, "drastic")
@@ -306,6 +328,18 @@ class WorkingDirectory(unittest.TestCase):
)],
)
def test_uae4all_runs_from_its_install_directory(self):
found = records({f"{EMULATORS}/uae4all.sh": UAE4ALL}, "uae4all")
entered = [r for r in found if r["kind"] == "passes_path"]
self.assertEqual(
[(r["search_dir"], r["passes_to"], r["source_ref"]) for r in entered],
[(
"$md_inst",
"working directory of $romdir/amiga/+Start UAE4All.sh",
f"{EMULATORS}/uae4all.sh:69",
)],
)
def test_a_launch_script_that_no_longer_enters_a_folder_raises(self):
files = {f"{EMULATORS}/drastic.sh": DRASTIC.replace('pushd "', 'echo "')}
with self.assertRaises(rl.LauncherPatternMissing):
+47
View File
@@ -551,6 +551,53 @@ class Links(unittest.TestCase):
rl.links(tree)
START_PICO8 = text(
"""
#!/bin/bash
. /etc/profile
GAME_DIR="/storage/roms/pico-8/"
case ${HW_ARCH} in
aarch64)
STATIC_BIN="pico8_64"
;;
*)
STATIC_BIN="pico8_dyn"
;;
esac
if [ -d "${GAME_DIR}/${HW_ARCH}" ]
then
LAUNCH_DIR="${GAME_DIR}/${HW_ARCH}"
else
LAUNCH_DIR="${GAME_DIR}"
fi
${LAUNCH_DIR}/${STATIC_BIN} -home -root_path ${GAME_DIR} -joystick 0 ${OPTIONS} "${CART}"
"""
)
class ProgramsRunOutsideTheBiosFolder(unittest.TestCase):
PATH = f"{SA}/pico-8/scripts/start_pico8.sh"
def test_pico8_runs_its_binary_from_roms(self):
(record,) = [r.entry() for r in rl.pico8(RepoTree.from_files({self.PATH: START_PICO8}))]
self.assertEqual(
(record["emulator"], record["kind"], record["names"], record["search_dir"],
record["passes_to"]),
("pico-8", "fixed_name", ["pico8_64", "pico8_dyn"], "/storage/roms/pico-8", "exec"),
)
self.assertIn("HW_ARCH aarch64 runs pico8_64", record["depends_on_option"])
self.assertEqual(record["source_ref"], f"{self.PATH}:4-11, 15-22")
def test_a_script_without_its_binary_choice_raises(self):
broken = START_PICO8.replace('STATIC_BIN="pico8_dyn"', "STATIC_BIN=$OTHER")
with self.assertRaises(LauncherPatternMissing):
rl.pico8(RepoTree.from_files({self.PATH: broken}))
class Revision(unittest.TestCase):
def test_the_revision_is_the_one_the_bios_list_is_read_at(self):
url = (
+26 -12
View File
@@ -26,11 +26,16 @@ from common import ( # noqa: E402
load_emulator_profiles,
load_platform_config,
platform_layouts,
standalone_bindings_for,
standalone_destination,
standalone_root_for,
standalone_set,
)
from launchers import handed_directories, normalize_handle, standalone_root # noqa: E402
from launchers import ( # noqa: E402
bound_path,
handed_directories,
normalize_handle,
standalone_bindings,
)
def link(emulator: str, search_dir: str, passes_to: str, **fields: object) -> dict:
@@ -101,7 +106,11 @@ class HandedDirectories(unittest.TestCase):
class Root(unittest.TestCase):
def root(self, *launchers: dict, base: str = "bios") -> str | None:
return standalone_root(
found = self.bindings(*launchers, base=base).get("")
return found[1] if found else None
def bindings(self, *launchers: dict, base: str = "bios") -> dict[str, tuple[str, str]]:
return standalone_bindings(
{"emu"}, PROFILE["standalone_directories"], handed_directories(config(*launchers, base=base))
)
@@ -130,12 +139,15 @@ class Root(unittest.TestCase):
"dc",
)
def test_link_of_another_directory_gives_nothing(self) -> None:
def test_link_of_a_subfolder_binds_that_subfolder_only(self) -> None:
# A subfolder of the emulator's tree (Dolphin's Triforce) is not the
# directory standalone_path is relative to.
self.assertIsNone(self.root(
# directory standalone_path is relative to, only the files below it move.
found = self.bindings(
link("emu", "$bios_path/Triforce", "$XDG_DATA_HOME/emu/Triforce is a symlink to it"),
))
)
self.assertEqual(found, {"Triforce": ("", "Triforce")})
self.assertEqual(bound_path("Triforce/segaboot.gcm", found), ("", "Triforce/segaboot.gcm"))
self.assertIsNone(bound_path("boot.bin", found))
def test_two_folders_give_nothing(self) -> None:
self.assertIsNone(self.root(
@@ -186,13 +198,14 @@ class Layout(unittest.TestCase):
union = standalone_set(config(self.LINKED, base="BIOS")) | standalone_set(config(
link("emu", "${biosPath}/emu/bios", "symlink ${biosPath}/emu/bios -> ${HOME}/.local/share/emu"),
))
self.assertIsNone(standalone_root_for("emu", PROFILE, union))
self.assertEqual(standalone_bindings_for("emu", PROFILE, union), {})
def test_destination_function(self) -> None:
entry = PROFILE["files"][0]
self.assertEqual(standalone_destination(entry, "dc"), "dc/boot.bin")
self.assertEqual(standalone_destination(entry, None), "boot.bin")
self.assertEqual(standalone_destination(PROFILE["files"][2], "dc"), "dc/shared.bin")
self.assertEqual(standalone_destination(entry, PROFILE, {"": ("", "dc")}), ("", "dc/boot.bin"))
self.assertIsNone(standalone_destination(entry, PROFILE, {}))
# The entry both builds share keeps path in a dual-build profile.
self.assertIsNone(standalone_destination(PROFILE["files"][2], PROFILE, {"": ("", "dc")}))
class FlycastOnEachPlatform(unittest.TestCase):
@@ -215,7 +228,8 @@ class FlycastOnEachPlatform(unittest.TestCase):
with self.subTest(platform=platform):
cfg = load_platform_config(platform, str(REPO_ROOT / "platforms"))
self.assertEqual(
standalone_root_for("flycast", self.profile, standalone_set(cfg)), folder
standalone_bindings_for("flycast", self.profile, standalone_set(cfg)),
{"": ("", folder)},
)
def test_no_standalone_file_lands_at_the_bios_root(self) -> None: