mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 12:19:13 -05:00
feat: file mame software lists and floppy samples
The -listxml names the software lists each machine and slot card declares and the floppy drives it can instantiate. --devices now writes one entry per hash/<list>.xml the release ships under the systems of its placed machines (111 lists, 666 stay with hash/), and files each floppy sample under the systems whose drives select its form factor (floppy.cpp:1428-1473, 1759-1767). Machines a registered platform declares under one of the profile's systems seed the walk too: bbcb, cdimono1, cdimono2, dragon32, ep64 and ep128. Unplaced device sets go from 903 to 861.
This commit is contained in:
1 parent
7ac96f2b39
commit
2d74524d59
9 files changed
+1959
-26
No files matched your search
+1152
-1
File diff suppressed because it is too large.
Load diff
@@ -21,6 +21,7 @@ from common import PROFILE_BACKUP_SUFFIX, write_text_atomic, yaml_load
|
||||
_MAME_RELEASE_RE = re.compile(r"^0\.\d+")
|
||||
_MAMEDEV = "https://github.com/mamedev/mame"
|
||||
DEVICE_SECTION = " # --- Device ROM sets (generated from MAME -listxml) ---"
|
||||
SOFTLIST_SECTION = " # --- Software lists (generated from MAME -listxml) ---"
|
||||
_FILES_END = re.compile(r"^\S", re.M)
|
||||
|
||||
|
||||
@@ -553,9 +554,9 @@ def _append_new_entries(text: str, files: list[dict], original: str) -> str:
|
||||
lines.append(_format_contents(fe["contents"]))
|
||||
|
||||
if lines:
|
||||
# New BIOS root sets go before the generated device section, which
|
||||
# runs to the end of the files list.
|
||||
at = _device_section_start(text)
|
||||
# New BIOS root sets go before the generated sections, which run to
|
||||
# the end of the files list.
|
||||
at = generated_start(text)
|
||||
if at is None:
|
||||
text = text.rstrip("\n") + "\n" + "\n".join(lines) + "\n"
|
||||
else:
|
||||
@@ -570,6 +571,18 @@ def _device_section_start(text: str) -> int | None:
|
||||
return match.start() if match else None
|
||||
|
||||
|
||||
def _softlist_section_start(text: str) -> int | None:
|
||||
"""Offset of the generated software list section header, if any."""
|
||||
match = re.search(rf"^{re.escape(SOFTLIST_SECTION)}$", text, re.M)
|
||||
return match.start() if match else None
|
||||
|
||||
|
||||
def generated_start(text: str) -> int | None:
|
||||
"""Offset of the first generated section: what a reader wrote ends there."""
|
||||
found = [at for at in (_softlist_section_start(text), _device_section_start(text)) if at is not None]
|
||||
return min(found) if found else None
|
||||
|
||||
|
||||
def _device_contents(roms: list[dict], old: list[dict]) -> list[dict]:
|
||||
"""Contents of a device set, keeping the member descriptions a reader wrote."""
|
||||
written = {str(c.get("name")): c.get("description") for c in old or []}
|
||||
@@ -641,7 +654,11 @@ def _set_section_systems(text: str, systems: dict[str, list[str]]) -> str:
|
||||
start = _device_section_start(text)
|
||||
if start is None:
|
||||
return text
|
||||
head, section = text[:start], text[start:]
|
||||
return text[:start] + _rewrite_systems(text[start:], systems)
|
||||
|
||||
|
||||
def _rewrite_systems(section: str, systems: dict[str, list[str]]) -> str:
|
||||
"""Rewrite the system fields of the named entries of one stretch of text."""
|
||||
out: list[str] = []
|
||||
current = ""
|
||||
in_list = False
|
||||
@@ -661,7 +678,7 @@ def _set_section_systems(text: str, systems: dict[str, list[str]]) -> str:
|
||||
out.append(line)
|
||||
if current in systems and re.match(r"^ category:", line):
|
||||
out.extend(_system_lines(systems[current]))
|
||||
return head + "\n".join(out)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def _format_device_entry(name: str, device: dict, role: str) -> str:
|
||||
@@ -762,6 +779,118 @@ def merge_mame_devices(
|
||||
return report
|
||||
|
||||
|
||||
def _softlist_section(profile: dict, lists: dict[str, list[str]], role: str) -> str:
|
||||
"""The generated software list section, one entry per list and tree.
|
||||
|
||||
A list takes the fields of each hash/ tree the profile delivers (a
|
||||
bundled tree ships with the emulator, not in a pack) and the path of
|
||||
that tree. Members the section names leave the tree's own entry.
|
||||
"""
|
||||
trees = [
|
||||
entry for entry in profile.get("files") or []
|
||||
if entry.get("type") == "directory"
|
||||
and str(entry.get("name")) == "hash/"
|
||||
and not entry.get("bundled")
|
||||
]
|
||||
blocks: list[str] = []
|
||||
for tree in trees:
|
||||
base = str(tree.get("path") or tree["name"])
|
||||
for name, systems in sorted(lists.items()):
|
||||
lines = [
|
||||
f" - name: {_yaml_scalar(name + '.xml')}",
|
||||
f" path: {_yaml_scalar(base + name + '.xml')}",
|
||||
]
|
||||
if tree.get("mode"):
|
||||
lines.append(f" mode: {tree['mode']}")
|
||||
lines += [
|
||||
" category: game_data",
|
||||
" required: false",
|
||||
*_system_lines(systems),
|
||||
]
|
||||
blocks.append("\n".join(lines))
|
||||
if not blocks:
|
||||
return ""
|
||||
lead = [
|
||||
SOFTLIST_SECTION,
|
||||
" # software_list_device::parse opens <list>.xml for each software list a",
|
||||
" # machine declares. Each list below is filed under the systems whose",
|
||||
" # machines declare it, a list no such machine declares stays with its",
|
||||
" # hash/ tree.",
|
||||
f" # ref: {role}src/emu/softlist_dev.cpp:281-313,",
|
||||
f" # {role}src/frontend/mame/infoxml.cpp:2184-2198",
|
||||
]
|
||||
return "\n".join(lead) + "\n\n" + "\n\n".join(blocks) + "\n"
|
||||
|
||||
|
||||
def merge_mame_support(
|
||||
profile_path: str,
|
||||
lists: dict[str, list[str]],
|
||||
samples: dict[str, list[str]],
|
||||
write: bool = False,
|
||||
) -> dict[str, list[str]]:
|
||||
"""File the software lists and the floppy samples of a MAME profile.
|
||||
|
||||
*lists* maps a software list to the systems whose machines declare it,
|
||||
for the lists the release ships. They are written as the generated
|
||||
software list section, ahead of the device section, and a refresh
|
||||
rewrites the whole section. *samples* maps a floppy sample name to the
|
||||
systems whose drives load it: each written entry named <sample>.wav
|
||||
under a samples/floppy/ path gets them, and one no system loads gets
|
||||
none.
|
||||
"""
|
||||
path = Path(profile_path)
|
||||
text = path.read_text(encoding="utf-8")
|
||||
profile = yaml_load(text) or {}
|
||||
role = mame_ref_role(profile)
|
||||
|
||||
original = text
|
||||
start = _softlist_section_start(text)
|
||||
if start is not None:
|
||||
device = _device_section_start(text)
|
||||
if device is not None and device > start:
|
||||
end = device
|
||||
else:
|
||||
end_match = _FILES_END.search(text, text.index("\n", start) + 1)
|
||||
end = end_match.start() if end_match else len(text)
|
||||
text = text[:start] + text[end:]
|
||||
section = _softlist_section(profile, lists, role)
|
||||
|
||||
head_end = generated_start(text)
|
||||
head = text if head_end is None else text[:head_end]
|
||||
written = yaml_load(head) or {}
|
||||
sample_systems: dict[str, list[str]] = {}
|
||||
for entry in written.get("files") or []:
|
||||
name = str(entry.get("name") or "")
|
||||
if "samples/floppy/" not in str(entry.get("path") or "") or not name.endswith(".wav"):
|
||||
continue
|
||||
wanted = samples.get(name[:-4], [])
|
||||
if _entry_systems(entry) != wanted:
|
||||
sample_systems[name] = wanted
|
||||
head = _rewrite_systems(head, sample_systems) if sample_systems else head
|
||||
rest = "" if head_end is None else text[head_end:]
|
||||
|
||||
if section and not rest:
|
||||
files_at = re.search(r"^files:[ \t]*$", head, re.M)
|
||||
if files_at is None or _FILES_END.search(head, files_at.end()):
|
||||
raise ValueError(
|
||||
"files: must be the last top-level key to hold a software list section"
|
||||
)
|
||||
patched = head.rstrip("\n") + "\n"
|
||||
if section:
|
||||
patched += "\n" + section
|
||||
if rest:
|
||||
patched += "\n" + rest
|
||||
|
||||
if write and patched != original:
|
||||
shutil.copy2(path, path.with_suffix(PROFILE_BACKUP_SUFFIX))
|
||||
write_text_atomic(str(path), patched)
|
||||
return {
|
||||
"lists": sorted(lists),
|
||||
"samples": sorted(sample_systems),
|
||||
"changed": [str(path)] if patched != original else [],
|
||||
}
|
||||
|
||||
|
||||
def _add_source_refs(text: str, refs: dict[str, str]) -> str:
|
||||
"""Give each named entry a source_ref line, right after its category."""
|
||||
if not refs:
|
||||
|
||||
+175
-11
@@ -10,7 +10,10 @@ at the release tag. A set whose block cannot be told apart is left without
|
||||
lines and reported.
|
||||
|
||||
The same document says which machines load each set, so a set is filed under
|
||||
the systems of those machines (attribute_systems).
|
||||
the systems of those machines (attribute_systems). It also names the software
|
||||
lists each machine declares and the floppy drives it can instantiate, which
|
||||
files hash/<list>.xml (attribute_softlists) and the floppy sound samples
|
||||
(floppy_sample_systems) the same way.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -19,7 +22,7 @@ import html
|
||||
import io
|
||||
import re
|
||||
import zipfile
|
||||
from collections.abc import Callable, Iterator
|
||||
from collections.abc import Callable, Collection, Iterable, Iterator
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
@@ -27,7 +30,7 @@ from typing import NamedTuple
|
||||
|
||||
from common import yaml_load
|
||||
|
||||
from ._hash_merge import _device_section_start
|
||||
from ._hash_merge import generated_start
|
||||
from .mame_parser import _collect_rom_macros, _expand_macros, _parse_rom_entries
|
||||
|
||||
LISTXML_CHUNK = 8 * 1024 * 1024
|
||||
@@ -40,6 +43,7 @@ _BIOSSET = re.compile(rb"<biosset\b([^>]*)>")
|
||||
_ROM = re.compile(rb"<rom\b([^>]*)>")
|
||||
_DEVICE_REF = re.compile(rb'<device_ref\b[^>]*\bname="([^"]+)"')
|
||||
_SLOT_OPTION = re.compile(rb'<slotoption\b[^>]*\bdevname="([^"]+)"')
|
||||
_SOFTWARE_LIST = re.compile(rb'<softwarelist\b[^>]*\bname="([^"]+)"')
|
||||
|
||||
_ROM_START = re.compile(r"ROM_START\s*\(\s*(\w+)\s*\)")
|
||||
_ROM_END = re.compile(r"\bROM_END\b")
|
||||
@@ -112,12 +116,15 @@ class MachineLinks(NamedTuple):
|
||||
*romof* is the parent the element names. *uses* are the devices it can
|
||||
instantiate: every subdevice of its default configuration, each device a
|
||||
selectable slot option adds, and for a device its parent ROM device,
|
||||
whose set the device's ROMs are also searched in.
|
||||
whose set the device's ROMs are also searched in. *softlists* are the
|
||||
software lists the element declares, *sourcefile* the file defining it.
|
||||
"""
|
||||
|
||||
romof: str
|
||||
isdevice: bool
|
||||
uses: tuple[str, ...]
|
||||
softlists: tuple[str, ...] = ()
|
||||
sourcefile: str = ""
|
||||
|
||||
|
||||
class Release(NamedTuple):
|
||||
@@ -134,8 +141,9 @@ def scan_release(stream: io.BufferedIOBase) -> Release:
|
||||
driver's parent (src/frontend/mame/infoxml.cpp:761-765) or a device's
|
||||
parent ROM device (856-858), device_ref every subdevice of the default
|
||||
configuration (786, 1010-1015), slotoption each selectable card of a
|
||||
slot (2129-2172). device_t::searchpath puts the parent ROM device after
|
||||
the device itself (src/emu/device.cpp:140-152).
|
||||
slot (2129-2172), softwarelist each software list device of the
|
||||
configuration (2184-2198). device_t::searchpath puts the parent ROM
|
||||
device after the device itself (src/emu/device.cpp:140-152).
|
||||
"""
|
||||
tag = ""
|
||||
devices: dict[str, dict] = {}
|
||||
@@ -162,7 +170,11 @@ def scan_release(stream: io.BufferedIOBase) -> Release:
|
||||
if isdevice and romof:
|
||||
uses.add(romof)
|
||||
uses.discard(name)
|
||||
machines[name] = MachineLinks(romof, isdevice, tuple(sorted(uses)))
|
||||
softlists = {found.decode() for found in _SOFTWARE_LIST.findall(body)}
|
||||
machines[name] = MachineLinks(
|
||||
romof, isdevice, tuple(sorted(uses)), tuple(sorted(softlists)),
|
||||
attrs.get("sourcefile", ""),
|
||||
)
|
||||
if isdevice:
|
||||
roms = _device_roms(body)
|
||||
if roms:
|
||||
@@ -187,13 +199,14 @@ def scan_device_sets(stream: io.BufferedIOBase) -> tuple[str, dict[str, dict]]:
|
||||
|
||||
|
||||
def profile_seeds(text: str) -> dict[str, str]:
|
||||
"""Set name to system, for the bios_zip entries above the device section.
|
||||
"""Set name to system, for the bios_zip entries before the generated ones.
|
||||
|
||||
These are the entries a reader wrote, the seeds attribute_systems starts
|
||||
from. The generated section is left out: its systems derive from these,
|
||||
and reading them back would carry a previous release's answer forward.
|
||||
from. The generated sections are left out: their systems derive from
|
||||
these, and reading them back would carry a previous release's answer
|
||||
forward.
|
||||
"""
|
||||
start = _device_section_start(text)
|
||||
start = generated_start(text)
|
||||
written = yaml_load(text if start is None else text[:start]) or {}
|
||||
seeds: dict[str, str] = {}
|
||||
for entry in written.get("files") or []:
|
||||
@@ -259,6 +272,157 @@ def attribute_systems(
|
||||
return {name: sorted(found) for name, found in systems.items()}
|
||||
|
||||
|
||||
def platform_seeds(
|
||||
stated: Iterable[tuple[str, str]],
|
||||
machines: dict[str, MachineLinks],
|
||||
systems: Collection[str],
|
||||
written: dict[str, str],
|
||||
) -> tuple[dict[str, str], dict[str, list[str]]]:
|
||||
"""Seeds a platform file states, and the sets it states two ways.
|
||||
|
||||
*stated* are (archive name, system) pairs a platform declares. A pair
|
||||
seeds a set when the archive is a machine of the release, not a device,
|
||||
the system is one the profile lists, and the profile has not written
|
||||
the set itself. A set stated under two of the profile's systems seeds
|
||||
nothing and is returned with them.
|
||||
"""
|
||||
found: dict[str, set[str]] = {}
|
||||
listed = set(systems)
|
||||
for archive, system in stated:
|
||||
if not archive.endswith(".zip") or system not in listed:
|
||||
continue
|
||||
name = archive[:-4]
|
||||
links = machines.get(name)
|
||||
if name in written or links is None or links.isdevice:
|
||||
continue
|
||||
found.setdefault(name, set()).add(system)
|
||||
seeds = {name: next(iter(s)) for name, s in found.items() if len(s) == 1}
|
||||
conflicts = {name: sorted(s) for name, s in found.items() if len(s) > 1}
|
||||
return seeds, conflicts
|
||||
|
||||
|
||||
def attribute_softlists(
|
||||
machines: dict[str, MachineLinks], placed: dict[str, list[str]]
|
||||
) -> dict[str, list[str]]:
|
||||
"""Systems whose machines declare each software list.
|
||||
|
||||
*placed* is what attribute_systems returns. A list goes to the systems
|
||||
of every element declaring it: a machine writes the lists of its whole
|
||||
configuration and a slot card its own (infoxml.cpp:2184-2198).
|
||||
software_list_device::parse opens <list>.xml from the hash path
|
||||
(src/emu/softlist_dev.cpp:281-313).
|
||||
"""
|
||||
lists: dict[str, set[str]] = {}
|
||||
for name, systems in placed.items():
|
||||
links = machines.get(name)
|
||||
for listed in links.softlists if links else ():
|
||||
lists.setdefault(listed, set()).update(systems)
|
||||
return {name: sorted(found) for name, found in lists.items()}
|
||||
|
||||
|
||||
# floppy_image_device adds its sound device under this shortname
|
||||
# (src/devices/imagedev/floppy.cpp:50, 2076-2082).
|
||||
FLOPPY_SOUND = "flopsnd"
|
||||
|
||||
_FORM_FACTOR = re.compile(r"\bm_form_factor\s*=\s*floppy_image::(FF_\w+)")
|
||||
_SAMPLE_FORM_FACTOR = re.compile(
|
||||
r"\bm_default_samples\.set_form_factor\s*\(\s*floppy_image::(FF_\w+)"
|
||||
r"|\bm_default_samples\.add_\w+_sample\s*\(\s*\"([^\"]+)\""
|
||||
)
|
||||
|
||||
|
||||
def default_floppy_samples(text: str) -> dict[str, str]:
|
||||
"""Each sample of floppy_sound_device's default list, to its form factor.
|
||||
|
||||
The constructor sets a form factor, then adds the samples recorded for
|
||||
it (src/devices/imagedev/floppy.cpp:1656-1686).
|
||||
"""
|
||||
body = re.search(
|
||||
r"floppy_sound_device::floppy_sound_device\s*\([^)]*\)[^{]*\{(.*?)\n\}",
|
||||
blank_comments(text),
|
||||
re.S,
|
||||
)
|
||||
samples: dict[str, str] = {}
|
||||
current = ""
|
||||
for match in _SAMPLE_FORM_FACTOR.finditer(body.group(1) if body else ""):
|
||||
if match.group(1):
|
||||
current = match.group(1)
|
||||
elif current:
|
||||
samples[match.group(2)] = current
|
||||
return samples
|
||||
|
||||
|
||||
def drive_form_factor(text: str, shortname: str) -> str:
|
||||
"""Form factor a floppy drive type sets in setup_characteristics, or ""."""
|
||||
text = blank_comments(text)
|
||||
declaration = re.search(
|
||||
r"DEFINE_DEVICE_TYPE\w*\s*\(\s*\w+\s*,\s*(\w+)\s*,\s*\"" + re.escape(shortname) + '"',
|
||||
text,
|
||||
)
|
||||
if declaration is None:
|
||||
return ""
|
||||
body = re.search(
|
||||
rf"\b{re.escape(declaration.group(1))}::setup_characteristics\s*\(\s*\)\s*\{{(.*?)\n\}}",
|
||||
text,
|
||||
re.S,
|
||||
)
|
||||
found = _FORM_FACTOR.search(body.group(1)) if body else None
|
||||
return found.group(1) if found else ""
|
||||
|
||||
|
||||
def selected_form_factor(form_factor: str, available: Collection[str]) -> str:
|
||||
"""The list floppy_sound_samples::select takes for a drive's form factor.
|
||||
|
||||
3" falls back to 3.5", anything else to 5.25", and a form factor no list
|
||||
answers takes none (src/devices/imagedev/floppy.cpp:1428-1473).
|
||||
"""
|
||||
while form_factor not in available:
|
||||
if form_factor == "FF_3":
|
||||
form_factor = "FF_35"
|
||||
elif form_factor != "FF_525":
|
||||
form_factor = "FF_525"
|
||||
else:
|
||||
return ""
|
||||
return form_factor
|
||||
|
||||
|
||||
def floppy_drives(machines: dict[str, MachineLinks]) -> list[str]:
|
||||
"""Device types that instantiate the floppy sound device."""
|
||||
return sorted(
|
||||
name for name, links in machines.items()
|
||||
if links.isdevice and FLOPPY_SOUND in links.uses
|
||||
)
|
||||
|
||||
|
||||
def floppy_sample_systems(
|
||||
machines: dict[str, MachineLinks],
|
||||
placed: dict[str, list[str]],
|
||||
samples: dict[str, str],
|
||||
form_factors: dict[str, str],
|
||||
) -> dict[str, list[str]]:
|
||||
"""Systems whose machines load each floppy sound sample.
|
||||
|
||||
Every drive starts a floppy sound device, which loads the default list
|
||||
of the form factor its drive selects (floppy.cpp:1709-1738,
|
||||
1759-1767). *samples* maps a sample to its form factor
|
||||
(default_floppy_samples), *form_factors* a drive to its own
|
||||
(drive_form_factor). A drive whose form factor is unknown is taken to
|
||||
load every list.
|
||||
"""
|
||||
available = set(samples.values())
|
||||
found: dict[str, set[str]] = {}
|
||||
for drive in floppy_drives(machines):
|
||||
systems = placed.get(drive)
|
||||
if not systems:
|
||||
continue
|
||||
own = form_factors.get(drive, "")
|
||||
chosen = {selected_form_factor(own, available)} if own else available
|
||||
for sample, form_factor in samples.items():
|
||||
if form_factor in chosen:
|
||||
found.setdefault(sample, set()).update(systems)
|
||||
return {name: sorted(s) for name, s in found.items()}
|
||||
|
||||
|
||||
def source_path(sourcefile: str) -> str:
|
||||
"""Repository path of a listxml sourcefile.
|
||||
|
||||
|
||||
@@ -23,7 +23,14 @@ from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from common import github_token, source_yaml_paths, write_text_atomic, yaml_load
|
||||
from common import (
|
||||
github_token,
|
||||
list_registered_platforms,
|
||||
load_platform_config,
|
||||
source_yaml_paths,
|
||||
write_text_atomic,
|
||||
yaml_load,
|
||||
)
|
||||
|
||||
import upstream
|
||||
|
||||
@@ -33,13 +40,21 @@ from ._hash_merge import (
|
||||
compute_diff,
|
||||
merge_mame_devices,
|
||||
merge_mame_profile,
|
||||
merge_mame_support,
|
||||
)
|
||||
from .mame_devices import (
|
||||
attribute_softlists,
|
||||
attribute_systems,
|
||||
default_floppy_samples,
|
||||
drive_form_factor,
|
||||
floppy_drives,
|
||||
floppy_sample_systems,
|
||||
locate_sources,
|
||||
open_listxml,
|
||||
platform_seeds,
|
||||
profile_seeds,
|
||||
scan_release,
|
||||
source_path,
|
||||
)
|
||||
from .mame_parser import parse_mame_source_tree
|
||||
|
||||
@@ -51,6 +66,8 @@ _CACHE_PATH = _ROOT / "data" / "mame-hashes.json"
|
||||
# release by a second run while the first was still parsing it.
|
||||
_CLONE_DIR = _ROOT / "tmp" / f"mame-{os.getpid()}"
|
||||
_EMULATORS_DIR = _ROOT / "emulators"
|
||||
_PLATFORMS_DIR = _ROOT / "platforms"
|
||||
_FLOPPY_SOURCE = "src/devices/imagedev/floppy.cpp"
|
||||
_REPO_URL = "https://github.com/mamedev/mame.git"
|
||||
_STALE_HOURS = 24
|
||||
|
||||
@@ -474,6 +491,17 @@ def _write_release(args: argparse.Namespace) -> None:
|
||||
print(f"{args.profile}: {len(profile['files'])} BIOS root sets at {args.release}")
|
||||
|
||||
|
||||
def _platform_archives() -> list[tuple[str, str]]:
|
||||
"""(file name, system) for every file a registered platform declares."""
|
||||
stated: list[tuple[str, str]] = []
|
||||
for name in list_registered_platforms(str(_PLATFORMS_DIR), include_archived=True):
|
||||
config = load_platform_config(name, str(_PLATFORMS_DIR))
|
||||
for system, data in (config.get("systems") or {}).items():
|
||||
for entry in (data or {}).get("files") or []:
|
||||
stated.append((str(entry.get("name") or ""), str(system)))
|
||||
return stated
|
||||
|
||||
|
||||
def _merge_devices(args: argparse.Namespace) -> None:
|
||||
"""Document the device ROM sets of the release mame.yml is pinned on.
|
||||
|
||||
@@ -498,15 +526,39 @@ def _merge_devices(args: argparse.Namespace) -> None:
|
||||
f"{tag} is {commit}, mame.yml upstream_commit is "
|
||||
f"{profile.get('upstream_commit')}"
|
||||
)
|
||||
unplaced = locate_sources(
|
||||
devices,
|
||||
lambda path: upstream.fetch_file(repo, commit, path, upstream.CACHE_DIR),
|
||||
|
||||
def fetch(path: str) -> list[str] | None:
|
||||
return upstream.fetch_file(repo, commit, path, upstream.CACHE_DIR)
|
||||
|
||||
unplaced = locate_sources(devices, fetch)
|
||||
written = profile_seeds(text)
|
||||
stated, conflicts = platform_seeds(
|
||||
_platform_archives(), machines, profile.get("systems") or [], written
|
||||
)
|
||||
systems = attribute_systems(machines, profile_seeds(text))
|
||||
systems = attribute_systems(machines, {**stated, **written})
|
||||
for set_name, device in devices.items():
|
||||
device["systems"] = systems.get(set_name, [])
|
||||
report = merge_mame_devices(str(profile_path), devices, write=not args.dry_run)
|
||||
|
||||
shipped = upstream.list_directory(repo, commit, "hash", upstream.CACHE_DIR)
|
||||
if shipped is None:
|
||||
raise RuntimeError(f"hash/ of {repo.slug} at {commit} could not be listed")
|
||||
present = {name[:-4] for name in shipped if name.endswith(".xml")}
|
||||
declared = attribute_softlists(machines, systems)
|
||||
lists = {name: found for name, found in declared.items() if name in present}
|
||||
floppy = fetch(_FLOPPY_SOURCE)
|
||||
if floppy is None:
|
||||
raise RuntimeError(f"{_FLOPPY_SOURCE} is absent at {commit}")
|
||||
samples = default_floppy_samples("\n".join(floppy))
|
||||
form_factors: dict[str, str] = {}
|
||||
for drive in floppy_drives(machines):
|
||||
lines = fetch(source_path(machines[drive].sourcefile))
|
||||
form_factors[drive] = drive_form_factor("\n".join(lines or []), drive)
|
||||
sample_systems = floppy_sample_systems(machines, systems, samples, form_factors)
|
||||
support = merge_mame_support(
|
||||
str(profile_path), lists, sample_systems, write=not args.dry_run
|
||||
)
|
||||
|
||||
print(f"mame-devices: {len(devices)} device ROM sets in the {tag} listxml")
|
||||
print(f" {profile_path.name}:")
|
||||
print(f" + {len(report['added'])} added")
|
||||
@@ -518,9 +570,27 @@ def _merge_devices(args: argparse.Namespace) -> None:
|
||||
print(
|
||||
f" systems: {by_count[1]} sets under one system, "
|
||||
f"{sum(n for k, n in by_count.items() if k > 1)} under several, "
|
||||
f"{by_count[0]} loaded by no machine a profile entry places "
|
||||
f"{by_count[0]} loaded by no placed machine "
|
||||
"(they serve every system of the profile)"
|
||||
)
|
||||
print(
|
||||
f" seeds: {len(written)} written in {profile_path.name}, "
|
||||
f"{len(stated)} from platform files"
|
||||
)
|
||||
for set_name, found in sorted(conflicts.items()):
|
||||
print(f" {set_name}.zip stated under {', '.join(found)}, left unseeded")
|
||||
print(
|
||||
f" software lists: {len(lists)} filed under their systems, "
|
||||
f"{len(present) - len(lists)} declared by no placed machine stay with hash/, "
|
||||
f"{len(set(declared) - present)} placed lists absent from hash/"
|
||||
)
|
||||
unread = sorted(drive for drive, found in form_factors.items() if not found)
|
||||
print(
|
||||
f" floppy samples: {len(sample_systems)} of {len(samples)} filed, "
|
||||
f"{len(support['samples'])} entries rewritten"
|
||||
)
|
||||
for drive in unread:
|
||||
print(f" {drive}: no form factor read, taken to load every list")
|
||||
if unplaced:
|
||||
print(f" {len(unplaced)} without source lines:")
|
||||
for set_name, reason in sorted(unplaced.items()):
|
||||
|
||||
@@ -713,6 +713,28 @@ def _tree_url(repo: Repo, sha: str) -> str | None:
|
||||
return f"{repo.api_base}/repos/{repo.slug}/git/trees/{sha}?recursive=1"
|
||||
|
||||
|
||||
def list_directory(
|
||||
repo: Repo, sha: str, path: str, cache_dir: str, offline: bool = False
|
||||
) -> list[str] | None:
|
||||
"""Names of the blobs directly under one directory at one revision.
|
||||
|
||||
One request for the directory alone, where the whole tree of a large
|
||||
repository comes back truncated. None when the forge has no such call
|
||||
or the directory does not exist there.
|
||||
"""
|
||||
if repo.family != "github":
|
||||
return None
|
||||
url = f"{repo.api_base}/repos/{repo.slug}/git/trees/{sha}:{path.strip('/')}"
|
||||
payload = _api(url, cache_dir, offline)
|
||||
if not isinstance(payload, dict) or payload.get("truncated"):
|
||||
return None
|
||||
return sorted(
|
||||
entry["path"]
|
||||
for entry in payload.get("tree") or []
|
||||
if isinstance(entry, dict) and entry.get("type") == "blob" and entry.get("path")
|
||||
)
|
||||
|
||||
|
||||
TREE_PAGE_SIZE = 100
|
||||
MAX_TREE_PAGES = 50
|
||||
|
||||
|
||||
+340
-1
@@ -18,22 +18,31 @@ from pathlib import Path
|
||||
from scripts.common import yaml_load
|
||||
from scripts.scraper._hash_merge import (
|
||||
DEVICE_SECTION,
|
||||
SOFTLIST_SECTION,
|
||||
mame_ref_role,
|
||||
merge_mame_devices,
|
||||
merge_mame_profile,
|
||||
merge_mame_support,
|
||||
)
|
||||
from scripts.scraper.mame_devices import (
|
||||
ABSENT,
|
||||
AMBIGUOUS,
|
||||
NO_FILE,
|
||||
MachineLinks,
|
||||
attribute_softlists,
|
||||
attribute_systems,
|
||||
default_floppy_samples,
|
||||
drive_form_factor,
|
||||
find_rom_block,
|
||||
floppy_drives,
|
||||
floppy_sample_systems,
|
||||
locate_sources,
|
||||
open_listxml,
|
||||
platform_seeds,
|
||||
profile_seeds,
|
||||
scan_device_sets,
|
||||
scan_release,
|
||||
selected_form_factor,
|
||||
source_path,
|
||||
)
|
||||
|
||||
@@ -429,7 +438,10 @@ class TheListxmlSaysWhoLoadsASet(unittest.TestCase):
|
||||
|
||||
def test_links(self):
|
||||
machines = _release().machines
|
||||
self.assertEqual(machines["mslugj"], MachineLinks("mslug", False, ("cart_dev",)))
|
||||
self.assertEqual(
|
||||
machines["mslugj"],
|
||||
MachineLinks("mslug", False, ("cart_dev",), (), "snk/neogeo.cpp"),
|
||||
)
|
||||
self.assertEqual(machines["apple2gs"].uses, ("a2diskii", "a2ssc"))
|
||||
# A device's ROMs are searched in its parent ROM device's set too.
|
||||
self.assertEqual(machines["cart_dev"].uses, ("cart_base",))
|
||||
@@ -633,6 +645,333 @@ class APickReadsTheEntrySystems(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
SUPPORT_LISTXML = b"""<?xml version="1.0"?>
|
||||
<mame build="0.289 (mame0289)" debug="no" mameconfig="10">
|
||||
\t<machine name="bbcb" sourcefile="acorn/bbcb.cpp">
|
||||
\t\t<device_ref tag=":fdc:0:525qd" name="floppy_525_qd"/>
|
||||
\t\t<softwarelist tag="flop_ls" name="bbc_flop" status="original"/>
|
||||
\t</machine>
|
||||
\t<machine name="bbcbus" sourcefile="acorn/bbcb.cpp" cloneof="bbcb" romof="bbcb">
|
||||
\t\t<softwarelist tag="flop_ls" name="bbc_flop_us" status="original"/>
|
||||
\t</machine>
|
||||
\t<machine name="apple2gs" sourcefile="apple/apple2gs.cpp">
|
||||
\t\t<slot name="sl6">
|
||||
\t\t\t<slotoption name="disk" devname="a2disk35"/>
|
||||
\t\t</slot>
|
||||
\t</machine>
|
||||
\t<machine name="a2disk35" sourcefile="devices/bus/a2bus/a2disk35.cpp" isdevice="yes" runnable="no">
|
||||
\t\t<device_ref tag=":0:35dd" name="floppy_35_dd"/>
|
||||
\t\t<softwarelist tag="flop35" name="apple2gs_flop" status="original"/>
|
||||
\t</machine>
|
||||
\t<machine name="pcw" sourcefile="amstrad/pcw.cpp">
|
||||
\t\t<device_ref tag=":0:3" name="floppy_3_sssd"/>
|
||||
\t</machine>
|
||||
\t<machine name="ibm5150" sourcefile="ibm/ibmpc.cpp">
|
||||
\t\t<device_ref tag=":0:8" name="floppy_8_sssd"/>
|
||||
\t\t<softwarelist tag="flop" name="ibm5150" status="original"/>
|
||||
\t</machine>
|
||||
\t<machine name="floppy_525_qd" sourcefile="devices/imagedev/floppy.cpp" isdevice="yes" runnable="no">
|
||||
\t\t<device_ref tag=":flopsnd" name="flopsnd"/>
|
||||
\t</machine>
|
||||
\t<machine name="floppy_35_dd" sourcefile="devices/imagedev/floppy.cpp" isdevice="yes" runnable="no">
|
||||
\t\t<device_ref tag=":flopsnd" name="flopsnd"/>
|
||||
\t</machine>
|
||||
\t<machine name="floppy_3_sssd" sourcefile="devices/imagedev/floppy.cpp" isdevice="yes" runnable="no">
|
||||
\t\t<device_ref tag=":flopsnd" name="flopsnd"/>
|
||||
\t</machine>
|
||||
\t<machine name="floppy_8_sssd" sourcefile="devices/imagedev/floppy.cpp" isdevice="yes" runnable="no">
|
||||
\t\t<device_ref tag=":flopsnd" name="flopsnd"/>
|
||||
\t</machine>
|
||||
\t<machine name="flopsnd" sourcefile="devices/imagedev/floppy.cpp" isdevice="yes" runnable="no">
|
||||
\t</machine>
|
||||
</mame>
|
||||
"""
|
||||
|
||||
SUPPORT_SEEDS = {"bbcb": "acorn-bbc-micro", "apple2gs": "apple2gs", "pcw": "pcw"}
|
||||
|
||||
FLOPPY_CPP = """\
|
||||
floppy_sound_device::floppy_sound_device(const machine_config &mconfig, const char *tag)
|
||||
\t: samples_device(mconfig, FLOPPYSOUND, tag, owner, clock),
|
||||
\tm_spin_kind(floppy_sound_samples::QUIET)
|
||||
{
|
||||
\t// m_default_samples.add_spin_sample("commented_out", 0);
|
||||
\tm_default_samples.set_form_factor(floppy_image::FF_35, "floppy");
|
||||
\tm_default_samples.add_spin_sample("35_spin", floppy_sound_samples::START_EMPTY);
|
||||
\tm_default_samples.add_seek_sample("35_seek", 2, 3);
|
||||
\tm_default_samples.set_form_factor(floppy_image::FF_525, "floppy");
|
||||
\tm_default_samples.add_step_sample("525_step");
|
||||
}
|
||||
|
||||
DEFINE_DEVICE_TYPE(FLOPPY_35_DD, floppy_35_dd, "floppy_35_dd", "3.5 inch drive")
|
||||
DEFINE_DEVICE_TYPE(FLOPPY_525_QD, floppy_525_qd, "floppy_525_qd", "5.25 inch drive")
|
||||
DEFINE_DEVICE_TYPE(FLOPPY_3_SSSD, floppy_3_sssd, "floppy_3_sssd", "3 inch drive")
|
||||
DEFINE_DEVICE_TYPE(FLOPPY_8_SSSD, floppy_8_sssd, "floppy_8_sssd", "8 inch drive")
|
||||
|
||||
void floppy_35_dd::setup_characteristics()
|
||||
{
|
||||
\tm_form_factor = floppy_image::FF_35;
|
||||
}
|
||||
|
||||
void floppy_525_qd::setup_characteristics()
|
||||
{
|
||||
\tm_form_factor = floppy_image::FF_525;
|
||||
}
|
||||
|
||||
void floppy_3_sssd::setup_characteristics()
|
||||
{
|
||||
\tm_form_factor = floppy_image::FF_3;
|
||||
}
|
||||
|
||||
void floppy_8_sssd::setup_characteristics()
|
||||
{
|
||||
\tm_form_factor = floppy_image::FF_8;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def _support():
|
||||
machines = scan_release(io.BytesIO(SUPPORT_LISTXML)).machines
|
||||
return machines, attribute_systems(machines, SUPPORT_SEEDS)
|
||||
|
||||
|
||||
class SoftwareListsFollowTheMachinesThatDeclareThem(unittest.TestCase):
|
||||
"""software_list_device::parse opens <list>.xml for each list a machine
|
||||
declares: a --system pick needs the lists of that system's machines."""
|
||||
|
||||
def test_the_scan_keeps_lists_and_source_files(self):
|
||||
machines = scan_release(io.BytesIO(SUPPORT_LISTXML)).machines
|
||||
self.assertEqual(machines["bbcb"].softlists, ("bbc_flop",))
|
||||
self.assertEqual(machines["a2disk35"].sourcefile, "devices/bus/a2bus/a2disk35.cpp")
|
||||
|
||||
def test_lists_of_machines_clones_and_slot_cards(self):
|
||||
lists = attribute_softlists(*_support())
|
||||
self.assertEqual(lists["bbc_flop"], ["acorn-bbc-micro"])
|
||||
self.assertEqual(lists["bbc_flop_us"], ["acorn-bbc-micro"])
|
||||
self.assertEqual(lists["apple2gs_flop"], ["apple2gs"])
|
||||
# No entry places the PC: its list serves no system of the profile.
|
||||
self.assertNotIn("ibm5150", lists)
|
||||
|
||||
|
||||
class FloppySamplesFollowTheDrives(unittest.TestCase):
|
||||
"""Every drive adds a floppy sound device that loads the default list of
|
||||
the form factor its drive selects (floppy.cpp:1428-1473, 1709-1738)."""
|
||||
|
||||
def test_default_list_by_form_factor(self):
|
||||
self.assertEqual(
|
||||
default_floppy_samples(FLOPPY_CPP),
|
||||
{"35_spin": "FF_35", "35_seek": "FF_35", "525_step": "FF_525"},
|
||||
)
|
||||
|
||||
def test_drive_form_factor(self):
|
||||
self.assertEqual(drive_form_factor(FLOPPY_CPP, "floppy_35_dd"), "FF_35")
|
||||
self.assertEqual(drive_form_factor(FLOPPY_CPP, "floppy_3_sssd"), "FF_3")
|
||||
self.assertEqual(drive_form_factor(FLOPPY_CPP, "missing"), "")
|
||||
|
||||
def test_select_falls_back_like_the_code(self):
|
||||
available = {"FF_35", "FF_525"}
|
||||
self.assertEqual(selected_form_factor("FF_35", available), "FF_35")
|
||||
self.assertEqual(selected_form_factor("FF_3", available), "FF_35")
|
||||
self.assertEqual(selected_form_factor("FF_8", available), "FF_525")
|
||||
self.assertEqual(selected_form_factor("FF_TWIG", available), "FF_525")
|
||||
self.assertEqual(selected_form_factor("FF_3", {"FF_525"}), "FF_525")
|
||||
self.assertEqual(selected_form_factor("FF_35", set()), "")
|
||||
|
||||
def test_samples_go_to_the_systems_of_the_drives(self):
|
||||
machines, systems = _support()
|
||||
self.assertEqual(
|
||||
floppy_drives(machines),
|
||||
["floppy_35_dd", "floppy_3_sssd", "floppy_525_qd", "floppy_8_sssd"],
|
||||
)
|
||||
samples = default_floppy_samples(FLOPPY_CPP)
|
||||
form_factors = {d: drive_form_factor(FLOPPY_CPP, d) for d in floppy_drives(machines)}
|
||||
found = floppy_sample_systems(machines, systems, samples, form_factors)
|
||||
self.assertEqual(found["35_spin"], ["apple2gs", "pcw"])
|
||||
self.assertEqual(found["525_step"], ["acorn-bbc-micro"])
|
||||
|
||||
def test_an_unread_form_factor_loads_every_list(self):
|
||||
machines, systems = _support()
|
||||
samples = default_floppy_samples(FLOPPY_CPP)
|
||||
found = floppy_sample_systems(machines, systems, samples, {})
|
||||
self.assertEqual(found["525_step"], ["acorn-bbc-micro", "apple2gs", "pcw"])
|
||||
|
||||
|
||||
class PlatformFilesSeedMachines(unittest.TestCase):
|
||||
|
||||
def test_a_platform_statement_seeds_a_machine(self):
|
||||
machines = scan_release(io.BytesIO(SUPPORT_LISTXML)).machines
|
||||
seeds, conflicts = platform_seeds(
|
||||
[
|
||||
("bbcb.zip", "acorn-bbc-micro"),
|
||||
("bbcb.zip", "bbcmicro"), # not a system of the profile
|
||||
("ibm5150.zip", "ibm-pc"), # idem
|
||||
("a2disk35.zip", "apple2gs"), # a device, not a machine
|
||||
("pcw.zip", "pcw"), # written by the profile
|
||||
("apple2gs.zip", "apple2gs"),
|
||||
("apple2gs.zip", "acorn-bbc-micro"),
|
||||
("missing.zip", "pcw"),
|
||||
("bbcb.rom", "acorn-bbc-micro"),
|
||||
],
|
||||
machines,
|
||||
["acorn-bbc-micro", "apple2gs", "pcw"],
|
||||
{"pcw": "pcw"},
|
||||
)
|
||||
self.assertEqual(seeds, {"bbcb": "acorn-bbc-micro"})
|
||||
self.assertEqual(conflicts, {"apple2gs": ["acorn-bbc-micro", "apple2gs"]})
|
||||
|
||||
|
||||
SUPPORT_PROFILE = """emulator: MAME
|
||||
source: "https://github.com/libretro/mame"
|
||||
upstream: "https://github.com/mamedev/mame"
|
||||
systems:
|
||||
- acorn-bbc-micro
|
||||
- apple2gs
|
||||
files:
|
||||
- name: "hash/"
|
||||
path: "mame/hash/"
|
||||
type: directory
|
||||
mode: libretro
|
||||
category: game_data
|
||||
required: false
|
||||
|
||||
- name: "hash/"
|
||||
path: "hash/"
|
||||
type: directory
|
||||
mode: standalone
|
||||
category: game_data
|
||||
required: false
|
||||
bundled: true
|
||||
|
||||
- name: "35_spin.wav"
|
||||
path: "mame/samples/floppy/35_spin.wav"
|
||||
mode: libretro
|
||||
category: game_data
|
||||
required: false
|
||||
size: 4
|
||||
|
||||
- name: "525_step.wav"
|
||||
path: "mame/samples/floppy/525_step.wav"
|
||||
mode: libretro
|
||||
category: game_data
|
||||
system: pcw
|
||||
required: false
|
||||
|
||||
- name: neogeo.zip
|
||||
required: true
|
||||
category: bios_zip
|
||||
system: acorn-bbc-micro
|
||||
"""
|
||||
|
||||
|
||||
class TheSupportMergeWritesListsAndSamples(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.TemporaryDirectory()
|
||||
self.path = Path(self._tmp.name) / "mame.yml"
|
||||
self.path.write_text(
|
||||
SUPPORT_PROFILE + "\n" + DEVICE_SECTION + "\n\n - name: ym2610.zip\n"
|
||||
" required: false\n category: bios_zip\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
self.lists = {"bbc_flop": ["acorn-bbc-micro"], "apple2gs_flop": ["acorn-bbc-micro", "apple2gs"]}
|
||||
self.samples = {"35_spin": ["apple2gs"]}
|
||||
|
||||
def tearDown(self):
|
||||
self._tmp.cleanup()
|
||||
|
||||
def _merge(self) -> dict:
|
||||
return merge_mame_support(str(self.path), self.lists, self.samples, write=True)
|
||||
|
||||
def _entries(self) -> list[dict]:
|
||||
return yaml_load(self.path.read_text(encoding="utf-8"))["files"]
|
||||
|
||||
def test_one_entry_per_list_under_the_delivered_tree(self):
|
||||
self._merge()
|
||||
text = self.path.read_text(encoding="utf-8")
|
||||
self.assertLess(text.index(SOFTLIST_SECTION), text.index(DEVICE_SECTION))
|
||||
lists = [e for e in self._entries() if str(e["name"]).endswith(".xml")]
|
||||
self.assertEqual(
|
||||
[(e["name"], e["mode"], e.get("system"), e.get("systems")) for e in lists],
|
||||
[
|
||||
("apple2gs_flop.xml", "libretro", None, ["acorn-bbc-micro", "apple2gs"]),
|
||||
("bbc_flop.xml", "libretro", "acorn-bbc-micro", None),
|
||||
],
|
||||
)
|
||||
self.assertFalse(any(str(e.get("path")).startswith("hash/") and e["name"] != "hash/"
|
||||
for e in self._entries()))
|
||||
self.assertIn("upstream src/emu/softlist_dev.cpp:281-313", text)
|
||||
|
||||
def test_samples_get_the_systems_of_their_drives(self):
|
||||
self._merge()
|
||||
entries = {e["name"]: e for e in self._entries()}
|
||||
self.assertEqual(entries["35_spin.wav"]["system"], "apple2gs")
|
||||
self.assertEqual(entries["35_spin.wav"]["size"], 4)
|
||||
# No drive of a profile system loads it any more: it serves them all.
|
||||
self.assertNotIn("system", entries["525_step.wav"])
|
||||
self.assertEqual(entries["neogeo.zip"]["system"], "acorn-bbc-micro")
|
||||
|
||||
def test_a_refresh_rewrites_the_section(self):
|
||||
self._merge()
|
||||
self.lists = {"bbc_flop": ["apple2gs"]}
|
||||
self._merge()
|
||||
names = [e["name"] for e in self._entries()]
|
||||
self.assertNotIn("apple2gs_flop.xml", names)
|
||||
self.assertEqual(names.count("bbc_flop.xml"), 1)
|
||||
self.assertEqual(names[-1], "ym2610.zip")
|
||||
self.lists = {}
|
||||
self._merge()
|
||||
self.assertNotIn(SOFTLIST_SECTION, self.path.read_text(encoding="utf-8"))
|
||||
|
||||
def test_a_second_run_changes_nothing(self):
|
||||
self._merge()
|
||||
first = self.path.read_text(encoding="utf-8")
|
||||
report = self._merge()
|
||||
self.assertEqual(report["changed"], [])
|
||||
self.assertEqual(report["samples"], [])
|
||||
self.assertEqual(self.path.read_text(encoding="utf-8"), first)
|
||||
|
||||
def test_new_bios_roots_and_seeds_stay_ahead_of_the_sections(self):
|
||||
self._merge()
|
||||
text = self.path.read_text(encoding="utf-8")
|
||||
self.assertEqual(profile_seeds(text), {"neogeo": "acorn-bbc-micro"})
|
||||
|
||||
def test_a_pick_takes_its_lists_and_leaves_the_others(self):
|
||||
from scripts.common import entry_serves_systems, expand_directory_entries
|
||||
|
||||
self._merge()
|
||||
profile = yaml_load(self.path.read_text(encoding="utf-8"))
|
||||
db = {"files": {}, "indexes": {"by_path_suffix": {}}}
|
||||
for index, dest in enumerate(
|
||||
["mame/hash/bbc_flop.xml", "mame/hash/apple2gs_flop.xml", "mame/hash/nes.xml"]
|
||||
):
|
||||
sha1 = f"{index:040d}"
|
||||
db["files"][sha1] = {"path": f"bios/Other/{dest}", "name": dest.rsplit("/", 1)[1]}
|
||||
db["indexes"]["by_path_suffix"][dest] = [sha1]
|
||||
files = [e for e in profile["files"] if e.get("mode") != "standalone"]
|
||||
files = expand_directory_entries(files, db)
|
||||
picked = sorted(
|
||||
str(e.get("path"))
|
||||
for e in files
|
||||
if str(e.get("path", "")).startswith("mame/hash/")
|
||||
and entry_serves_systems(e, profile, ["apple2gs"])
|
||||
)
|
||||
self.assertEqual(picked, ["mame/hash/apple2gs_flop.xml", "mame/hash/nes.xml"])
|
||||
|
||||
|
||||
class TheRepositoryProfile(unittest.TestCase):
|
||||
"""mame.yml as committed: every list filed is one the hash tree holds."""
|
||||
|
||||
def test_generated_lists_sit_inside_the_hash_tree(self):
|
||||
path = Path(__file__).resolve().parents[1] / "emulators" / "mame.yml"
|
||||
text = path.read_text(encoding="utf-8")
|
||||
start = text.find(SOFTLIST_SECTION)
|
||||
self.assertNotEqual(start, -1)
|
||||
section = yaml_load("files:\n" + text[start:text.index(DEVICE_SECTION)])["files"]
|
||||
self.assertTrue(section)
|
||||
for entry in section:
|
||||
self.assertEqual(entry["path"], "mame/hash/" + entry["name"])
|
||||
self.assertTrue(entry.get("system") or entry.get("systems"), entry["name"])
|
||||
|
||||
|
||||
class Cli(unittest.TestCase):
|
||||
|
||||
def test_devices_needs_a_listxml(self):
|
||||
|
||||
@@ -598,6 +598,36 @@ class TestCompare(unittest.TestCase):
|
||||
upstream._http_json, upstream._http_text = self._orig
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_list_directory_reads_one_tree(self):
|
||||
seen: list[str] = []
|
||||
|
||||
def answer(url):
|
||||
seen.append(url)
|
||||
return {
|
||||
"truncated": False,
|
||||
"tree": [
|
||||
{"path": "b.xml", "type": "blob"},
|
||||
{"path": "a.xml", "type": "blob"},
|
||||
{"path": "sub", "type": "tree"},
|
||||
],
|
||||
}
|
||||
|
||||
upstream._http_json = answer
|
||||
self.assertEqual(
|
||||
upstream.list_directory(self.repo, "abc", "hash/", self.dir), ["a.xml", "b.xml"]
|
||||
)
|
||||
self.assertTrue(seen[0].endswith("/repos/o/n/git/trees/abc:hash"))
|
||||
# Cached: offline, the same answer without a request.
|
||||
self.assertEqual(
|
||||
upstream.list_directory(self.repo, "abc", "hash", self.dir, offline=True),
|
||||
["a.xml", "b.xml"],
|
||||
)
|
||||
self.assertEqual(len(seen), 1)
|
||||
upstream._http_json = lambda url: {"truncated": True, "tree": []}
|
||||
self.assertIsNone(upstream.list_directory(self.repo, "def", "hash", self.dir))
|
||||
gitlab = parse_repo("https://gitlab.com/o/n")
|
||||
self.assertIsNone(upstream.list_directory(gitlab, "abc", "hash", self.dir))
|
||||
|
||||
def test_renamed_file_carries_previous_path(self):
|
||||
self.payload = {
|
||||
"files": [
|
||||
|
||||
+13
-2
@@ -117,6 +117,17 @@ and merged into a profile, on an inline listxml and driver source. Covers:
|
||||
`systems:` written and rewritten by a refresh, a written entry's system
|
||||
kept, and a `--system` pick reading the entry's own systems through the gap
|
||||
report and the core extras
|
||||
- Seeds a platform file states: a machine of the release under one of the
|
||||
profile's systems, a device, an unlisted system or a set stated two ways
|
||||
seeding nothing
|
||||
- Support files: each software list filed under the systems whose machines
|
||||
and slot cards declare it, one entry per list of the delivered `hash/`
|
||||
tree, a pick keeping its lists and the unfiled ones, floppy samples filed
|
||||
through the form factor each drive selects (3" falls back to 3.5", the
|
||||
rest to 5.25"), a refresh rewriting the section and a second run changing
|
||||
nothing
|
||||
- An entry's `system:` or `systems:` outside its profile's list refused by
|
||||
the semantic check
|
||||
|
||||
**test_fbneo_parser.** Tests the FBNeo C source parser that identifies
|
||||
`BDF_BOARDROM` sets. Same inline fixture approach.
|
||||
@@ -150,8 +161,8 @@ replaced by an injected fetch function.
|
||||
|
||||
**test_upstream.** Covers forge URL parsing for GitHub, GitLab and Forgejo,
|
||||
the content-addressed cache and its atomic write, revision and tag resolution,
|
||||
and tree comparison. The HTTP layer is replaced at module level, so nothing
|
||||
leaves the machine.
|
||||
tree comparison and the listing of one directory. The HTTP layer is replaced at
|
||||
module level, so nothing leaves the machine.
|
||||
|
||||
### Tests that read the working tree
|
||||
|
||||
|
||||
@@ -877,6 +877,23 @@ their parent ROM devices. A set loaded by machines of one system gets
|
||||
system of the profile. The driver file is never used: `neogeo.cpp` declares
|
||||
the MVS and the AES. The output counts the sets in each case.
|
||||
|
||||
A machine set no written entry lists is also seeded when a registered platform
|
||||
declares `<set>.zip` under one of the profile's systems (Batocera files
|
||||
`bbcb.zip` under `acorn-bbc-micro`). The set must be a machine of the release,
|
||||
not a device, and a set two platforms state under two different systems seeds
|
||||
nothing and is listed.
|
||||
|
||||
The same walk files the support trees. Each `hash/<list>.xml` the release
|
||||
ships and a placed machine or slot card declares (`softwarelist`) is written
|
||||
under the `Software lists (generated from MAME -listxml)` comment, ahead of the
|
||||
device section, with the systems of those machines. A list no placed machine
|
||||
declares stays a member of the `hash/` entry. Each floppy sound sample gets the
|
||||
systems whose machines can instantiate a drive selecting its list: the form
|
||||
factor comes from the drive's `setup_characteristics` in the source, 3" drives
|
||||
take the 3.5" list and every other drive the 5.25" one, as
|
||||
`floppy_sound_samples::select` does. `plugins/` stays unfiled: every machine
|
||||
starts the Lua engine.
|
||||
|
||||
A plain run (pipeline step 2a) that moves `mame.yml` to a new release
|
||||
downloads that release's `-listxml` into `.cache/dats/` and runs the device
|
||||
merge itself, so the BIOS root sets and the device sets name one release.
|
||||
|
||||
Reference in new issue
Block a user