mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-06 10:03:49 -06:00
feat: pack readme names spellings left out
A pack folded for Windows and macOS keeps one spelling of each case twin. Extracted on a Steam Deck or Linux, the core asking for the other spelling finds nothing, and only the installer added it. Where the platform also runs on a filesystem that keeps case, the README now lists each spelling left out beside the one it holds: 12 for EmuDeck and 10 for RetroArch, the manifest's case_variants.
This commit is contained in:
1 parent
4efcb3da64
commit
55e2c8328e
4 files changed
+157
-3
No files matched your search
@@ -608,6 +608,17 @@ def _pack_data_directories(
|
|||||||
return added
|
return added
|
||||||
|
|
||||||
|
|
||||||
|
def _case_twin_pairs(
|
||||||
|
twins: list[str], seen_destinations: set[str], base_dest: str, flatten: bool,
|
||||||
|
) -> list[tuple[str, str]]:
|
||||||
|
"""Each spelling left out, with the spelling the pack holds."""
|
||||||
|
held = {
|
||||||
|
_flat(dest, base_dest, flatten).lower(): _flat(dest, base_dest, flatten)
|
||||||
|
for dest in seen_destinations
|
||||||
|
}
|
||||||
|
return sorted({(twin, held[twin.lower()]) for twin in twins if twin.lower() in held})
|
||||||
|
|
||||||
|
|
||||||
def _report_pack_counts(
|
def _report_pack_counts(
|
||||||
zip_path: str,
|
zip_path: str,
|
||||||
file_status: dict,
|
file_status: dict,
|
||||||
@@ -879,6 +890,8 @@ def generate_pack(
|
|||||||
user_provided = []
|
user_provided = []
|
||||||
seen_destinations: set[str] = set()
|
seen_destinations: set[str] = set()
|
||||||
seen_lower: set[str] = set() # only used when case_insensitive=True
|
seen_lower: set[str] = set() # only used when case_insensitive=True
|
||||||
|
# Spellings left out as case twins of a packed file, for the README.
|
||||||
|
case_twins: list[str] = []
|
||||||
seen_parents: set[str] = (
|
seen_parents: set[str] = (
|
||||||
set()
|
set()
|
||||||
) # parent dirs of added files (path conflict detection)
|
) # parent dirs of added files (path conflict detection)
|
||||||
@@ -1185,6 +1198,8 @@ def generate_pack(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if already_packed:
|
if already_packed:
|
||||||
|
if dedup_key not in seen_destinations:
|
||||||
|
case_twins.append(_flat(full_dest, base_dest, flatten))
|
||||||
continue
|
continue
|
||||||
if within is not None and not _same_bytes(
|
if within is not None and not _same_bytes(
|
||||||
local_path, within.get(_flat(full_dest, base_dest, flatten))
|
local_path, within.get(_flat(full_dest, base_dest, flatten))
|
||||||
@@ -1267,10 +1282,13 @@ def generate_pack(
|
|||||||
# Taken, a case twin on a case-insensitive filesystem, or a
|
# Taken, a case twin on a case-insensitive filesystem, or a
|
||||||
# file/directory conflict (SGB1.sfc vs SGB1.sfc/): verify replays
|
# file/directory conflict (SGB1.sfc vs SGB1.sfc/): verify replays
|
||||||
# the same rule (packextras.hidden_extras).
|
# the same rule (packextras.hidden_extras).
|
||||||
if placement_block(
|
block = placement_block(
|
||||||
full_dest, seen_destinations, seen_lower, seen_parents,
|
full_dest, seen_destinations, seen_lower, seen_parents,
|
||||||
case_insensitive,
|
case_insensitive,
|
||||||
):
|
)
|
||||||
|
if block:
|
||||||
|
if block == "case_twin" and extra_resolver.resolve(fe, dest)[0]:
|
||||||
|
case_twins.append(_flat(full_dest, base_dest, flatten))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# The emulator's own check picks the copy, as for a declared file
|
# The emulator's own check picks the copy, as for a declared file
|
||||||
@@ -1364,6 +1382,37 @@ def generate_pack(
|
|||||||
if _registry_path.exists():
|
if _registry_path.exists():
|
||||||
with open(_registry_path) as _rf:
|
with open(_registry_path) as _rf:
|
||||||
_pack_registry = (yaml_load(_rf) or {}).get("platforms", {})
|
_pack_registry = (yaml_load(_rf) or {}).get("platforms", {})
|
||||||
|
keeps_case = case_insensitive and any(
|
||||||
|
d.get("os") not in CASE_FOLDING_OS
|
||||||
|
for d in (_pack_registry.get(platform_name, {}).get("install", {}) or {})
|
||||||
|
.get("detect", [])
|
||||||
|
)
|
||||||
|
if keeps_case and source != "platform" and precomputed_extras is None:
|
||||||
|
# The spellings a folded platform counts as declared never reach
|
||||||
|
# the extras: collected as the filesystem that keeps case sees
|
||||||
|
# them, as the manifest's case_variants are (Mesen-CE's bios.col
|
||||||
|
# beside the declared BIOS.col).
|
||||||
|
unfolded = _collect_emulator_extras(
|
||||||
|
{**config, "case_insensitive_fs": False}, emulators_dir, db, set(),
|
||||||
|
base_dest, emu_profiles, target_cores=target_cores,
|
||||||
|
include_all=(source == "truth"),
|
||||||
|
)
|
||||||
|
if system_filter:
|
||||||
|
unfolded = _extras_for_systems(unfolded, system_filter)
|
||||||
|
prefix = _detect_extras_prefix(config, base_dest)
|
||||||
|
for fe in unfolded:
|
||||||
|
if required_only and fe.get("required") is False:
|
||||||
|
continue
|
||||||
|
if region_drops and extra_region_key(fe, declared_root(config)) in region_drops:
|
||||||
|
continue
|
||||||
|
full = extra_full_destination(fe, prefix)
|
||||||
|
if (
|
||||||
|
full and full not in seen_destinations and full.lower() in seen_lower
|
||||||
|
and extra_resolver.resolve(
|
||||||
|
fe, sanitize_pack_path(fe.get("destination", fe["name"]))
|
||||||
|
)[0]
|
||||||
|
):
|
||||||
|
case_twins.append(_flat(full, base_dest, flatten))
|
||||||
readme_text = _build_readme(
|
readme_text = _build_readme(
|
||||||
platform_name, platform_display, base_dest, total_files, num_systems,
|
platform_name, platform_display, base_dest, total_files, num_systems,
|
||||||
source=source,
|
source=source,
|
||||||
@@ -1389,6 +1438,10 @@ def generate_pack(
|
|||||||
) if save_count else ""
|
) if save_count else ""
|
||||||
),
|
),
|
||||||
save_by_system=bool(save_count and save_layout(config).by_system),
|
save_by_system=bool(save_count and save_layout(config).by_system),
|
||||||
|
case_twins=(
|
||||||
|
_case_twin_pairs(case_twins, seen_destinations, base_dest, flatten)
|
||||||
|
if keeps_case else []
|
||||||
|
),
|
||||||
)
|
)
|
||||||
_write_generated_member(zf, "README.txt", readme_text)
|
_write_generated_member(zf, "README.txt", readme_text)
|
||||||
|
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ def _build_readme(
|
|||||||
save_folder: str = "",
|
save_folder: str = "",
|
||||||
save_where: str = "",
|
save_where: str = "",
|
||||||
save_by_system: bool = False,
|
save_by_system: bool = False,
|
||||||
|
case_twins: list[tuple[str, str]] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Build a personalized step-by-step README for each platform pack.
|
"""Build a personalized step-by-step README for each platform pack.
|
||||||
|
|
||||||
@@ -216,6 +217,10 @@ def _build_readme(
|
|||||||
frontend's save directory, empty when the pack holds none, and
|
frontend's save directory, empty when the pack holds none, and
|
||||||
``save_where`` that directory's default place (save_place).
|
``save_where`` that directory's default place (save_place).
|
||||||
``save_by_system`` is set when the launcher hands each system a folder.
|
``save_by_system`` is set when the launcher hands each system a folder.
|
||||||
|
``case_twins`` pairs each spelling the pack leaves out, a case twin of a
|
||||||
|
packed file, with the one it holds: given for a platform that also runs
|
||||||
|
where names keep their case (EmuDeck and RetroArch on Linux), where the
|
||||||
|
core asking for the other spelling finds nothing.
|
||||||
"""
|
"""
|
||||||
narrowings = narrowings or []
|
narrowings = narrowings or []
|
||||||
sep = "=" * 50
|
sep = "=" * 50
|
||||||
@@ -245,6 +250,20 @@ def _build_readme(
|
|||||||
+ (pack_manual(platform_name) or _generic_manual(bios_paths))
|
+ (pack_manual(platform_name) or _generic_manual(bios_paths))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if case_twins:
|
||||||
|
listed = "".join(
|
||||||
|
f" {missing} (the pack has {held})\n" for missing, held in case_twins
|
||||||
|
)
|
||||||
|
guide += (
|
||||||
|
"NAMES THAT DIFFER ONLY BY CASE\n\n"
|
||||||
|
" Some cores open the same file under another spelling. This\n"
|
||||||
|
" pack holds one spelling of each, all Windows and macOS can\n"
|
||||||
|
" store in one folder. Linux, SteamOS and Android keep both:\n"
|
||||||
|
" there, run the installer, which adds these spellings, or\n"
|
||||||
|
" copy each file under the name below.\n\n"
|
||||||
|
f"{listed}\n"
|
||||||
|
)
|
||||||
|
|
||||||
if save_folder:
|
if save_folder:
|
||||||
guide += _save_section(
|
guide += _save_section(
|
||||||
platform_display, save_folder, save_where, save_by_system,
|
platform_display, save_folder, save_where, save_by_system,
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ went red. The flag lived in retrobat.yml and vanished at the next scrape.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import hashlib
|
||||||
|
import io
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -29,5 +34,82 @@ class CaseInsensitiveWhereTheFilesystemIs(unittest.TestCase):
|
|||||||
self.assertTrue(config.get("case_insensitive_fs"))
|
self.assertTrue(config.get("case_insensitive_fs"))
|
||||||
|
|
||||||
|
|
||||||
|
class ThePackReadmeNamesTheSpellingsItLeavesOut(unittest.TestCase):
|
||||||
|
"""The EmuDeck pack is folded for Windows and extracted on a Steam Deck,
|
||||||
|
where the core asking for bios.col finds only BIOS.col. Only the
|
||||||
|
installer adds that spelling (case_variants), and the pack's README said
|
||||||
|
nothing about it."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
import common
|
||||||
|
import generate_db
|
||||||
|
import generate_pack
|
||||||
|
|
||||||
|
self.gp = generate_pack
|
||||||
|
tmp = tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp")
|
||||||
|
self.addCleanup(tmp.cleanup)
|
||||||
|
previous = os.getcwd()
|
||||||
|
os.chdir(tmp.name)
|
||||||
|
self.addCleanup(os.chdir, previous)
|
||||||
|
Path("bios/Coleco").mkdir(parents=True)
|
||||||
|
payload = b"coleco bios"
|
||||||
|
Path("bios/Coleco/BIOS.col").write_bytes(payload)
|
||||||
|
sha1 = hashlib.sha1(payload).hexdigest()
|
||||||
|
files = {sha1: {"path": "bios/Coleco/BIOS.col", "name": "BIOS.col",
|
||||||
|
"size": len(payload), "sha1": sha1,
|
||||||
|
"md5": hashlib.md5(payload).hexdigest(),
|
||||||
|
"sha256": hashlib.sha256(payload).hexdigest(),
|
||||||
|
"crc32": "00000001"}}
|
||||||
|
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
|
||||||
|
Path("platforms").mkdir()
|
||||||
|
Path("emulators").mkdir()
|
||||||
|
self.config = {
|
||||||
|
"platform": "P", "verification_mode": "existence",
|
||||||
|
"case_insensitive_fs": True, "cores": ["mesence"],
|
||||||
|
"systems": {"coleco": {"files": [{"name": "BIOS.col"}]}},
|
||||||
|
}
|
||||||
|
Path("emulators/mesence.yml").write_text(yaml.dump({
|
||||||
|
"emulator": "mesence", "type": "libretro", "systems": ["coleco"],
|
||||||
|
"files": [{"name": "bios.col", "required": True}],
|
||||||
|
}))
|
||||||
|
common._platform_config_cache.clear()
|
||||||
|
common._emulator_profiles_cache.clear()
|
||||||
|
self.addCleanup(common._platform_config_cache.clear)
|
||||||
|
self.addCleanup(common._emulator_profiles_cache.clear)
|
||||||
|
|
||||||
|
def _readme(self, systems: list[str]) -> str:
|
||||||
|
import common
|
||||||
|
|
||||||
|
Path("platforms/p.yml").write_text(yaml.dump(self.config))
|
||||||
|
Path("platforms/_registry.yml").write_text(yaml.dump({"platforms": {"p": {
|
||||||
|
"status": "active",
|
||||||
|
"install": {"detect": [{"os": os_name} for os_name in systems]},
|
||||||
|
}}}))
|
||||||
|
common._platform_config_cache.clear()
|
||||||
|
texts: dict[str, str] = {}
|
||||||
|
real = self.gp._write_generated_member
|
||||||
|
|
||||||
|
def capture(zf, arcname, text):
|
||||||
|
texts[arcname] = text
|
||||||
|
return real(zf, arcname, text)
|
||||||
|
|
||||||
|
self.gp._write_generated_member = capture
|
||||||
|
self.addCleanup(setattr, self.gp, "_write_generated_member", real)
|
||||||
|
with contextlib.redirect_stdout(io.StringIO()):
|
||||||
|
self.gp.generate_pack(
|
||||||
|
"p", "platforms", self.db, "bios", "", emulators_dir="emulators",
|
||||||
|
offline=True, record=self.gp.PackRecord(),
|
||||||
|
)
|
||||||
|
return texts["README.txt"]
|
||||||
|
|
||||||
|
def test_a_platform_also_on_linux_names_them(self):
|
||||||
|
readme = self._readme(["windows", "linux"])
|
||||||
|
self.assertIn("NAMES THAT DIFFER ONLY BY CASE", readme)
|
||||||
|
self.assertIn(" bios.col (the pack has BIOS.col)", readme)
|
||||||
|
|
||||||
|
def test_a_platform_only_on_windows_does_not(self):
|
||||||
|
self.assertNotIn("DIFFER ONLY BY CASE", self._readme(["windows"]))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
@@ -405,7 +405,7 @@ resolution.
|
|||||||
| MAME BIOS ZIPs | `contents` field documents inner structure |
|
| MAME BIOS ZIPs | `contents` field documents inner structure |
|
||||||
| Trees addressed by path (RPG Maker, ScummVM, tzdata, c64-emu and the rest of `NODEDUP_DIRS`) | excluded from dedup to keep files that are equal by design |
|
| Trees addressed by path (RPG Maker, ScummVM, tzdata, c64-emu and the rest of `NODEDUP_DIRS`) | excluded from dedup to keep files that are equal by design |
|
||||||
| `strip_components` in data dirs | flattens cache prefix to match expected path |
|
| `strip_components` in data dirs | flattens cache prefix to match expected path |
|
||||||
| case-insensitive dedup | prevents `font.rom` + `FONT.ROM` conflicts on Windows/macOS |
|
| case-insensitive dedup | prevents `font.rom` + `FONT.ROM` conflicts on Windows/macOS. Where the platform also runs on a filesystem that keeps case (EmuDeck and RetroArch on Linux), the pack README lists each spelling it leaves out, and the installer adds them (`case_variants`) |
|
||||||
| frozen snapshot cores | `.info` may reflect current version while code is pinned to an old one. Only the frozen source at the pinned tag is reliable (e.g. desmume2015, mame2003) |
|
| frozen snapshot cores | `.info` may reflect current version while code is pinned to an old one. Only the frozen source at the pinned tag is reliable (e.g. desmume2015, mame2003) |
|
||||||
|
|
||||||
### File resolution chain
|
### File resolution chain
|
||||||
|
|||||||
Reference in new issue
Block a user