chore: merge configs root and switch folders

This commit is contained in:
Abdessamad Derraz committed 2026-11-09 20:09:05 +01:00
commit 24eab97bc7
24 files changed
+826 -203

No files matched your search

+1
View File
@@ -63,6 +63,7 @@ files:
standalone_directories:
- "$XDG_DATA_HOME/eden"
- "$HOME/.local/share/eden"
- "PORTABLE_DIR"
data_root:
windows:
path: "%APPDATA%/eden"
+1
View File
@@ -19,6 +19,7 @@ mode: standalone
standalone_directories:
- "$XDG_CONFIG_HOME/Ryujinx"
- "$HOME/.config/Ryujinx"
- "DefaultPortableDir"
data_root:
windows:
path: "%APPDATA%/Ryujinx"
+273 -1
View File
@@ -39,7 +39,12 @@ notes: |
priority over the global sys_modules/ directory.
System fonts are mounted from fonts/font/ (/preinst/common/font on real
PS4) and fonts/font2/ (/system/common/font2 on real PS4). These are SST
PS4) and fonts/font2/ (/system/common/font2 on real PS4)
(src/emulator.cpp:639-661). The font library opens each by its dumped name
or its PS4 name in fonts/, then fonts/font/, then fonts/font2/
(src/core/libraries/font/font_internal.cpp:391-468), and falls back to the
Noto fonts it embeds (font_internal.cpp:119-127). The entries below put
each in fonts/font/, which both the mount and the library read. These are SST
(Sony Standard Text) typeface files in OTF format. The emulator mounts
the directories wholesale, and games load individual fonts by name. Without
fonts, the emulator warns "No dumped system fonts, expect missing text
@@ -239,3 +244,270 @@ files:
description: "PS4 XML parser"
note: "No HLE implementation. The module is stubbed when absent."
source_ref: "src/core/libraries/sysmodule/sysmodule_internal.cpp:251"
- name: "SST-Light.otf"
aliases: ["SST-EU-ROMAN-L.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SST-Light.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:419, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SST-Roman.otf"
aliases: ["SST-EU-ROMAN.OTF", "SST-EU-ROMAN-R.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SST-Roman.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:420, 422, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SST-Medium.otf"
aliases: ["SST-EU-ROMAN-M.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SST-Medium.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:421, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SST-Italic.otf"
aliases: ["SST-EU-ROMAN-I.OTF", "SST-ITALIC-R.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SST-Italic.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:423, 427, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SST-Bold.otf"
aliases: ["SST-EU-ROMAN-B.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SST-Bold.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:424, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SST-BoldItalic.otf"
aliases: ["SST-EU-ROMAN-BI.OTF", "SST-ITALIC-B.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SST-BoldItalic.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:425, 429, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SST-LightItalic.otf"
aliases: ["SST-ITALIC-L.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SST-LightItalic.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:426, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SST-MediumItalic.otf"
aliases: ["SST-ITALIC-M.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SST-MediumItalic.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:428, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTTypewriter-Roman.otf"
aliases: ["SST-TYPEWRITER-R.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTTypewriter-Roman.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:430, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTTypewriter-Bd.otf"
aliases: ["SST-TYPEWRITER-B.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTTypewriter-Bd.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:431, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTJpPro-Regular.otf"
aliases: ["SST-JPPRO-R.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTJpPro-Regular.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:432, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTJpPro-Bold.otf"
aliases: ["SST-JPPRO-B.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTJpPro-Bold.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:433, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "DFHEI5-SONY.ttf"
aliases: ["SST-CNGB-HEI-R.TTF"]
required: false
hle_fallback: true
path: "fonts/font/DFHEI5-SONY.ttf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:434, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTAribStdB24-Regular.ttf"
aliases: ["SST-ARIB-STD-B24-R.TTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTAribStdB24-Regular.ttf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:435, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTArabic-Roman.otf"
aliases: ["SST-ARABIC-R.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTArabic-Roman.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:436, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTArabic-Light.otf"
aliases: ["SST-ARABIC-L.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTArabic-Light.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:437, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTArabic-Medium.otf"
aliases: ["SST-ARABIC-M.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTArabic-Medium.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:438, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTArabic-Bold.otf"
aliases: ["SST-ARABIC-B.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTArabic-Bold.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:439, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SCEPS4Yoongd-Light.otf"
aliases: ["SCE-EXT-HANGUL-L.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SCEPS4Yoongd-Light.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:440, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SCEPS4Yoongd-Medium.otf"
aliases: ["SCE-EXT-HANGUL-R.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SCEPS4Yoongd-Medium.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:441, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SCEPS4Yoongd-Bold.otf"
aliases: ["SCE-EXT-HANGUL-B.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SCEPS4Yoongd-Bold.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:442, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "e046323ms.ttf"
aliases: ["SSTCC-SERIF-MONO.OTF"]
required: false
hle_fallback: true
path: "fonts/font/e046323ms.ttf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:443, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "e046323ts.ttf"
aliases: ["SSTCC-SERIF.OTF"]
required: false
hle_fallback: true
path: "fonts/font/e046323ts.ttf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:444, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "n023055ms.ttf"
aliases: ["SSTCC-SANSSERIF-MONO.OTF"]
required: false
hle_fallback: true
path: "fonts/font/n023055ms.ttf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:445, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "n023055ts.ttf"
aliases: ["SSTCC-SANSSERIF.OTF"]
required: false
hle_fallback: true
path: "fonts/font/n023055ts.ttf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:446, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "d013013ds.ttf"
aliases: ["SSTCC-CUSUAL.OTF"]
required: false
hle_fallback: true
path: "fonts/font/d013013ds.ttf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:447, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "k006004ds.ttf"
aliases: ["SSTCC-CURSIVE.OTF"]
required: false
hle_fallback: true
path: "fonts/font/k006004ds.ttf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:448, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "c041056ts.ttf"
aliases: ["SSTCC-SMALLCAPITAL.OTF"]
required: false
hle_fallback: true
path: "fonts/font/c041056ts.ttf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:449, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SCE-RDC-R-JPN.otf"
aliases: ["SCE-JP-CATTLEYA-L.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SCE-RDC-R-JPN.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:450, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SCE-RDC-B-JPN.otf"
aliases: ["SCE-JP-CATTLEYA-B.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SCE-RDC-B-JPN.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:451, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTThai-Light.otf"
aliases: ["SST-THAI-L.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTThai-Light.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:452, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTThai-Roman.otf"
aliases: ["SST-THAI-R.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTThai-Roman.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:453, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTThai-Medium.otf"
aliases: ["SST-THAI-M.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTThai-Medium.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:454, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTThai-Bold.otf"
aliases: ["SST-THAI-B.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTThai-Bold.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:455, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTVietnamese-Light.otf"
aliases: ["SST-VIETNAMESE-L.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTVietnamese-Light.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:456, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTVietnamese-Roman.otf"
aliases: ["SST-VIETNAMESE-R.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTVietnamese-Roman.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:457, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTVietnamese-Medium.otf"
aliases: ["SST-VIETNAMESE-M.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTVietnamese-Medium.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:458, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
- name: "SSTVietnamese-Bold.otf"
aliases: ["SST-VIETNAMESE-B.OTF"]
required: false
hle_fallback: true
path: "fonts/font/SSTVietnamese-Bold.otf"
description: "PS4 system font"
source_ref: "src/core/libraries/font/font_internal.cpp:459, src/core/libraries/font/font_internal.cpp:391-415, src/emulator.cpp:639-661"
+1
View File
@@ -59,6 +59,7 @@ files:
standalone_directories:
- "$XDG_DATA_HOME/yuzu"
- "$HOME/.local/share/yuzu"
- "PORTABLE_DIR"
data_root:
windows:
path: "%APPDATA%/yuzu"
+1 -1
View File
@@ -16,7 +16,7 @@
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
# the site's build rewrites this line there (scripts/sitebootstrap.py).
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
$defaultInstallSha256 = "28316d79b6f4f846f8091bc27e59655894aee48905be686b7971554c2fe17dbc"
$defaultInstallSha256 = "9e135ad9a52ec64148c079e2ce32f080c818b2af71c8420f93a76c09be41962e"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+73 -58
View File
@@ -143,6 +143,7 @@ OMISSION_REASONS = {
"no_save_system": "no system runs the core",
"outside_bios_folder": "read outside the BIOS folder",
"no_roms_directory": "no roms folder",
"no_configs_directory": "no configs folder",
"no_emulators_directory": "no emulator folder",
}
# What a file is when it is not firmware, as the manifest's category field
@@ -3528,14 +3529,20 @@ def _roms_relation(value: object, field: str) -> tuple[int, PurePosixPath]:
return ups, PurePosixPath(*names)
def _roms_root(bios_path: Path, from_bios: str) -> Path | None:
"""The roms root of a BIOS folder, on the host's own path type.
# The roots a list places files below by their place relative to the BIOS
# folder: the roms root (ROCKNIX's /storage/roms) and the frontend's
# configs folder (Batocera's /userdata/system/configs).
RELATIVE_ROOTS = ("roms", "configs")
def _relative_root(bios_path: Path, from_bios: str, root: str = "roms") -> Path | None:
"""A relative root of a BIOS folder, on the host's own path type.
The BIOS folder's ancestor at the depth the relation climbs, then the
named folders below it. None when the joined and normalized path is
anything else.
"""
ups, names = _roms_relation(from_bios, "roms_directory.from_bios")
ups, names = _roms_relation(from_bios, f"{root}_directory.from_bios")
if ups > len(bios_path.parents):
return None
ancestor = bios_path if not ups else bios_path.parents[ups - 1]
@@ -3671,8 +3678,9 @@ class SavePlace(NamedTuple):
# BIOS folder -> where the files read below its frontend's save directory go,
# set once the save directory is known (save_directory_for).
_SAVE_DIRS: dict[Path, SavePlace] = {}
# BIOS folder -> the platform's roms folder, set by place_roms_entries.
_ROMS_DIRS: dict[Path, Path] = {}
# (BIOS folder, root) -> the platform's roms or configs folder, set by
# place_relative_entries.
_RELATIVE_DIRS: dict[tuple[Path, str], Path] = {}
# (BIOS folder, emulator key) -> the emulator folder, set by
# place_emulator_entries.
_EMULATOR_DIRS: dict[tuple[Path, str], Path] = {}
@@ -3686,11 +3694,11 @@ def _entry_place(root: Path, entry: dict) -> tuple[Path, object]:
core's own folder where the frontend sorts saves by core. Any other is
relative to the BIOS folder.
"""
if entry.get("root") == "roms":
roms = _ROMS_DIRS.get(root)
if roms is None:
raise ValueError(f"no roms folder known for {entry.get('dest')}")
return roms, entry.get("dest")
if entry.get("root") in RELATIVE_ROOTS:
folder = _RELATIVE_DIRS.get((root, entry["root"]))
if folder is None:
raise ValueError(f"no {entry['root']} folder known for {entry.get('dest')}")
return folder, entry.get("dest")
if entry.get("root") == "emulators":
key, _, rest = str(entry.get("dest")).partition("/")
folder = _EMULATOR_DIRS.get((root, key))
@@ -3731,7 +3739,7 @@ def _entry_key(entry: dict) -> str:
if root == "emulator":
# A path in one emulator's own folder: two emulators read two files.
return f"emulator\0{','.join(entry.get('cores') or [])}\0{dest}"
return f"{root}\0{dest}" if root in ("save", "roms", "emulators") else dest
return f"{root}\0{dest}" if root in ("save", "emulators", *RELATIVE_ROOTS) else dest
def _check_root(entry: dict, dest: str, omitted: bool = False) -> None:
@@ -3739,7 +3747,8 @@ def _check_root(entry: dict, dest: str, omitted: bool = False) -> None:
the emulator's own folder, outside the BIOS tree), and core_folder one
folder name of a save entry."""
root = entry.get("root")
if root not in (None, "save", "roms", "emulators") and not (omitted and root == "emulator"):
if root not in (None, "save", "emulators", *RELATIVE_ROOTS) and not (
omitted and root == "emulator"):
raise ValueError(f"invalid root for {dest}")
if root == "emulators" and len(PurePosixPath(dest).parts) < 2:
raise ValueError(f"an emulators entry names its folder first: {dest}")
@@ -3752,24 +3761,27 @@ def _check_root(entry: dict, dest: str, omitted: bool = False) -> None:
raise ValueError(f"core_folder must be one folder name for {dest}")
def _check_roms_directory(data: dict) -> None:
"""The manifest's roms_directory, which every installable root: roms entry needs."""
layout = data.get("roms_directory")
def _check_relative_directories(data: dict) -> None:
"""The manifest's roms_directory and configs_directory, which every
installable root: roms or root: configs entry needs."""
installable = [
entry for field in ("files", "case_variants")
for entry in data.get(field) or [] if isinstance(entry, dict)
]
if layout is None:
if any(entry.get("root") == "roms" for entry in installable):
raise ValueError("entries below the roms folder and no roms_directory")
return
if (
not isinstance(layout, dict)
or set(layout) != {"bios_folder", "from_bios"}
or len(_safe_relative_path(layout["bios_folder"], "roms_directory.bios_folder").parts) != 1
):
raise ValueError("invalid roms_directory")
_roms_relation(layout["from_bios"], "roms_directory.from_bios")
for root in RELATIVE_ROOTS:
field = f"{root}_directory"
layout = data.get(field)
if layout is None:
if any(entry.get("root") == root for entry in installable):
raise ValueError(f"entries below the {root} folder and no {field}")
continue
if (
not isinstance(layout, dict)
or set(layout) != {"bios_folder", "from_bios"}
or len(_safe_relative_path(layout["bios_folder"], f"{field}.bios_folder").parts) != 1
):
raise ValueError(f"invalid {field}")
_roms_relation(layout["from_bios"], f"{field}.from_bios")
def _check_emulators_directory(data: dict) -> None:
@@ -3969,7 +3981,7 @@ def _check_entry(
# Below a roms or emulator folder a colon has no use: refused outright.
dest = str(_safe_relative_path(
entry.get("dest"), f"{where}.dest",
colons=entry.get("root") not in ("roms", "emulators"),
colons=entry.get("root") not in ("emulators", *RELATIVE_ROOTS),
))
key = _entry_key({**entry, "dest": dest})
if key in seen_destinations:
@@ -4109,7 +4121,7 @@ def _validate_manifest(data: object, plat: str) -> dict:
for index, entry in enumerate(data_dirs):
_check_data_directory(entry, f"data_directories[{index}]")
_check_save_directory(data)
_check_roms_directory(data)
_check_relative_directories(data)
_check_emulators_directory(data)
declared_total_files = data.get("total_files")
@@ -5376,39 +5388,41 @@ def place_save_entries(
return kept, unplaced
def place_roms_entries(
def place_relative_entries(
files: list[dict], bios_path: Path, manifest: dict, display: str, say: bool = True,
) -> tuple[list[dict], list[dict]]:
"""Find the roms folder for the entries a launcher reads below it.
"""Find the roms and configs folders for the entries read below them.
The list gives the roms root relative to the BIOS folder, as the OS's
own source places them (ROCKNIX's /storage/roms holds bios/): a BIOS
folder of the platform's name puts the roms root there. Any other is a
folder this run cannot relate to the roms, and the entries are left out
with the reason.
The list gives each root relative to the BIOS folder, as the OS's own
source places them (ROCKNIX's /storage/roms holds bios/, Batocera's
configs folder is ../system/configs): a BIOS folder of the platform's
name puts the root there. Any other is a folder this run cannot relate
to the root, and the entries are left out with the reason.
"""
below = [entry for entry in files if entry.get("root") == "roms"]
if not below:
return files, []
layout = manifest.get("roms_directory") or {}
roms = (
_roms_root(bios_path, layout["from_bios"])
if layout and bios_path.name == layout.get("bios_folder") else None
)
if roms is not None:
_ROMS_DIRS[bios_path] = roms
return files, []
if say:
print(f" {_plural(len(below), 'file')} read from {display}'s roms folder left out: "
f"{bios_path} is not its {layout.get('bios_folder') or 'BIOS'} folder.")
kept = [entry for entry in files if entry.get("root") != "roms"]
unplaced = [
{"dest": entry["dest"], "name": PurePosixPath(entry["dest"]).name,
"system": "", "systems": list(entry.get("systems") or []), "required": False,
"reason": "no_roms_directory", "cores": entry.get("cores"), "root": "roms"}
for entry in below
]
return kept, unplaced
unplaced: list[dict] = []
for root in RELATIVE_ROOTS:
below = [entry for entry in files if entry.get("root") == root]
if not below:
continue
layout = manifest.get(f"{root}_directory") or {}
folder = (
_relative_root(bios_path, layout["from_bios"], root)
if layout and bios_path.name == layout.get("bios_folder") else None
)
if folder is not None:
_RELATIVE_DIRS[(bios_path, root)] = folder
continue
if say:
print(f" {_plural(len(below), 'file')} read from {display}'s {root} folder left "
f"out: {bios_path} is not its {layout.get('bios_folder') or 'BIOS'} folder.")
files = [entry for entry in files if entry.get("root") != root]
unplaced += [
{"dest": entry["dest"], "name": PurePosixPath(entry["dest"]).name,
"system": "", "systems": list(entry.get("systems") or []), "required": False,
"reason": f"no_{root}_directory", "cores": entry.get("cores"), "root": root}
for entry in below
]
return files, unplaced
def _config_keys(path: Path) -> set[str]:
@@ -16114,7 +16128,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
display = display_name(plat_name, manifest)
files, unplaced = place_save_entries(
files, bios_path, manifest, args.save_dir, display, say=False)
files, beyond = place_roms_entries(files, bios_path, manifest, display, say=not listing)
files, beyond = place_relative_entries(
files, bios_path, manifest, display, say=not listing)
files, elsewhere = place_emulator_entries(
files, bios_path, manifest, display, say=not listing)
unplaced = [*unplaced, *beyond, *elsewhere]
+1 -1
View File
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
# curl or wget stop on a cut transfer before the hash is read: the site's
# build rewrites this line there (scripts/sitebootstrap.py).
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
DEFAULT_INSTALL_SHA256="28316d79b6f4f846f8091bc27e59655894aee48905be686b7971554c2fe17dbc"
DEFAULT_INSTALL_SHA256="9e135ad9a52ec64148c079e2ce32f080c818b2af71c8420f93a76c09be41962e"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 1143586,
"sha256": "28316d79b6f4f846f8091bc27e59655894aee48905be686b7971554c2fe17dbc",
"size": 1144393,
"sha256": "9e135ad9a52ec64148c079e2ce32f080c818b2af71c8420f93a76c09be41962e",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 1197211,
"sha256": "c66dc01820e1610e7e476b417a44fc3c9007886d574aef81bcdb41441f1e1a8c"
"size": 1198018,
"sha256": "4729710b43a017c62bd275ec7651d489d56e5646cae6e87c73717e13413c6b5d"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 1143586,
"sha256": "28316d79b6f4f846f8091bc27e59655894aee48905be686b7971554c2fe17dbc",
"size": 1144393,
"sha256": "9e135ad9a52ec64148c079e2ce32f080c818b2af71c8420f93a76c09be41962e",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 1143586,
"sha256": "28316d79b6f4f846f8091bc27e59655894aee48905be686b7971554c2fe17dbc",
"size": 1144393,
"sha256": "9e135ad9a52ec64148c079e2ce32f080c818b2af71c8420f93a76c09be41962e",
"executable": false
},
{
+4
View File
@@ -6598,3 +6598,7 @@ launchers:
\ / KEYS_DIR"
fails_when_missing: false
source_ref: "package/batocera/emulators/citron/002-adjust-paths.patch:9-10"
configs_root:
path: /userdata/system/configs
from_bios: ../system/configs
source_ref: "package/batocera/core/batocera-configgen/configgen/configgen/batoceraPaths.py:14, 19-20, 25"
+4 -10
View File
@@ -186,7 +186,6 @@ cores:
- superbroswar
- supermodel
- supermodel-legacy
- suyu
- swanstation
- taradino
- tgbdual
@@ -4970,7 +4969,6 @@ standalone_cores:
- stella
- supermodel
- supermodel-legacy
- suyu
- taradino
- theforceengine
- thextech
@@ -6143,14 +6141,6 @@ launchers:
depends_on_option: not system.name == 'ti99'
source_ref: "package/system/knulli-configgen/configgen/configgen/generators/libretro/libretroMAMEConfig.py:431-432,\
\ 435"
- emulator: suyu
build: standalone
kind: passes_path
names: []
search_dir: /userdata/saves/switch
passes_to: environment XDG_DATA_HOME
fails_when_missing: false
source_ref: "package/system/knulli-configgen/configgen/configgen/generators/suyu/suyuGenerator.py:41"
- emulator: citron
build: standalone
kind: passes_path
@@ -6160,3 +6150,7 @@ launchers:
\ / KEYS_DIR"
fails_when_missing: false
source_ref: "batocera/package/batocera/emulators/citron/002-adjust-paths.patch:9-10"
configs_root:
path: /userdata/system/configs
from_bios: ../system/configs
source_ref: "package/system/knulli-configgen/configgen/configgen/batoceraPaths.py:14, 19-20, 25"
+27 -2
View File
@@ -4971,17 +4971,42 @@ launchers:
kind: passes_path
names: []
search_dir: "<AppConfig.GetFullPath(\"citron\")>/user"
passes_to: PORTABLE_DIR, the user folder beside citron.exe
passes_to: PORTABLE_DIR, the portable folder beside citron.exe
fails_when_missing: false
source_ref: "emulatorLauncher/Generators/Citron.Generator.cs:25, 29-30"
- emulator: eden
build: standalone
kind: passes_path
names: []
search_dir: "<AppConfig.GetFullPath(\"eden\")>/user"
passes_to: PORTABLE_DIR, the portable folder beside eden.exe
fails_when_missing: false
source_ref: "emulatorLauncher/Generators/Eden.Generator.cs:28, 35-36"
- emulator: ryujinx
build: standalone
kind: passes_path
names: []
search_dir: "<AppConfig.GetFullPath(\"ryujinx\")>/portable"
passes_to: DefaultPortableDir, the portable folder beside Ryujinx.exe
fails_when_missing: false
depends_on_option: unless saves/switch/ryujinx/portable exists, passed with -r
source_ref: "emulatorLauncher/Generators/Ryujinx.Generator.cs:31, 73-75"
- emulator: shadps4
build: standalone
kind: passes_path
names: []
search_dir: "<AppConfig.GetFullPath(\"shadps4\")>/user"
passes_to: PORTABLE_DIR, the user folder beside shadPS4.exe
passes_to: PORTABLE_DIR, the portable folder beside shadPS4.exe
fails_when_missing: false
source_ref: "emulatorLauncher/Generators/ShadPS4.Generator.cs:29, 158-160"
- emulator: yuzu
build: standalone
kind: passes_path
names: []
search_dir: "<AppConfig.GetFullPath(\"yuzu\")>/user"
passes_to: PORTABLE_DIR, the portable folder beside yuzu.exe
fails_when_missing: false
source_ref: "emulatorLauncher/Generators/Yuzu.Generator.cs:24, 33-34"
- emulator: bizhawk
build: standalone
kind: names_searched
+32 -7
View File
@@ -207,7 +207,8 @@
"no_save_system",
"outside_bios_folder",
"no_roms_directory",
"no_emulators_directory"
"no_emulators_directory",
"no_configs_directory"
],
"description": "Why the file is left out, as a code. enumDescriptions gives the meaning of each code, in the order of enum.",
"enumDescriptions": [
@@ -222,7 +223,8 @@
"The launcher gives each system its own save folder and no system of the platform runs the core.",
"The platform runs the emulator from a folder outside the BIOS tree and links none of the BIOS tree where the emulator reads this file (root: emulator).",
"The platform's launcher runs the file from its roms folder (root: roms) and the run cannot tell where that folder is: the BIOS folder is not the one the platform keeps in it.",
"The platform's launcher runs the file from one of its emulator folders (root: emulators) and the run cannot place it there: the BIOS folder is not the one beside the platform's launcher config, or that config sets the folder."
"The platform's launcher runs the file from one of its emulator folders (root: emulators) and the run cannot place it there: the BIOS folder is not the one beside the platform's launcher config, or that config sets the folder.",
"The platform's launcher starts the emulator from a folder of its configs folder (root: configs) and the run cannot tell where that folder is: the BIOS folder is not the one the platform keeps beside it."
]
},
"category": {
@@ -367,12 +369,13 @@
}
},
"root": {
"description": "save: dest is relative to the save directory the frontend hands the core (RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY), not to the BIOS directory. emulator: dest is the path the emulator reads in its own folder, which the platform keeps outside the BIOS tree (reason outside_bios_folder). roms: dest is relative to the platform's roms folder (roms_directory). emulators: dest is the emulator folder's key then the path in it (emulators_directory). Absent for a file of the BIOS tree.",
"description": "save: dest is relative to the save directory the frontend hands the core (RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY), not to the BIOS directory. emulator: dest is the path the emulator reads in its own folder, which the platform keeps outside the BIOS tree (reason outside_bios_folder). roms: dest is relative to the platform's roms folder (roms_directory). emulators: dest is the emulator folder's key then the path in it (emulators_directory). configs: dest is relative to the frontend's configs folder (configs_directory). Absent for a file of the BIOS tree.",
"enum": [
"save",
"emulator",
"roms",
"emulators"
"emulators",
"configs"
]
},
"core_folder": {
@@ -581,6 +584,27 @@
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$"
}
}
},
"configs_directory": {
"description": "Where the frontend keeps its emulators' configs, present when an entry carries root: configs. from_bios is that folder relative to the BIOS folder, as the platform's own path code builds it (Batocera's batoceraPaths CONFIGS, ../system/configs), with the rules of roms_directory.from_bios. It is followed only from a BIOS folder named bios_folder; any other leaves root: configs entries out (no_configs_directory).",
"type": "object",
"additionalProperties": false,
"required": [
"bios_folder",
"from_bios"
],
"properties": {
"bios_folder": {
"type": "string",
"minLength": 1,
"pattern": "^[^/\\\\]+$"
},
"from_bios": {
"type": "string",
"pattern": "^(\\.|\\.\\.(/\\.\\.)?(/[^/\\\\:.][^/\\\\:]*)*)$",
"maxLength": 1024
}
}
}
},
"$defs": {
@@ -599,7 +623,7 @@
"dest": {
"type": "string",
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$",
"description": "Path below base_destination, below the save directory when root is save, below the roms folder when root is roms, or in an emulator folder when root is emulators (its key first)."
"description": "Path below base_destination, below the save directory when root is save, below the roms folder when root is roms, below the configs folder when root is configs, or in an emulator folder when root is emulators (its key first)."
},
"sha1": {
"type": "string",
@@ -1082,11 +1106,12 @@
}
},
"root": {
"description": "save: dest is relative to the save directory the frontend hands the core (RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY), not to the BIOS directory. roms: dest is relative to the platform's roms folder, which roms_directory locates (a file its launcher runs from there, ROCKNIX's roms/pico-8). emulators: dest is the key of an emulator folder of the platform, then the path in it, located by emulators_directory (RetroBat's emulators/pico8). Absent for a file of the BIOS tree.",
"description": "save: dest is relative to the save directory the frontend hands the core (RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY), not to the BIOS directory. roms: dest is relative to the platform's roms folder, which roms_directory locates (a file its launcher runs from there, ROCKNIX's roms/pico-8). emulators: dest is the key of an emulator folder of the platform, then the path in it, located by emulators_directory (RetroBat's emulators/pico8). configs: dest is relative to the frontend's configs folder, which configs_directory locates (Batocera starts shadPS4 from configs/shadps4). Absent for a file of the BIOS tree.",
"enum": [
"save",
"roms",
"emulators"
"emulators",
"configs"
]
},
"core_folder": {
+24
View File
@@ -188,6 +188,30 @@
}
}
}
},
"configs_root": {
"description": "Where the frontend keeps its emulators' configs, as its own path code builds it (Batocera's batoceraPaths CONFIGS): path as the OS spells it, from_bios relative to the BIOS folder. A launcher that starts an emulator from a folder below it (shadPS4's configs/shadps4) places that emulator's user folder there, and the install manifest writes such a file as root: configs.",
"type": "object",
"additionalProperties": false,
"required": [
"path",
"from_bios",
"source_ref"
],
"properties": {
"path": {
"type": "string",
"minLength": 1
},
"from_bios": {
"type": "string",
"minLength": 1
},
"source_ref": {
"type": "string",
"minLength": 1
}
}
}
},
"$defs": {
+11 -1
View File
@@ -2825,7 +2825,8 @@ OUTSIDE_BIOS_FOLDER = "outside_bios_folder"
# describes. Each is located by the registry entry <root>_directory.
ROMS_ROOT = "roms"
EMULATORS_ROOT = "emulators"
OUTER_ROOTS = (ROMS_ROOT, EMULATORS_ROOT)
CONFIGS_ROOT = "configs"
OUTER_ROOTS = (ROMS_ROOT, EMULATORS_ROOT, CONFIGS_ROOT)
def outer_layout(config: dict, root: str) -> dict | None:
@@ -2858,6 +2859,15 @@ def outer_layout(config: dict, root: str) -> dict | None:
return {"path": str(path), "install_prefix": "" if inside == "." else inside}
if root == EMULATORS_ROOT:
return config.get("emulators_directory") or None
if root == CONFIGS_ROOT:
# The frontend's configs folder, as its path code builds it
# (configs_root, Batocera's batoceraPaths CONFIGS).
found = config.get("configs_root") or {}
base = str(config.get("base_destination") or "")
if not (found.get("path") and found.get("from_bios") and base):
return None
return {"path": str(found["path"]), "from_bios": str(found["from_bios"]),
"bios_folder": base}
return None
+29 -2
View File
@@ -331,6 +331,8 @@ BIOS_TREE = ""
ROMS_TREE = "roms"
# The emulator folders of a platform whose registry describes them.
EMULATORS_TREE = "emulators"
# The frontend's configs folder of a platform whose file records it.
CONFIGS_TREE = "configs"
# The tree a platform's list installs into when it starts above the BIOS
# folder: a plain dest, with no prefix.
INSTALL_TREE = "install"
@@ -433,12 +435,18 @@ def handed_directories(config: dict) -> list[HandedDirectory]:
source_ref=str(record.get("source_ref", "")),
roms_place=None if folder is not None else (
_roms_place(config, folder_in_roms(raw, config))
or _configs_place(folder_in_configs(raw, config))
or _emulators_place(folder_in_emulators(raw, config))
),
))
return found
def _configs_place(path: str | None) -> Place | None:
"""A folder below the configs root, as a place of the configs tree."""
return None if path is None else (CONFIGS_TREE, path)
def _emulators_place(path: str | None) -> Place | None:
"""A folder an emulator key names, as a place of the emulators tree."""
return None if path is None else (EMULATORS_TREE, path)
@@ -458,6 +466,18 @@ def _roms_place(config: dict, path: str | None) -> Place | None:
return (ROMS_TREE, path)
def folder_in_configs(directory: str, config: dict) -> str | None:
"""A directory relative to the frontend's configs folder, None outside it.
Only a platform whose file records its configs root has one
(``configs_root.path``): Batocera's /userdata/system/configs, where its
generators start shadPS4 from configs/shadps4.
"""
from common import CONFIGS_ROOT # noqa: PLC0415
return folder_in_root(directory, config, CONFIGS_ROOT)
def folder_in_roms(directory: str, config: dict) -> str | None:
"""A directory relative to the platform's roms directory, None outside it.
@@ -465,9 +485,16 @@ def folder_in_roms(directory: str, config: dict) -> str | None:
(``roms_root.path``): ROCKNIX's /storage/roms, which holds its BIOS
folder and the folders its launchers run programs from.
"""
from common import ROMS_ROOT, outer_layout # noqa: PLC0415
from common import ROMS_ROOT # noqa: PLC0415
roms = str((outer_layout(config, ROMS_ROOT) or {}).get("path") or "").rstrip("/")
return folder_in_root(directory, config, ROMS_ROOT)
def folder_in_root(directory: str, config: dict, root: str) -> str | None:
"""A directory relative to an outer root's path, None outside it."""
from common import outer_layout # noqa: PLC0415
roms = str((outer_layout(config, root) or {}).get("path") or "").rstrip("/")
directory = directory.strip().rstrip("/")
if not roms or not directory.startswith(roms + "/"):
return None
+26
View File
@@ -97,6 +97,32 @@ def path_constants(source: Source) -> dict[str, str]:
return constants
def configs_root(cg: Configgen) -> dict[str, str]:
"""Where configgen keeps the emulators' configs, relative to the BIOS folder.
batoceraPaths' CONFIGS (/userdata/system/configs), the folder a generator
starts an emulator from (shadPS4's configs/shadps4), with the lines that
build it and BIOS.
"""
import posixpath # noqa: PLC0415
src = cg.source(f"{cg.generators.rsplit('/', 1)[0]}/batoceraPaths.py")
if "CONFIGS" not in cg.paths:
src.fail("no CONFIGS path constant")
wanted: set[str] = {"CONFIGS", "BIOS"}
spans = []
for node in reversed(src.tree.body):
name = _assigned_name(node)
if name in wanted:
spans.append((node.lineno, node.end_lineno or node.lineno))
wanted |= {n.id for n in ast.walk(node.value) if isinstance(n, ast.Name)} # type: ignore[union-attr]
return {
"path": cg.paths["CONFIGS"],
"from_bios": posixpath.relpath(cg.paths["CONFIGS"], cg.bios),
"source_ref": src.ref(spans),
}
def _path_value(node: ast.AST | None, constants: dict[str, str]) -> str | None:
if isinstance(node, ast.Name):
return constants.get(node.id)
+94 -8
View File
@@ -14,6 +14,7 @@ import re
import sys
import urllib.error
import urllib.request
from collections.abc import Callable, Iterable
from pathlib import Path
@@ -28,7 +29,7 @@ from .base_scraper import (
md5_field,
requirement_entry,
)
from .batocera_launchers import extract_launchers
from .batocera_launchers import Configgen, configs_root, extract_launchers
PLATFORM_NAME = "batocera"
@@ -168,7 +169,8 @@ def es_systems_libretro_systems(text: str, dropped: set[str]) -> dict[str, list[
def es_systems_by_system(
es_systems_text: str, defaults_text: str, dropped: set[str]
es_systems_text: str, defaults_text: str, dropped: set[str],
built: Callable[[str], bool] | None = None,
) -> dict[str, list[str]]:
"""system -> the cores es_systems.yml offers it, in the file's order.
@@ -195,6 +197,10 @@ def es_systems_by_system(
default = defaults.get(system)
if isinstance(default, dict):
emulator, core = default.get("emulator"), default.get("core")
if (built is not None and emulator and emulator != "libretro"
and str(emulator) not in names and not built(str(emulator))):
# A default the build lacks (configgen_cores).
emulator = core = None
if emulator and emulator != "libretro":
names.append(str(emulator))
if core:
@@ -472,7 +478,8 @@ def platform_systems(
def configgen_cores(
defaults_text: str, es_systems_text: str, dropped: set[str]
defaults_text: str, es_systems_text: str, dropped: set[str],
built: Callable[[str], bool] | None = None,
) -> tuple[list[str], list[str], list[str]]:
"""Core names, standalone cores and dual-mode cores of a configgen build.
@@ -481,9 +488,16 @@ def configgen_cores(
dual-mode ones those es_systems.yml also offers under libretro (dolphin
for GameCube). `dropped` names what es_systems.yml files under an
emulator without being one.
A default es_systems.yml does not offer names an emulator the build may
lack: KNULLI's defaults pick suyu for the Switch, and no package of its
tree, its batocera submodule or its buildroot builds suyu. `built` says
whether the build has a standalone emulator (packages_and_programs); a
default it answers no for is not run, nor its core.
"""
data = yaml_load_upstream(defaults_text)
cores, standalone = es_systems_emulators(es_systems_text)
offered = set(cores)
libretro = es_systems_libretro(es_systems_text)
if not cores:
raise RuntimeError("es_systems.yml names no emulator")
@@ -492,6 +506,9 @@ def configgen_cores(
continue
emulator = cfg.get("emulator", "")
core = cfg.get("core", "")
if (built is not None and emulator and emulator != "libretro"
and emulator not in offered and not built(emulator)):
continue
if core:
cores.add(core)
if emulator and emulator != "libretro":
@@ -502,6 +519,57 @@ def configgen_cores(
return sorted(cores - dropped), sorted(standalone - dropped), sorted(dual)
BATOCERA_REPO = "batocera-linux/batocera.linux"
def package_names(paths: Iterable[str]) -> set[str]:
"""The buildroot packages a tree defines: each folder holding a Config.in."""
return {
path.rsplit("/", 2)[-2]
for path in paths
if path.endswith("/Config.in") and path.count("/") >= 1
}
def repo_tree(repo: str, ref: str) -> list[str]:
"""Every path of a GitHub repository at a revision."""
url = f"https://api.github.com/repos/{repo}/git/trees/{ref}?recursive=1"
req = urllib.request.Request(url, headers=github_headers())
with urllib.request.urlopen(req, timeout=60) as resp:
found = json.loads(resp.read().decode("utf-8"))
if found.get("truncated"):
raise RuntimeError(f"{url}: tree truncated")
return [entry["path"] for entry in found.get("tree", [])]
def submodule_revision(repo: str, path: str, ref: str) -> str:
"""The commit a repository's submodule at `path` pins at a revision."""
url = f"https://api.github.com/repos/{repo}/contents/{path}?ref={ref}"
req = urllib.request.Request(url, headers=github_headers())
with urllib.request.urlopen(req, timeout=30) as resp:
found = json.loads(resp.read().decode("utf-8"))
if not isinstance(found, dict) or found.get("type") != "submodule" or not found.get("sha"):
raise RuntimeError(f"{url}: {path} is not a submodule")
return str(found["sha"])
def packages_and_programs(packages: set[str], launchers: list[dict]) -> Callable[[str], bool]:
"""Whether a build has a standalone emulator.
It does when a package of that name exists, or when its launcher runs a
program the user puts in the BIOS folder (Batocera's lexaloffle runs
/userdata/bios/pico-8/pico8), which no package builds.
"""
supplied = {
name.strip()
for record in launchers
if record.get("passes_to") == "exec"
and str(record.get("search_dir") or "").startswith("/userdata/bios")
for name in str(record.get("emulator", "")).split(",")
}
return lambda emulator: emulator in packages or emulator in supplied
class Scraper(BaseScraper):
"""Scraper for batocera-systems Python dict."""
@@ -509,12 +577,26 @@ class Scraper(BaseScraper):
self.tag = fetch_stable_tag()
super().__init__(url=SOURCE_URL.format(tag=self.tag))
def _fetch_cores(self) -> tuple[list[str], list[str], list[str]]:
"""Core names, standalone cores and dual-mode cores at the pinned tag."""
def _fetch_cores(
self, launchers: list[dict] | None = None
) -> tuple[list[str], list[str], list[str]]:
"""Core names, standalone cores and dual-mode cores at the pinned tag.
A default es_systems.yml does not offer counts when a package of
Batocera's tree or its buildroot builds it, or when its launcher
runs a program from the BIOS folder.
"""
built = None
if launchers is not None:
paths = repo_tree(BATOCERA_REPO, self.tag)
buildroot = submodule_revision(BATOCERA_REPO, "buildroot", self.tag)
paths += repo_tree("batocera-linux/buildroot", buildroot)
built = packages_and_programs(package_names(paths), launchers)
return configgen_cores(
self._fetch_text(CONFIGGEN_DEFAULTS_URL.format(tag=self.tag)),
self._fetch_text(ES_SYSTEMS_URL.format(tag=self.tag)),
_not_cores(),
built,
)
def _fetch_text(self, url: str) -> str:
@@ -548,7 +630,10 @@ class Scraper(BaseScraper):
SYSTEM_SLUG_MAP,
)
cores, standalone, dual = self._fetch_cores()
launchers = extract_launchers(
lambda path: self._fetch_text(f"{_RAW_BASE}/{self.tag}/{path}")
)
cores, standalone, dual = self._fetch_cores(launchers)
libretro_systems = es_systems_libretro_systems(
self._fetch_text(ES_SYSTEMS_URL.format(tag=self.tag)), _not_cores()
)
@@ -570,8 +655,9 @@ class Scraper(BaseScraper):
result["dual_mode_cores"] = dual
# The systems each libretro core runs for: its save folders.
result["libretro_systems"] = libretro_systems
result["launchers"] = extract_launchers(
lambda path: self._fetch_text(f"{_RAW_BASE}/{self.tag}/{path}")
result["launchers"] = launchers
result["configs_root"] = configs_root(
Configgen(lambda path: self._fetch_text(f"{_RAW_BASE}/{self.tag}/{path}"))
)
return result
-36
View File
@@ -48,7 +48,6 @@ try:
)
from .configgen_launchers import (
Launcher,
_calls,
LauncherShapeError,
Source,
_assigned_name,
@@ -74,7 +73,6 @@ except ImportError:
)
from configgen_launchers import ( # type: ignore[no-redef]
Launcher,
_calls,
LauncherShapeError,
Source,
_assigned_name,
@@ -1062,39 +1060,6 @@ EXTRACTORS: tuple[Callable[[Configgen], list[Launcher]], ...] = (
)
# Generators whose command sets the emulator's XDG data folder, the parent
# of every user folder a yuzu-family emulator derives.
DATA_HOME_GENERATORS = ("suyu.suyuGenerator",)
def data_homes(cg: Configgen) -> list[Launcher]:
"""Command(..., env={"XDG_DATA_HOME": <path>}) in the generators above.
The path need not lie in /userdata/bios: KNULLI's suyu keeps its data
in /userdata/saves/switch, which tells where its keys are not.
"""
launchers = []
for module in DATA_HOME_GENERATORS:
src = cg.generator(module)
found = [
(_path_value(item, cg.paths), key)
for call in _calls(src.tree, attr="Command")
for keyword in call.keywords
if keyword.arg == "env" and isinstance(keyword.value, ast.Dict)
for key, item in zip(keyword.value.keys, keyword.value.values)
if isinstance(key, ast.Constant) and key.value == "XDG_DATA_HOME"
]
if not found or found[0][0] is None:
src.fail("the command environment sets no XDG_DATA_HOME path")
value, node = found[0]
launchers.append(Launcher(
emulator=cg.emulator(module), build="standalone", kind="passes_path", names=[],
search_dir=value, passes_to="environment XDG_DATA_HOME",
source_ref=src.ref([(node.lineno, node.end_lineno)]),
))
return launchers
# KNULLI builds Batocera's emulator packages from this submodule, at the
# batocera.linux commit it pins: Citron's path patch is there.
BATOCERA_SUBMODULE = "batocera"
@@ -1114,7 +1079,6 @@ def extract_launchers(
launchers += boot_disks(cg, read)
for spec in GENERIC_SITES:
launchers += bios_sites(cg, spec)
launchers += data_homes(cg)
if batocera is not None:
prefix = f"{BATOCERA_SUBMODULE}/"
launchers += patched_paths(
+27 -6
View File
@@ -41,11 +41,17 @@ from .batocera_scraper import (
es_systems_by_system,
es_systems_libretro_systems,
extract_systems_dict,
package_names,
packages_and_programs,
platform_systems,
repo_tree,
requirements_from_systems,
submodule_revision,
)
from .es_emulators import attach_system_cores
from .batocera_launchers import configs_root
from .knulli_launchers import BATOCERA_SUBMODULE, extract_launchers
from .knulli_launchers import configgen as knulli_configgen
PLATFORM_NAME = "knulli"
@@ -123,11 +129,27 @@ class Scraper(BaseScraper):
defaults = self._fetch_text(CONFIGGEN_DEFAULTS_TEMPLATE.format(ref=self.tag))
es_systems = self._fetch_text(ES_SYSTEMS_TEMPLATE.format(ref=self.tag))
dropped = _not_cores("batocera") | _not_cores(PLATFORM_NAME)
cores, standalone, dual = configgen_cores(defaults, es_systems, dropped)
revision = self.submodule_revision()
launchers = extract_launchers(
lambda path: self._fetch_text(_RAW.format(ref=self.tag) + path),
lambda path: self._fetch_text(_BATOCERA_RAW.format(ref=revision) + path),
)
# The packages KNULLI builds come from its own tree, its batocera
# submodule and its buildroot submodule: a default no package of
# them builds (suyu) is not run.
paths = repo_tree(REPO, self.tag)
paths += repo_tree("batocera-linux/batocera.linux", revision)
paths += repo_tree(
"knulli-cfw/buildroot", submodule_revision(REPO, "buildroot", self.tag)
)
built = packages_and_programs(package_names(paths), launchers)
cores, standalone, dual = configgen_cores(defaults, es_systems, dropped, built)
# Each system's own list (psx, es_systems.yml:640-653: libretro
# pcsx_rearmed, swanstation, mednafen_psx, then duckstation and
# duckstation-legacy).
attach_system_cores(systems, es_systems_by_system(es_systems, defaults, dropped))
attach_system_cores(
systems, es_systems_by_system(es_systems, defaults, dropped, built)
)
result = {
"platform": "KNULLI",
"version": self.tag,
@@ -146,10 +168,9 @@ class Scraper(BaseScraper):
# configgen hands a libretro core /userdata/saves/<system>, as
# Batocera's does: the systems each core runs for are its save folders.
result["libretro_systems"] = es_systems_libretro_systems(es_systems, dropped)
revision = self.submodule_revision()
result["launchers"] = extract_launchers(
lambda path: self._fetch_text(_RAW.format(ref=self.tag) + path),
lambda path: self._fetch_text(_BATOCERA_RAW.format(ref=revision) + path),
result["launchers"] = launchers
result["configs_root"] = configs_root(
knulli_configgen(lambda path: self._fetch_text(_RAW.format(ref=self.tag) + path))
)
return result
+41 -19
View File
@@ -32,6 +32,7 @@ SOURCE_FILES = (
f"{GENERATORS}/DosBoxPure.Generator.cs",
f"{GENERATORS}/DosBoxStaging.Generator.cs",
f"{GENERATORS}/Duckstation.Generator.cs",
f"{GENERATORS}/Eden.Generator.cs",
f"{GENERATORS}/Eka2l1.Generator.cs",
f"{GENERATORS}/Fbneo.Generator.cs",
f"{GENERATORS}/Flycast.Generator.cs",
@@ -57,6 +58,7 @@ SOURCE_FILES = (
f"{GENERATORS}/Project64.Generator.cs",
f"{GENERATORS}/Raine.Generator.cs",
f"{GENERATORS}/Rpcs3.Generator.cs",
f"{GENERATORS}/Ryujinx.Generator.cs",
f"{GENERATORS}/SSF.Generator.cs",
f"{GENERATORS}/ScummVm.Generator.cs",
f"{GENERATORS}/ShadPS4.Generator.cs",
@@ -68,6 +70,7 @@ SOURCE_FILES = (
f"{GENERATORS}/Xroar.Generator.cs",
f"{GENERATORS}/Yabasanshiro.Generator.cs",
f"{GENERATORS}/Ymir.Generator.cs",
f"{GENERATORS}/Yuzu.Generator.cs",
f"{GENERATORS}/mGBA.Generator.cs",
FEATURES,
)
@@ -2982,44 +2985,63 @@ def xroar(sources: dict[str, Source]) -> list[Record]:
return _covered(src, records)
# Generators that create a portable user folder beside the emulator's binary,
# and the emulator each one starts: the folder AppConfig.GetFullPath gives.
# Generators that start an emulator with a portable data folder beside its
# binary: (generator, emulator key, binary, the code's name for the folder).
PORTABLE_USERS = (
("Citron.Generator.cs", "citron", "citron.exe"),
("ShadPS4.Generator.cs", "shadps4", "shadPS4.exe"),
("Citron.Generator.cs", "citron", "citron.exe", "PORTABLE_DIR"),
("Eden.Generator.cs", "eden", "eden.exe", "PORTABLE_DIR"),
("Ryujinx.Generator.cs", "ryujinx", "Ryujinx.exe", "DefaultPortableDir"),
("ShadPS4.Generator.cs", "shadps4", "shadPS4.exe", "PORTABLE_DIR"),
("Yuzu.Generator.cs", "yuzu", "yuzu.exe", "PORTABLE_DIR"),
)
_EMULATOR_FOLDER = (
r"GetEmulatorPathAndExe\(emulator, out string path, out string exe, \"[^\"]+\"\)"
r"|string path = AppConfig\.GetFullPath\(emulator\);"
r"|string path = AppConfig\.GetFullPath\((?:emulator(?:\.Replace\(\"-\", \" \"\))?"
r"|\"\w+\")\);"
)
_PORTABLE_FOLDER = r'string (userFolder|setupPath) = Path\.Combine\(path, "(\w+)"\);'
# Ryujinx's generator hands --root-data-dir saves/switch/ryujinx/portable
# instead when that folder exists.
_ALTERNATE_ROOT = (
r'string portablePath = Path\.Combine\(AppConfig\.GetFullPath\("saves"\), "switch", '
r'"ryujinx", "portable"\);'
)
def portable_users(sources: dict[str, Source]) -> list[Record]:
"""The portable user folder a generator creates beside the binary.
"""The portable data folder a generator gives an emulator beside its binary.
The emulator folder comes from AppConfig.GetFullPath(emulator), and the
generator creates <folder>/user before launching: the emulator then
keeps its data there (keys/, sys_modules/) in place of %APPDATA%.
The emulator folder comes from AppConfig.GetFullPath, and the generator
creates or configures <folder>/user (<folder>/portable for Ryujinx)
before launching: the emulator then keeps its data there (keys/,
sys_modules/, system/) in place of %APPDATA%.
"""
records = []
for name, emulator, binary in PORTABLE_USERS:
for name, emulator, binary, handle in PORTABLE_USERS:
src = sources[f"{GENERATORS}/{name}"]
found = src.find(_EMULATOR_FOLDER, f"{emulator} folder")
user = src.find(r'string userFolder = Path\.Combine\(path, "(\w+)"\);', "user folder")
create = src.find(
r"Directory\.CreateDirectory\(userFolder\)", "user folder creation", user.end()
)
folder = src.find(_PORTABLE_FOLDER, "portable folder")
spans = [src.lines(found.start(), found.end()), src.lines(folder.start(), folder.end())]
if folder.group(1) == "userFolder":
create = src.find(
r"Directory\.CreateDirectory\(userFolder\)", "user folder creation", folder.end()
)
spans[-1] = src.lines(folder.start(), create.end())
depends = None
alternate = re.search(_ALTERNATE_ROOT, src.text)
if alternate:
spans.append(src.lines(alternate.start(), alternate.end()))
depends = "unless saves/switch/ryujinx/portable exists, passed with -r"
records.append(_record(
emulator,
"standalone",
"passes_path",
[],
source_ref((src.path, [
src.lines(found.start(), found.end()), src.lines(user.start(), create.end()),
])),
search_dir=f'<AppConfig.GetFullPath("{emulator}")>/{user.group(1)}',
passes_to=f"PORTABLE_DIR, the user folder beside {binary}",
source_ref((src.path, spans)),
search_dir=f'<AppConfig.GetFullPath("{emulator}")>/{folder.group(2)}',
passes_to=f"{handle}, the portable folder beside {binary}",
fails_when_missing=False,
depends_on_option=depends,
))
return records
+1
View File
@@ -980,6 +980,7 @@ _UNPLACED_REASONS = {
"outside_bios_folder": "read in a folder the platform keeps outside the BIOS tree",
"no_roms_directory": "read below a roms folder the platform's registry does not name",
"no_emulators_directory": "read in an emulator folder the platform's registry does not describe",
"no_configs_directory": "read below a configs folder the platform's file does not record",
"no_platform_slug": "no platform slug for its system",
"below_platform_folder": "below the folder the platform lists",
"no_save_directory": "read below a save directory the platform keeps in no fixed place",
+5 -5
View File
@@ -1150,7 +1150,7 @@ class OuterRootsAreUntrusted(unittest.TestCase):
def test_unsafe_from_bios_is_refused(self):
for value in (*self.UNSAFE, "../../../x", "../x/..", "x", "./x", "/x", "..x", None):
with self.subTest(from_bios=value), self.assertRaises(ValueError):
install._check_roms_directory({"roms_directory": {**self.ROMS, "from_bios": value}})
install._check_relative_directories({"roms_directory": {**self.ROMS, "from_bios": value}})
def test_the_relations_the_rom_folders_files_use_are_accepted(self):
bios = Path("/card/system/bios")
@@ -1161,13 +1161,13 @@ class OuterRootsAreUntrusted(unittest.TestCase):
}
for value, root in expected.items():
with self.subTest(from_bios=value):
install._check_roms_directory({"roms_directory": {**self.ROMS, "from_bios": value}})
self.assertEqual(install._roms_root(bios, value), root)
install._check_relative_directories({"roms_directory": {**self.ROMS, "from_bios": value}})
self.assertEqual(install._relative_root(bios, value), root)
self.assertEqual(install.MAX_ROMS_UPS, 2)
def test_a_list_without_a_roms_root_leaves_the_files_out(self):
files = [{"dest": "pico-8/pico8_64", "root": "roms", "cores": ["x"]}]
kept, left = install.place_roms_entries(files, Path("/card/roms/bios"), {}, "x", say=False)
kept, left = install.place_relative_entries(files, Path("/card/roms/bios"), {}, "x", say=False)
self.assertEqual((kept, [o["reason"] for o in left]), ([], ["no_roms_directory"]))
def test_unsafe_emulator_folders_are_refused(self):
@@ -1192,7 +1192,7 @@ class OuterRootsAreUntrusted(unittest.TestCase):
with unittest.mock.patch.object(install.os, "name", host):
for value in ("../C:x", "..\\x", "../x:", "../Roms:x"):
with self.subTest(host=host, from_bios=value), self.assertRaises(ValueError):
install._check_roms_directory(
install._check_relative_directories(
{"roms_directory": {**self.ROMS, "from_bios": value}})
for value in ("C:x/{key}", "x:/{key}", "emu:x/{key}"):
layout = {**self.EMULATORS, "searched": [value], "default": value}
+135 -31
View File
@@ -21,7 +21,6 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from launchers import handed_directories, standalone_bindings # noqa: E402
from scraper import knulli_launchers as kl # noqa: E402
from scraper import retrobat_launchers as rl # noqa: E402
from scraper.batocera_launchers import ( # noqa: E402
GENERATORS,
@@ -128,35 +127,27 @@ class AFolderTheGeneratorEnters(unittest.TestCase):
self.assertEqual(found, {"": ("outside", "")})
class ADataFolderTheEnvironmentSets(unittest.TestCase):
SUYU = f"{kl.GENERATORS}/suyu/suyuGenerator.py"
TEXT = textwrap.dedent("""\
from ...batoceraPaths import CACHE, CONFIGS, SAVES
class SuyuGenerator:
def generate(self, system, rom):
commandArray = ["/usr/bin/suyu", "-f", "-g", rom]
return Command.Command(array=commandArray, env={
"XDG_CONFIG_HOME": CONFIGS,
"XDG_DATA_HOME": SAVES / "switch",
})
""")
def test_the_data_folder_is_a_record_and_binds_outside(self):
files = {f"{kl.CONFIGGEN}/batoceraPaths.py": PATHS,
f"{kl.GENERATORS}/importer.py": IMPORTER_TEXT, self.SUYU: self.TEXT}
(record,) = records(kl.data_homes(kl.configgen(files.__getitem__)))
self.assertEqual(
(record["emulator"], record["search_dir"], record["passes_to"], record["source_ref"]),
("suyu", "/userdata/saves/switch", "environment XDG_DATA_HOME", f"{self.SUYU}:8"),
)
config = {"base_destination": "bios", "standalone_cores": ["suyu"], "launchers": [record]}
class AVariableADeclaredFolderIsSpelledFrom(unittest.TestCase):
def test_a_data_folder_outside_the_tree_binds_outside(self):
record = {"emulator": "emu", "build": "standalone", "kind": "passes_path",
"names": [], "search_dir": "/userdata/saves/switch",
"passes_to": "environment XDG_DATA_HOME", "source_ref": "x.py:1"}
config = {"base_destination": "bios", "standalone_cores": ["emu"], "launchers": [record]}
found = standalone_bindings(
{"suyu"}, ["$XDG_DATA_HOME/suyu", "SuyuPath::KeysDir = keys"],
handed_directories(config),
{"emu"}, ["$XDG_DATA_HOME/emu"], handed_directories(config)
)
self.assertEqual(found, {"": ("outside", "")})
def test_a_data_folder_in_the_tree_binds_below_it(self):
record = {"emulator": "emu", "build": "standalone", "kind": "passes_path",
"names": [], "search_dir": "/userdata/bios/data",
"passes_to": "environment XDG_DATA_HOME", "source_ref": "x.py:1"}
config = {"base_destination": "bios", "standalone_cores": ["emu"], "launchers": [record]}
found = standalone_bindings(
{"emu"}, ["$XDG_DATA_HOME/emu"], handed_directories(config)
)
self.assertEqual(found, {"": ("", "data/emu")})
class APortableUserFolder(unittest.TestCase):
CITRON = """
@@ -185,12 +176,28 @@ class APortableUserFolder(unittest.TestCase):
}
"""
RYUJINX = """
public override System.Diagnostics.ProcessStartInfo Generate(string system, string emulator, string core, string rom, string playersControllers, ScreenResolution resolution)
{
string path = AppConfig.GetFullPath("ryujinx");
string portablePath = Path.Combine(AppConfig.GetFullPath("saves"), "switch", "ryujinx", "portable");
string setupPath = Path.Combine(path, "portable");
}
"""
def sources(self) -> dict[str, rl.Source]:
def source(ref: str, text: str) -> rl.Source:
return rl.Source(ref, textwrap.dedent(text).lstrip("\n"))
def yuzu_like(exe: str) -> str:
return self.CITRON.replace("citron.exe", exe)
return {
f"{rl.GENERATORS}/Citron.Generator.cs": rl.Source(
"c", textwrap.dedent(self.CITRON).lstrip("\n")),
f"{rl.GENERATORS}/ShadPS4.Generator.cs": rl.Source(
"s", textwrap.dedent(self.SHADPS4).lstrip("\n")),
f"{rl.GENERATORS}/Citron.Generator.cs": source("c", self.CITRON),
f"{rl.GENERATORS}/Eden.Generator.cs": source("e", yuzu_like("eden.exe")),
f"{rl.GENERATORS}/Ryujinx.Generator.cs": source("r", self.RYUJINX),
f"{rl.GENERATORS}/ShadPS4.Generator.cs": source("s", self.SHADPS4),
f"{rl.GENERATORS}/Yuzu.Generator.cs": source("y", yuzu_like("yuzu.exe")),
}
def test_each_generator_gives_its_user_folder(self):
@@ -198,7 +205,15 @@ class APortableUserFolder(unittest.TestCase):
self.assertEqual(
[(r["emulator"], r["search_dir"], r["source_ref"]) for r in found],
[("citron", '<AppConfig.GetFullPath("citron")>/user', "c:3, 7-8"),
("shadps4", '<AppConfig.GetFullPath("shadps4")>/user', "s:3, 8-10")],
("eden", '<AppConfig.GetFullPath("eden")>/user', "e:3, 7-8"),
("ryujinx", '<AppConfig.GetFullPath("ryujinx")>/portable', "r:3-5"),
("shadps4", '<AppConfig.GetFullPath("shadps4")>/user', "s:3, 8-10"),
("yuzu", '<AppConfig.GetFullPath("yuzu")>/user', "y:3, 7-8")],
)
ryujinx = found[2]
self.assertEqual(ryujinx["passes_to"], "DefaultPortableDir, the portable folder beside Ryujinx.exe")
self.assertEqual(
ryujinx["depends_on_option"], "unless saves/switch/ryujinx/portable exists, passed with -r"
)
def test_the_user_folder_binds_below_the_emulator_folder(self):
@@ -213,5 +228,94 @@ class APortableUserFolder(unittest.TestCase):
self.assertEqual(found, {"": ("emulators", "citron/user")})
class ADefaultTheBuildLacksIsNotRun(unittest.TestCase):
"""KNULLI's configgen-defaults.yml picks suyu for the Switch, which its
es_systems.yml does not offer and no package builds."""
ES = textwrap.dedent("""\
switch:
emulators:
citron:
citron: { requireAnyOf: [BR2_PACKAGE_CITRON] }
voxatron:
emulators: {}
""")
DEFAULTS = textwrap.dedent("""\
switch:
emulator: suyu
core: suyu
voxatron:
emulator: lexaloffle
core: vox_official
kodi:
emulator: kodi
core: kodi
""")
LAUNCHERS = [{"emulator": "lexaloffle", "kind": "fixed_name", "passes_to": "exec",
"search_dir": "/userdata/bios"}]
def test_only_what_a_package_or_the_user_supplies_is_run(self):
from scraper import batocera_scraper as bs
paths = ["package/batocera/emulators/citron/Config.in", "package/kodi/Config.in"]
built = bs.packages_and_programs(bs.package_names(paths), self.LAUNCHERS)
cores, standalone, _dual = bs.configgen_cores(self.DEFAULTS, self.ES, set(), built)
self.assertEqual(sorted(cores), ["citron", "kodi", "vox_official"])
self.assertIn("lexaloffle", standalone)
self.assertNotIn("suyu", standalone)
by_system = bs.es_systems_by_system(self.ES, self.DEFAULTS, set(), built)
self.assertEqual(by_system["switch"], ["citron"])
# Without the packages, every default counts, as before.
self.assertIn("suyu", bs.configgen_cores(self.DEFAULTS, self.ES, set())[0])
def test_the_real_knulli_list_names_no_suyu(self):
import yaml
config = yaml.safe_load((REPO_ROOT / "platforms" / "knulli.yml").read_text())
self.assertNotIn("suyu", config["cores"])
self.assertNotIn("suyu", config.get("standalone_cores") or [])
class AUserFolderInTheConfigsFolder(unittest.TestCase):
"""Batocera starts shadPS4 from /userdata/system/configs/shadps4, a folder
of the configs root its batoceraPaths builds (../system/configs)."""
def test_the_sys_modules_go_below_the_configs_root(self):
from common import load_emulator_profiles, load_platform_config, platform_layouts
from common import standalone_set
profile = load_emulator_profiles(str(REPO_ROOT / "emulators"))["shadps4"]
for platform in ("batocera", "knulli"):
config = load_platform_config(platform, str(REPO_ROOT / "platforms"))
((view, _standalone),) = platform_layouts("shadps4", profile, standalone_set(config))
with self.subTest(platform=platform):
self.assertEqual(
{(f.get("root"), f["standalone_path"].rsplit("/", 1)[0]) for f in view["files"]},
{("configs", "shadps4/user/sys_modules"), ("configs", "shadps4/user/fonts/font")},
)
def test_the_installer_follows_from_bios_to_the_configs_folder(self):
import install
manifest = {"configs_directory": {"bios_folder": "bios", "from_bios": "../system/configs"}}
install._check_relative_directories(manifest)
files = [{"dest": "shadps4/user/sys_modules/libSceNgs2.sprx", "root": "configs"}]
bios = Path("/userdata/bios")
kept, left = install.place_relative_entries(files, bios, manifest, "x", say=False)
self.assertEqual((len(kept), left), (1, []))
self.assertEqual(
install._entry_place(bios, kept[0])[0], Path("/userdata/system/configs")
)
kept, left = install.place_relative_entries(
files, Path("/elsewhere/firmware"), manifest, "x", say=False)
self.assertEqual((kept, [o["reason"] for o in left]), ([], ["no_configs_directory"]))
for value in ("../C:x", "../../../x", "../system/../x"):
with self.subTest(from_bios=value), self.assertRaises(ValueError):
install._check_relative_directories(
{"configs_directory": {"bios_folder": "bios", "from_bios": value}})
if __name__ == "__main__":
unittest.main()
+7 -7
View File
@@ -383,10 +383,10 @@ class AProgramRunFromItsFolder(unittest.TestCase):
import install
manifest = self.manifest("romsplat")
install._check_roms_directory(manifest)
install._check_relative_directories(manifest)
files = list(manifest["files"])
card = self.root / "card" / "roms" / "bios"
kept, left = install.place_roms_entries(files, card, manifest, "romsplat", say=False)
kept, left = install.place_relative_entries(files, card, manifest, "romsplat", say=False)
self.assertEqual((len(kept), left), (2, []))
self.assertEqual(
install._entry_path(card, kept[0]),
@@ -394,22 +394,22 @@ class AProgramRunFromItsFolder(unittest.TestCase):
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)
kept, left = install.place_relative_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})
install._check_relative_directories({"files": files})
# A roms root beside the BIOS folder (Batocera's ../roms).
beside = {**manifest, "roms_directory": {"bios_folder": "bios", "from_bios": "../roms"}}
install._check_roms_directory(beside)
install._check_relative_directories(beside)
disk = self.root / "userdata" / "bios"
install.place_roms_entries(files, disk, beside, "romsplat", say=False)
install.place_relative_entries(files, disk, beside, "romsplat", say=False)
self.assertEqual(install._entry_place(disk, kept_entry := files[0])[0],
self.root / "userdata" / "roms")
self.assertEqual(kept_entry["root"], "roms")
for climbing in ("roms", "../roms/..", "/storage/roms"):
with self.subTest(from_bios=climbing), self.assertRaises(ValueError):
install._check_roms_directory(
install._check_relative_directories(
{**manifest, "roms_directory": {"bios_folder": "bios", "from_bios": climbing}}
)