chore: merge the launcher folder placement

This commit is contained in:
Abdessamad Derraz committed 2026-11-10 12:03:30 +01:00
commit 51e5fd0dbd
7 files changed
+216 -11

No files matched your search

+4 -1
View File
@@ -22,6 +22,8 @@ systems:
bios_directory: "bios/"
resources_directory: "resources/"
standalone_directories:
- "[Folders] Bios = bios"
data_root:
android:
@@ -53,7 +55,8 @@ notes: |
A BIOS image is required and boot stops without one. It belongs in the bios
folder of the data directory given in data_root, and the Folders section of
PCSX2.ini names another path when set.
ref: AetherSX2-release MacOS/AetherSX2, aethersx2 archive/desktop/mac/README.md
ref: AetherSX2-release MacOS/AetherSX2, aethersx2 archive/desktop/mac/README.md,
aethersx2/pcsx2/Pcsx2Config.cpp:935-946
Detection is by content. The scan lists the BIOS folder with a *.* mask, keeps
files between 4 and 8 MB, and accepts an image whose romdir carries a RESET
+2
View File
@@ -24,6 +24,8 @@ systems:
bios_directory: "bios/"
resources_directory: "resources/"
standalone_directories:
- "[Folders] Bios = bios"
data_root:
android:
+2
View File
@@ -16,6 +16,8 @@ built_by: [retropie]
systems:
- nec-pc-88
- nec-pc-80
standalone_directories:
- "-romdir = rom"
notes: |
Continuation of Showzoh Fukunaga's QUASI88, version 0.7.1 at the pinned
+18 -3
View File
@@ -312,8 +312,10 @@ def launcher_places(config: dict) -> list[LauncherPlace]:
# component scripts $bios_path.
BIOS_ROOT_VARIABLES = ("$biosdir", "$home/RetroPie/BIOS", "${biosPath}", "$bios_path")
# The kinds that hand an emulator a whole directory.
HANDED_KINDS = ("links", "passes_path")
# The kinds that hand an emulator a whole directory. A scans_dir record sets
# the directory the emulator picks its file from: ROCKNIX writes ARMSX2's
# [Folders] Bios, EmuDeck PCSX2's.
HANDED_KINDS = ("links", "passes_path", "scans_dir")
# How a launcher record says the platform starts the emulator in a directory
# (retropie_launchers._working_directory), and the handle a profile gives
@@ -357,6 +359,19 @@ def normalize_handle(token: str) -> str:
return _FLATPAK_DATA.sub("$XDG_DATA_HOME", token)
_INI_KEY = re.compile(r"(\[[^\]\s]+\])\s+([^\s,;=()]+)")
def ini_keys(passes_to: str) -> frozenset[str]:
"""The INI keys a record's ``passes_to`` writes, as "[Section] Key".
A key alone is ambiguous: PCSX2 reads its BIOS folder from [Folders]
Bios and the image name from [Filenames] BIOS. A profile declares the
key it resolves a folder from in that spelling ("[Folders] Bios = bios").
"""
return frozenset(f"{section} {key}" for section, key in _INI_KEY.findall(passes_to))
def _folder_in_bios(directory: str, base: str) -> str | None:
"""A directory relative to the BIOS root, None when it lies outside."""
relative = _bios_relative(directory, base)
@@ -485,7 +500,7 @@ def handed_directories(config: dict) -> list[HandedDirectory]:
handle
for token in passes_to.split()
if (handle := normalize_handle(token)) and handle != directory
)
) | ini_keys(passes_to)
folder = _folder_in_bios(part, base)
for emulator in (e.strip() for e in str(record.get("emulator", "")).split(",")):
found.append(HandedDirectory(
+15 -4
View File
@@ -567,7 +567,15 @@ def _agnostic_scan_extras(
agnostic_votes.get(directory + "/", 0) + 1
)
for profile, standalone in platform_layouts(emu_name, profile, standalone_set or set()):
own = profile
for profile, standalone in platform_layouts(emu_name, own, standalone_set or set()):
# A layout copies each entry in order (a launcher's folder, a
# system directory): its files are looked up as the profile's
# own, or a bound layout scans nothing (KNULLI's bios/ps2).
origin = {
id(view): entry
for view, entry in zip(profile.get("files", []), own.get("files", []))
}
for f in profile.get("files", []):
if not is_profile_agnostic and not f.get("agnostic"):
continue
@@ -617,8 +625,8 @@ def _agnostic_scan_extras(
companion_ext, image = _companion_of(profile, fname)
path_prefix = (
resolved_dirs.get(id(image), "") if image
else resolved_dirs.get(id(f), "")
resolved_dirs.get(id(origin.get(id(image), image)), "") if image
else resolved_dirs.get(id(origin.get(id(f), f)), "")
)
# A placeholder (<bios>.bin) resolves to nothing and anchors no
# directory. The folders where the system's other profiles
@@ -639,7 +647,10 @@ def _agnostic_scan_extras(
# A name matching one file identifies it. A name matching several
# identifies nothing on its own, so the profile's other files have
# to agree on the directory before the scan walks it.
ambiguous = id(f) in named_only and len(by_name.get(fname, [])) > 1
ambiguous = (
id(origin.get(id(f), f)) in named_only
and len(by_name.get(fname, [])) > 1
)
if ambiguous and agnostic_votes.get(path_prefix, 0) < 2:
continue
+7 -3
View File
@@ -564,15 +564,19 @@ def find_undeclared_files(
if emu_name not in relevant:
continue
# Skip agnostic profiles entirely (filename-agnostic BIOS detection)
if profile.get("bios_mode") == "agnostic":
continue
# A filename-agnostic profile is served by the scan (packextras), all
# but the entries its layout reads outside the BIOS tree: those are
# named here, or a launcher folder drops them without a word
# (ARMSX2's arcade/proverb.zip once ROCKNIX binds bios/armsx2).
agnostic = profile.get("bios_mode") == "agnostic"
for profile, is_standalone in platform_layouts(emu_name, profile, standalone_set):
for f in expand_directory_entries(
profile.get("files", []), db, is_standalone
):
if agnostic and not f.get("unplaced"):
continue
fname = f.get("name", "")
# One destination rule for the dedup key and for the entry: a
# standalone build without standalone_path falls back on path.
+168
View File
@@ -553,5 +553,173 @@ class EachProgramItsOwnProfile(unittest.TestCase):
self.assertEqual(resolve_platform_cores(config, profiles), {"emu"})
BIOS_SETTING = {
"emulator": "ps2emu", "build": "standalone", "kind": "passes_path", "names": [],
"search_dir": "/userdata/bios/ps2",
"passes_to": "inis/PCSX2.ini [Folders] Bios = ../../../bios/ps2, relative to the data root",
"source_ref": "ps2emuGenerator.py:219",
}
# The same folder through a scans_dir record, and a record setting the image
# name alone, which hands no folder.
BIOS_SCAN = {**BIOS_SETTING, "kind": "scans_dir", "passes_to": "PCSX2.ini [Folders] Bios"}
BIOS_NAME = {**BIOS_SETTING, "passes_to": "PCSX2.ini [Filenames] BIOS"}
class ASettingSetsTheBiosFolder(unittest.TestCase):
"""KNULLI sets AetherSX2's [Folders] Bios to bios/ps2.
The profile reads its BIOS images by content from bios/ under its data
root, and names that setting in standalone_directories. Every image the
scan finds goes to ps2/ in the pack, the manifest and verify, and a file
the layout keeps outside its bios/ folder is named, never packed.
"""
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)
files, by_md5, by_name = {}, {}, {}
for folder, name, data in (
("Sony/PlayStation 2", "img-a.bin", b"PS2 IMAGE A....."),
("Sony/PlayStation 2", "img-b.bin", b"PS2 IMAGE B....."),
("Other/ps2emu/arcade", "boot.zip", b"BOOT PROGRAMS"),
):
where = self.root / "bios" / folder
where.mkdir(parents=True, exist_ok=True)
(where / name).write_bytes(data)
sha1 = hashlib.sha1(data).hexdigest()
md5 = hashlib.md5(data).hexdigest()
files[sha1] = {"path": str(where / 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()
platforms = {"setplat": [BIOS_SETTING], "scanplat": [BIOS_SCAN],
"nameplat": [BIOS_NAME], "plainplat": []}
for name, launchers in platforms.items():
(self.platforms / f"{name}.yml").write_text(yaml.safe_dump({
"platform": name, "version": "1", "verification_mode": "existence",
"base_destination": "bios", "cores": ["ps2emu"],
"standalone_cores": ["ps2emu"], "launchers": launchers,
"systems": {"sony-playstation-2": {"files": []}},
}))
(self.platforms / "_registry.yml").write_text(yaml.safe_dump({"platforms": {
name: {"config": f"{name}.yml"} for name in platforms
}}))
(self.emulators / "ps2emu.yml").write_text(yaml.safe_dump({
"emulator": "PS2Emu", "type": "standalone", "bios_mode": "agnostic",
"cores": ["ps2emu"], "systems": ["sony-playstation-2"],
"bios_directory": "bios/",
"standalone_directories": ["[Folders] Bios = bios"],
"files": [
{"name": "img-a.bin", "path": "bios/img-a.bin", "required": True,
"min_size": 16, "max_size": 16, "validation": ["size"]},
# Read below the data root, never below its bios/ folder.
{"name": "boot.zip", "standalone_path": "arcade/boot.zip",
"mode": "standalone", "required": False},
],
}))
def pack_names(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.endswith((".bin", ".zip")))
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_dests(self, platform: str) -> list[str]:
config = load_platform_config(platform, str(self.platforms))
result = verify_platform(config, self.db, str(self.emulators))
return sorted(d for d in result["pack_destinations"] if d.endswith((".bin", ".zip")))
def assert_placed(self, platform: str, expected: list[str]) -> None:
self.assertEqual(self.pack_names(platform), expected)
self.assertEqual(
sorted(f["dest"] for f in self.manifest(platform)["files"]), expected
)
self.assertEqual(self.verify_dests(platform), expected)
def test_every_scanned_image_lands_in_the_folder_set(self) -> None:
self.assert_placed("setplat", ["ps2/img-a.bin", "ps2/img-b.bin"])
def test_a_scans_dir_record_sets_it_too(self) -> None:
self.assert_placed("scanplat", ["ps2/img-a.bin", "ps2/img-b.bin"])
def test_the_image_name_setting_hands_no_folder(self) -> None:
self.assert_placed("nameplat", ["img-a.bin", "img-b.bin"])
def test_a_platform_without_the_record_keeps_the_layout(self) -> None:
self.assert_placed("plainplat", ["img-a.bin", "img-b.bin"])
def test_a_file_outside_the_bios_folder_is_named(self) -> None:
omitted = [o for o in self.manifest("setplat")["omitted_files"]
if o["name"] == "boot.zip"]
self.assertEqual(
[(o["dest"], o["root"], o["reason"], o["cores"]) for o in omitted],
[("arcade/boot.zip", "emulator", "outside_bios_folder", ["ps2emu"])],
)
def test_the_ini_key_carries_its_section(self) -> None:
from launchers import ini_keys
self.assertEqual(
ini_keys("PCSX2.ini [Folders] Bios = /storage/roms/bios/armsx2, [Filenames] BIOS"),
{"[Folders] Bios", "[Filenames] BIOS"},
)
self.assertEqual(ini_keys("command line -rompath"), frozenset())
class AetherSX2OnKnulli(unittest.TestCase):
"""The real profile against KNULLI's aethersx2Generator.py:219 record."""
def test_the_bios_images_land_in_ps2(self) -> None:
profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["aethersx2"]
config = load_platform_config("knulli", str(REPO_ROOT / "platforms"))
(view, standalone), = platform_layouts("aethersx2", profile, standalone_set(config))
self.assertTrue(standalone)
for entry in view["files"]:
path = entry.get("path") or ""
if path.startswith("bios/"):
self.assertEqual(entry["standalone_path"], "ps2/" + path.removeprefix("bios/"))
elif path:
self.assertEqual(entry.get("unplaced"), "outside_bios_folder", path)
class Quasi88OnRetroPie(unittest.TestCase):
"""RetroPie writes -romdir $biosdir/pc88 into quasi88.ini (quasi88.sh:67)."""
def test_the_roms_land_in_pc88(self) -> None:
profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["quasi88kai"]
config = load_platform_config("retropie", str(REPO_ROOT / "platforms"))
(view, standalone), = platform_layouts("quasi88kai", profile, standalone_set(config))
self.assertTrue(standalone)
paths = [e["path"] for e in view["files"]]
self.assertTrue(paths and all(p.startswith("rom/") for p in paths))
self.assertEqual(
[e["standalone_path"] for e in view["files"]],
["pc88/" + p.removeprefix("rom/") for p in paths],
)
if __name__ == "__main__":
unittest.main()