mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-04-13 12:22:33 -05:00
fix: rewrite emudeck exporter to match exact checkBIOS.sh format
This commit is contained in:
@@ -1,7 +1,12 @@
|
|||||||
"""Exporter for EmuDeck checkBIOS.sh format.
|
"""Exporter for EmuDeck checkBIOS.sh format.
|
||||||
|
|
||||||
Produces a bash script compatible with EmuDeck's checkBIOS.sh,
|
Produces a bash script matching the exact pattern of EmuDeck's
|
||||||
containing MD5 hash arrays and per-system check functions.
|
functions/checkBIOS.sh: per-system check functions with MD5 arrays
|
||||||
|
inside the function body, iterating over $biosPath/* files.
|
||||||
|
|
||||||
|
Two patterns:
|
||||||
|
- MD5 pattern: systems with known hashes, loop $biosPath/*, md5sum each, match
|
||||||
|
- File-exists pattern: systems with specific paths, check -f
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -11,47 +16,112 @@ from pathlib import Path
|
|||||||
|
|
||||||
from .base_exporter import BaseExporter
|
from .base_exporter import BaseExporter
|
||||||
|
|
||||||
# System slug -> (bash array name, check function name)
|
# Map our system IDs to EmuDeck function naming conventions
|
||||||
_SYSTEM_BASH_MAP: dict[str, tuple[str, str]] = {
|
_SYSTEM_CONFIG: dict[str, dict] = {
|
||||||
"sony-playstation": ("PSBios", "checkPS1BIOS"),
|
"sony-playstation": {
|
||||||
"sony-playstation-2": ("PS2Bios", "checkPS2BIOS"),
|
"func": "checkPS1BIOS",
|
||||||
"sega-mega-cd": ("CDBios", "checkSegaCDBios"),
|
"var": "PSXBIOS",
|
||||||
"sega-saturn": ("SaturnBios", "checkSaturnBios"),
|
"array": "PSBios",
|
||||||
"sega-dreamcast": ("DCBios", "checkDreamcastBios"),
|
"pattern": "md5",
|
||||||
"nintendo-ds": ("DSBios", "checkDSBios"),
|
},
|
||||||
|
"sony-playstation-2": {
|
||||||
|
"func": "checkPS2BIOS",
|
||||||
|
"var": "PS2BIOS",
|
||||||
|
"array": "PS2Bios",
|
||||||
|
"pattern": "md5",
|
||||||
|
},
|
||||||
|
"sega-mega-cd": {
|
||||||
|
"func": "checkSegaCDBios",
|
||||||
|
"var": "SEGACDBIOS",
|
||||||
|
"array": "CDBios",
|
||||||
|
"pattern": "md5",
|
||||||
|
},
|
||||||
|
"sega-saturn": {
|
||||||
|
"func": "checkSaturnBios",
|
||||||
|
"var": "SATURNBIOS",
|
||||||
|
"array": "SaturnBios",
|
||||||
|
"pattern": "md5",
|
||||||
|
},
|
||||||
|
"sega-dreamcast": {
|
||||||
|
"func": "checkDreamcastBios",
|
||||||
|
"var": "BIOS",
|
||||||
|
"array": "hashes",
|
||||||
|
"pattern": "md5",
|
||||||
|
},
|
||||||
|
"nintendo-ds": {
|
||||||
|
"func": "checkDSBios",
|
||||||
|
"var": "BIOS",
|
||||||
|
"array": "hashes",
|
||||||
|
"pattern": "md5",
|
||||||
|
},
|
||||||
|
"nintendo-switch": {
|
||||||
|
"func": "checkCitronBios",
|
||||||
|
"pattern": "file-exists",
|
||||||
|
"firmware_path": "$biosPath/citron/firmware",
|
||||||
|
"keys_path": "$biosPath/citron/keys/prod.keys",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _slug_to_bash_name(slug: str) -> str:
|
def _make_md5_function(cfg: dict, md5s: list[str]) -> list[str]:
|
||||||
"""Convert a system slug to a CamelCase bash identifier."""
|
"""Generate a MD5-checking function matching EmuDeck's exact pattern."""
|
||||||
parts = slug.split("-")
|
func = cfg["func"]
|
||||||
return "".join(p.capitalize() for p in parts) + "Bios"
|
var = cfg["var"]
|
||||||
|
array = cfg["array"]
|
||||||
|
md5_str = " ".join(md5s)
|
||||||
|
|
||||||
|
return [
|
||||||
|
f"{func}(){{",
|
||||||
|
"",
|
||||||
|
f'\t{var}="NULL"',
|
||||||
|
"",
|
||||||
|
'\tfor entry in "$biosPath/"*',
|
||||||
|
"\tdo",
|
||||||
|
'\t\tif [ -f "$entry" ]; then',
|
||||||
|
'\t\t\tmd5=($(md5sum "$entry"))',
|
||||||
|
f'\t\t\tif [[ "${var}" != true ]]; then',
|
||||||
|
f"\t\t\t\t{array}=({md5_str})",
|
||||||
|
f'\t\t\t\tfor i in "${{{array}[@]}}"',
|
||||||
|
"\t\t\t\tdo",
|
||||||
|
'\t\t\t\tif [[ "$md5" == *"${i}"* ]]; then',
|
||||||
|
f"\t\t\t\t\t{var}=true",
|
||||||
|
"\t\t\t\t\tbreak",
|
||||||
|
"\t\t\t\telse",
|
||||||
|
f"\t\t\t\t\t{var}=false",
|
||||||
|
"\t\t\t\tfi",
|
||||||
|
"\t\t\t\tdone",
|
||||||
|
"\t\t\tfi",
|
||||||
|
"\t\tfi",
|
||||||
|
"\tdone",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
f"\tif [ ${var} == true ]; then",
|
||||||
|
'\t\techo "$entry true";',
|
||||||
|
"\telse",
|
||||||
|
'\t\techo "false";',
|
||||||
|
"\tfi",
|
||||||
|
"}",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _slug_to_func_name(slug: str) -> str:
|
def _make_file_exists_function(cfg: dict) -> list[str]:
|
||||||
"""Convert a system slug to a check function name."""
|
"""Generate a file-exists function matching EmuDeck's pattern."""
|
||||||
parts = slug.split("-")
|
func = cfg["func"]
|
||||||
return "check" + "".join(p.capitalize() for p in parts) + "Bios"
|
firmware = cfg.get("firmware_path", "")
|
||||||
|
keys = cfg.get("keys_path", "")
|
||||||
|
|
||||||
|
return [
|
||||||
def _collect_md5s(files: list[dict]) -> list[str]:
|
f"{func}(){{",
|
||||||
"""Extract unique MD5 hashes from file entries."""
|
"",
|
||||||
hashes: list[str] = []
|
f'\tlocal FIRMWARE="{firmware}"',
|
||||||
seen: set[str] = set()
|
f'\tlocal KEYS="{keys}"',
|
||||||
for fe in files:
|
'\tif [[ -f "$KEYS" ]] && [[ "$( ls -A "$FIRMWARE")" ]]; then',
|
||||||
md5 = fe.get("md5", "")
|
'\t\t\techo "true";',
|
||||||
if isinstance(md5, list):
|
"\telse",
|
||||||
for h in md5:
|
'\t\t\techo "false";',
|
||||||
h_lower = h.lower()
|
"\tfi",
|
||||||
if h_lower and h_lower not in seen:
|
"}",
|
||||||
seen.add(h_lower)
|
]
|
||||||
hashes.append(h_lower)
|
|
||||||
elif md5:
|
|
||||||
h_lower = md5.lower()
|
|
||||||
if h_lower not in seen:
|
|
||||||
seen.add(h_lower)
|
|
||||||
hashes.append(h_lower)
|
|
||||||
return hashes
|
|
||||||
|
|
||||||
|
|
||||||
class Exporter(BaseExporter):
|
class Exporter(BaseExporter):
|
||||||
@@ -67,135 +137,71 @@ class Exporter(BaseExporter):
|
|||||||
output_path: str,
|
output_path: str,
|
||||||
scraped_data: dict | None = None,
|
scraped_data: dict | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
lines: list[str] = ["#!/bin/bash"]
|
||||||
|
|
||||||
systems = truth_data.get("systems", {})
|
systems = truth_data.get("systems", {})
|
||||||
|
|
||||||
# Collect per-system hash arrays and file lists
|
for sys_id, cfg in sorted(_SYSTEM_CONFIG.items(), key=lambda x: x[1]["func"]):
|
||||||
sys_hashes: dict[str, list[str]] = {}
|
sys_data = systems.get(sys_id)
|
||||||
sys_files: dict[str, list[dict]] = {}
|
if not sys_data:
|
||||||
for sys_id in sorted(systems):
|
|
||||||
files = systems[sys_id].get("files", [])
|
|
||||||
valid_files = [
|
|
||||||
f for f in files
|
|
||||||
if not f.get("name", "").startswith("_")
|
|
||||||
and not self._is_pattern(f.get("name", ""))
|
|
||||||
]
|
|
||||||
if not valid_files:
|
|
||||||
continue
|
continue
|
||||||
sys_files[sys_id] = valid_files
|
|
||||||
sys_hashes[sys_id] = _collect_md5s(valid_files)
|
|
||||||
|
|
||||||
lines: list[str] = [
|
|
||||||
"#!/bin/bash",
|
|
||||||
"# EmuDeck BIOS check script",
|
|
||||||
"# Generated from retrobios truth data",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Emit hash arrays for systems that have MD5s
|
|
||||||
for sys_id in sorted(sys_hashes):
|
|
||||||
hashes = sys_hashes[sys_id]
|
|
||||||
if not hashes:
|
|
||||||
continue
|
|
||||||
array_name, _ = _SYSTEM_BASH_MAP.get(
|
|
||||||
sys_id, (_slug_to_bash_name(sys_id), ""),
|
|
||||||
)
|
|
||||||
lines.append(f"{array_name}=({' '.join(hashes)})")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
# Emit check functions
|
|
||||||
for sys_id in sorted(sys_files):
|
|
||||||
hashes = sys_hashes.get(sys_id, [])
|
|
||||||
_, func_name = _SYSTEM_BASH_MAP.get(
|
|
||||||
sys_id, ("", _slug_to_func_name(sys_id)),
|
|
||||||
)
|
|
||||||
|
|
||||||
lines.append(f"{func_name}(){{")
|
|
||||||
|
|
||||||
if hashes:
|
|
||||||
array_name, _ = _SYSTEM_BASH_MAP.get(
|
|
||||||
sys_id, (_slug_to_bash_name(sys_id), ""),
|
|
||||||
)
|
|
||||||
lines.append(' localRONE="NULL"')
|
|
||||||
lines.append(' for entry in "$biosPath/"*')
|
|
||||||
lines.append(" do")
|
|
||||||
lines.append(' if [ -f "$entry" ]; then')
|
|
||||||
lines.append(' md5=($(md5sum "$entry"))')
|
|
||||||
lines.append(
|
|
||||||
f' for hash in "${{{array_name}[@]}}"; do',
|
|
||||||
)
|
|
||||||
lines.append(
|
|
||||||
' if [[ "$md5" == *"${hash}"* ]]; then',
|
|
||||||
)
|
|
||||||
lines.append(' RONE=true')
|
|
||||||
lines.append(" fi")
|
|
||||||
lines.append(" done")
|
|
||||||
lines.append(" fi")
|
|
||||||
lines.append(" done")
|
|
||||||
lines.append(' if [ $RONE == true ]; then')
|
|
||||||
lines.append(' echo "true"')
|
|
||||||
lines.append(" else")
|
|
||||||
lines.append(' echo "false"')
|
|
||||||
lines.append(" fi")
|
|
||||||
else:
|
|
||||||
# No MD5 hashes — check file existence
|
|
||||||
for fe in sys_files[sys_id]:
|
|
||||||
dest = self._dest(fe)
|
|
||||||
if dest:
|
|
||||||
lines.append(
|
|
||||||
f' if [ -f "$biosPath/{dest}" ]; then',
|
|
||||||
)
|
|
||||||
lines.append(' echo "true"')
|
|
||||||
lines.append(" return")
|
|
||||||
lines.append(" fi")
|
|
||||||
lines.append(' echo "false"')
|
|
||||||
|
|
||||||
lines.append("}")
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Emit setBIOSstatus aggregator
|
if cfg["pattern"] == "md5":
|
||||||
lines.append("setBIOSstatus(){")
|
md5s: list[str] = []
|
||||||
for sys_id in sorted(sys_files):
|
for fe in sys_data.get("files", []):
|
||||||
_, func_name = _SYSTEM_BASH_MAP.get(
|
name = fe.get("name", "")
|
||||||
sys_id, ("", _slug_to_func_name(sys_id)),
|
if self._is_pattern(name) or name.startswith("_"):
|
||||||
)
|
continue
|
||||||
var = re.sub(r"^check", "", func_name)
|
md5 = fe.get("md5", "")
|
||||||
var = re.sub(r"Bios$", "BIOS", var)
|
if isinstance(md5, list):
|
||||||
var = re.sub(r"BIOS$", "_bios", var)
|
md5s.extend(m for m in md5 if m and re.fullmatch(r"[a-f0-9]{32}", m))
|
||||||
lines.append(f" {var}=$({func_name})")
|
elif md5 and re.fullmatch(r"[a-f0-9]{32}", md5):
|
||||||
lines.append("}")
|
md5s.append(md5)
|
||||||
lines.append("")
|
if md5s:
|
||||||
|
lines.extend(_make_md5_function(cfg, md5s))
|
||||||
|
elif cfg["pattern"] == "file-exists":
|
||||||
|
lines.extend(_make_file_exists_function(cfg))
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
Path(output_path).write_text("\n".join(lines), encoding="utf-8")
|
Path(output_path).write_text("\n".join(lines), encoding="utf-8")
|
||||||
|
|
||||||
def validate(self, truth_data: dict, output_path: str) -> list[str]:
|
def validate(self, truth_data: dict, output_path: str) -> list[str]:
|
||||||
content = Path(output_path).read_text(encoding="utf-8")
|
content = Path(output_path).read_text(encoding="utf-8")
|
||||||
issues: list[str] = []
|
issues: list[str] = []
|
||||||
|
|
||||||
for sys_id, sys_data in truth_data.get("systems", {}).items():
|
systems = truth_data.get("systems", {})
|
||||||
files = sys_data.get("files", [])
|
for sys_id, cfg in _SYSTEM_CONFIG.items():
|
||||||
valid_files = [
|
if cfg["pattern"] != "md5":
|
||||||
f for f in files
|
|
||||||
if not f.get("name", "").startswith("_")
|
|
||||||
and not self._is_pattern(f.get("name", ""))
|
|
||||||
]
|
|
||||||
if not valid_files:
|
|
||||||
continue
|
continue
|
||||||
|
sys_data = systems.get(sys_id)
|
||||||
# Check that MD5 hashes appear in the output
|
if not sys_data:
|
||||||
for fe in valid_files:
|
continue
|
||||||
|
for fe in sys_data.get("files", []):
|
||||||
md5 = fe.get("md5", "")
|
md5 = fe.get("md5", "")
|
||||||
if isinstance(md5, list):
|
if isinstance(md5, list):
|
||||||
for h in md5:
|
md5 = md5[0] if md5 else ""
|
||||||
if h and h.lower() not in content:
|
if md5 and re.fullmatch(r"[a-f0-9]{32}", md5) and md5 not in content:
|
||||||
issues.append(f"missing hash: {h} ({sys_id})")
|
issues.append(f"missing md5: {md5} ({fe.get('name', '')})")
|
||||||
elif md5 and md5.lower() not in content:
|
|
||||||
issues.append(f"missing hash: {md5} ({sys_id})")
|
|
||||||
|
|
||||||
# Check that a check function exists for this system
|
for sys_id, cfg in _SYSTEM_CONFIG.items():
|
||||||
_, func_name = _SYSTEM_BASH_MAP.get(
|
func = cfg["func"]
|
||||||
sys_id, ("", _slug_to_func_name(sys_id)),
|
if func in content:
|
||||||
)
|
continue
|
||||||
if func_name not in content:
|
sys_data = systems.get(sys_id)
|
||||||
issues.append(f"missing function: {func_name} ({sys_id})")
|
if not sys_data or not sys_data.get("files"):
|
||||||
|
continue
|
||||||
|
# Only flag if the system has usable data for the function type
|
||||||
|
if cfg["pattern"] == "md5":
|
||||||
|
has_md5 = any(
|
||||||
|
fe.get("md5") and isinstance(fe.get("md5"), str)
|
||||||
|
and re.fullmatch(r"[a-f0-9]{32}", fe["md5"])
|
||||||
|
for fe in sys_data["files"]
|
||||||
|
)
|
||||||
|
if has_md5:
|
||||||
|
issues.append(f"missing function: {func}")
|
||||||
|
elif cfg["pattern"] == "file-exists":
|
||||||
|
issues.append(f"missing function: {func}")
|
||||||
|
|
||||||
return issues
|
return issues
|
||||||
|
|||||||
Reference in New Issue
Block a user