feat: record pcsx2's relative bios folder

configureINI writes [Folders] Bios = ../../../bios/ps2 into
inis/PCSX2.ini, which PCSX2 and AetherSX2 resolve against the config
folder configgen hands it. relative_folders reads the call, the ini
path and the value, and records the resolved /userdata/bios/ps2 as a
passes_path for pcsx2 and aethersx2.
This commit is contained in:
Abdessamad Derraz committed 2026-11-09 17:59:09 +01:00
1 parent cedeee2254
commit f0289e3624
3 files changed
+146 -1

No files matched your search

+18
View File
@@ -5732,6 +5732,24 @@ launchers:
depends_on_option: neocd_bios
source_ref: "package/system/knulli-configgen/configgen/configgen/generators/libretro/libretroOptions.py:2558-2559,\
\ 2566-2569"
- emulator: pcsx2
build: standalone
kind: passes_path
names: []
search_dir: /userdata/bios/ps2
passes_to: "inis/PCSX2.ini [Folders] Bios = ../../../bios/ps2, relative to /userdata/system/configs/PCSX2"
fails_when_missing: false
source_ref: "package/system/knulli-configgen/configgen/configgen/generators/pcsx2/pcsx2Generator.py:92,\
\ 176, 213"
- emulator: aethersx2
build: standalone
kind: passes_path
names: []
search_dir: /userdata/bios/ps2
passes_to: "inis/PCSX2.ini [Folders] Bios = ../../../bios/ps2, relative to /userdata/system/.config/aethersx2"
fails_when_missing: false
source_ref: "package/system/knulli-configgen/configgen/configgen/generators/aethersx2/aethersx2Generator.py:71,\
\ 182, 219"
- emulator: lexaloffle
build: standalone
kind: moves
+76 -1
View File
@@ -20,6 +20,7 @@ LauncherShapeError, as every other extractor does.
from __future__ import annotations
import ast
import posixpath
import re
from collections.abc import Callable, Iterator
from dataclasses import dataclass, field
@@ -834,8 +835,82 @@ def bios_sites(cg: Configgen, spec: GenericSites) -> list[Launcher]:
return launchers
# Generators that write a [Folders] path relative to the config folder they
# are given. PCSX2 and AetherSX2 resolve those paths against their data root,
# the folder holding inis/, which configureINI receives as config_directory.
RELATIVE_FOLDERS = (
("pcsx2/pcsx2Generator.py", "configureINI"),
("aethersx2/aethersx2Generator.py", "configureINI"),
)
def relative_folders(
cg: Configgen, specs: tuple[tuple[str, str], ...] = RELATIVE_FOLDERS
) -> list[Launcher]:
"""parser.set(section, key, "../<path>") resolved against config_directory."""
launchers = []
for path, function_name in specs:
src = cg.source(f"{cg.generators}/{path}")
reader = _Reader(cg, src)
function = src.function(function_name)
if not function.args.args:
src.fail(f"{function_name} takes no config folder")
folder = function.args.args[0].arg
ini = [
node for node in ast.walk(function)
if isinstance(node, ast.Assign) and isinstance(node.value, ast.BinOp)
and ast.unparse(node.value).startswith(f"{folder} /")
and ast.unparse(node.value).endswith(".ini'")
]
if len(ini) != 1:
src.fail(f"{function_name} builds no one ini path under {folder}")
parts = sorted(
(n for n in ast.walk(ini[0].value)
if isinstance(n, ast.Constant) and isinstance(n.value, str)),
key=lambda n: (n.lineno, n.col_offset),
)
ini_name = posixpath.join(*(str(n.value) for n in parts))
calls = [
node for node in ast.walk(src.tree)
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
and node.func.id == function_name and node.args
]
if len(calls) != 1:
src.fail(f"expected one call of {function_name}, found {len(calls)}")
base = reader.resolve(calls[0].args[0])
if base is None or not base.startswith("/"):
src.fail(f"{function_name} is given a folder that is not a path")
found = 0
for call in ast.walk(function):
if not (
isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute)
and call.func.attr == "set" and len(call.args) == 3
and all(isinstance(a, ast.Constant) for a in call.args)
):
continue
section, key, value = (str(a.value) for a in call.args) # type: ignore[attr-defined]
if not value.startswith("../"):
continue
resolved = posixpath.normpath(posixpath.join(base, value))
if not reader.under_bios(resolved):
continue
found += 1
launchers.append(Launcher(
emulator=cg.emulator(path.removesuffix(".py").replace("/", ".")),
build="standalone", kind="passes_path", names=[], search_dir=resolved,
passes_to=f"{ini_name} [{section}] {key} = {value}, relative to {base}",
fails_when_missing=False,
source_ref=src.ref([
src.span(reader.statement_of(calls[0])), src.span(ini[0]), _lines(call),
]),
))
if not found:
src.fail(f"{function_name} writes no relative folder under {cg.bios}")
return launchers
EXTRACTORS: tuple[Callable[[Configgen], list[Launcher]], ...] = (
*SHARED, lexaloffle, core_file_options,
*SHARED, lexaloffle, core_file_options, relative_folders,
)
+52
View File
@@ -59,6 +59,7 @@ IMPORTER = text(
'openmsx': ('openmsx.openmsxGenerator', 'OpenmsxGenerator'),
'hatari': ('hatari.hatariGenerator', 'HatariGenerator'),
'melonds': ('melonds.melondsGenerator', 'MelonDSGenerator'),
'pcsx2': ('pcsx2.pcsx2Generator', 'Pcsx2Generator'),
}
"""
)
@@ -305,6 +306,29 @@ MELONDS = text(
)
# pcsx2Generator.py: the default patches copied into BIOS when absent, and
# the BIOS folder written relative to the config folder.
PCSX2 = text(
"""
from ...batoceraPaths import BIOS, CONFIGS, DATAINIT_DIR
_PCSX2_CONFIG: Final = CONFIGS / "PCSX2"
_PCSX2_BIOS: Final = BIOS / "ps2"
class Pcsx2Generator(Generator):
def generate(self, system, rom, playersControllers, metadata, guns, wheels, gameResolution):
pcsx2Patches = _PCSX2_BIOS / "patches.zip"
configureINI(_PCSX2_CONFIG, _PCSX2_BIOS, system)
if not pcsx2Patches.exists():
shutil.copy(DATAINIT_DIR / "bios" / "ps2" / "patches.zip", pcsx2Patches)
def configureINI(config_directory, bios_directory, system):
configFileName = config_directory / 'inis' / "PCSX2.ini"
pcsx2INIConfig.set("Folders", "Bios", "../../../bios/ps2")
pcsx2INIConfig.set("Folders", "Logs", "../../logs")
"""
)
def tree(**overrides: str) -> dict[str, str]:
files = {
f"{kl.CONFIGGEN}/batoceraPaths.py": PATHS,
@@ -322,6 +346,7 @@ def tree(**overrides: str) -> dict[str, str]:
f"{G}/libretro/libretroMAMEConfig.py": MAME_CONFIG,
f"{G}/hatari/hatariGenerator.py": HATARI,
f"{G}/melonds/melondsGenerator.py": MELONDS,
f"{G}/pcsx2/pcsx2Generator.py": PCSX2,
}
files.update(overrides)
return files
@@ -498,6 +523,33 @@ class BiosSites(unittest.TestCase):
kl.bios_sites(kl.configgen(files.__getitem__), spec)
class CopiesAndFolders(unittest.TestCase):
def test_a_copy_into_bios_reads_from_the_folder_it_comes_from(self):
(record,) = sites("pcsx2/pcsx2Generator.py", "fixed_name")
self.assertEqual(record["kind"], "copies")
self.assertEqual(record["names"], ["patches.zip"])
self.assertEqual(record["search_dir"], "/usr/share/knulli/datainit/bios/ps2")
self.assertEqual(record["passes_to"], "/userdata/bios/ps2/patches.zip")
self.assertEqual(record["depends_on_option"], "not pcsx2Patches.exists()")
def test_a_relative_folder_resolved_against_the_config_folder(self):
(record,) = kl.relative_folders(configgen(), kl.RELATIVE_FOLDERS[:1])
self.assertEqual(record.kind, "passes_path")
self.assertEqual(record.search_dir, "/userdata/bios/ps2")
self.assertEqual(
record.passes_to,
"inis/PCSX2.ini [Folders] Bios = ../../../bios/ps2, relative to /userdata/system/configs/PCSX2",
)
self.assertEqual(record.source_ref, f"{G}/pcsx2/pcsx2Generator.py:9, 14-15")
def test_a_folder_written_outside_bios_raises(self):
broken = PCSX2.replace("../../../bios/ps2", "../../../saves/ps2")
with self.assertRaises(LauncherShapeError):
kl.relative_folders(
configgen(**{f"{G}/pcsx2/pcsx2Generator.py": broken}), kl.RELATIVE_FOLDERS[:1]
)
class PlatformFile(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None: