fix: type role libretro cores as libretro

2048 and gong (game), radio and video_processor (utility) and the test
cores (test) are libretro builds, but their role sat in type:, which
resolve_platform_cores filters on: all_libretro left them out, so no
platform resolved them and RetroArch's manifest named none, the
--installed-cores gap of issue 87. The role moves to role:, and type
stays libretro: type keeps saying how a profile is built and run, which
the resolver reads, while teaching the resolver that three role values
mean libretro would make type a classification again. The site and
coverage leave test cores out by role. fluidsynth, a library and no
libretro core, keeps type utility. A test keeps every buildbot core
named by RetroArch's resolution.
This commit is contained in:
Abdessamad Derraz committed 2026-11-09 23:01:22 +01:00
1 parent b4152f9aaf
commit 314b15b4ad
23 files changed
+104 -53

No files matched your search

+2 -1
View File
@@ -1,5 +1,6 @@
emulator: "00_example"
type: test
type: libretro
role: test
core_classification: pure_libretro
source: "https://github.com/libretro/libretro-core-info"
profiled_date: "2026-03-22"
+2 -1
View File
@@ -1,5 +1,6 @@
emulator: "2048"
type: game
type: libretro
role: game
core_classification: pure_libretro
source: "https://github.com/libretro/libretro-2048"
upstream: "https://github.com/gabrielecirulli/2048"
+2 -1
View File
@@ -1,5 +1,6 @@
emulator: Gong
type: game
type: libretro
role: game
core_classification: pure_libretro
source: "https://github.com/libretro/gong"
upstream: "https://danzaidan.itch.io/"
+2 -1
View File
@@ -1,5 +1,6 @@
emulator: "Radio"
type: utility
type: libretro
role: utility
core_classification: pure_libretro
source: "https://git.libretro.com/libretro/libretro-radio"
upstream: "https://github.com/fpscan/libretro-radio"
+2 -1
View File
@@ -1,5 +1,6 @@
emulator: test
type: test
type: libretro
role: test
core_classification: pure_libretro
source: "https://github.com/libretro/libretro-samples"
upstream: "https://github.com/libretro/libretro-samples"
+2 -1
View File
@@ -1,5 +1,6 @@
emulator: testgl
type: test
type: libretro
role: test
core_classification: pure_libretro
source: "https://github.com/libretro/libretro-samples"
upstream: "https://github.com/libretro/libretro-samples"
+2 -1
View File
@@ -1,5 +1,6 @@
emulator: testvulkan
type: test
type: libretro
role: test
core_classification: pure_libretro
source: "https://github.com/libretro/libretro-samples"
upstream: "https://github.com/libretro/libretro-samples"
+2 -1
View File
@@ -1,5 +1,6 @@
emulator: "V4L2"
type: utility
type: libretro
role: utility
core_classification: pure_libretro
source: "https://github.com/libretro/libretro-video-processor"
source_mirror: "https://git.libretro.com/libretro/libretro-video-processor"
+2 -1
View File
@@ -1,5 +1,6 @@
emulator: vidtest
type: test
type: libretro
role: test
core_classification: pure_libretro
source: "https://github.com/schellingb/vidtest_libretro"
profiled_date: "2026-03-25"
+8
View File
@@ -121,6 +121,14 @@
"standalone + libretro",
"alias",
"launcher",
"game",
"utility"
]
},
"role": {
"description": "What a libretro core is for when it emulates no machine: a game of its own (2048), a utility (radio, video_processor), or a test core. Descriptive: the core stays type libretro, so every reader of all_libretro reaches it, and the site and coverage leave test cores out by this field.",
"type": "string",
"enum": [
"game",
"utility",
"test"
+6 -6
View File
@@ -2209,7 +2209,7 @@ def profiles_for_systems(
wanted = {_norm_system_id(sid) for sid in system_ids}
matching = []
for name, profile in sorted(profiles.items()):
if profile.get("type") in ("launcher", "alias", "test"):
if profile.get("type") in ("launcher", "alias") or profile.get("role") == "test":
continue
if standalone and "standalone" not in profile.get("type", "libretro"):
continue
@@ -2263,7 +2263,7 @@ def select_emulator_profiles(
# and the spelling hints sent pcfx to two PlayStation cores.
claimants = sorted(
key for key in upstream_profile_index(all_profiles).get(name, ())
if all_profiles[key].get("type") not in ("launcher", "test")
if all_profiles[key].get("type") != "launcher" and all_profiles[key].get("role") != "test"
)
if len(claimants) == 1:
name = claimants[0]
@@ -2276,7 +2276,7 @@ def select_emulator_profiles(
available = sorted(
key
for key, value in all_profiles.items()
if value.get("type") not in ("alias", "test")
if value.get("type") != "alias" and value.get("role") != "test"
)
raise ProfileSelectionError(
f"emulator '{name}' not found\n"
@@ -2421,7 +2421,7 @@ def _normalize_hash_fields(node: object) -> None:
def unique_emulator_profiles(profiles: dict[str, dict]) -> dict[str, dict]:
"""Profiles counting as distinct emulators (aliases and tests excluded)."""
return {
k: v for k, v in profiles.items() if v.get("type") not in ("alias", "test")
k: v for k, v in profiles.items() if v.get("type") != "alias" and v.get("role") != "test"
}
@@ -3993,7 +3993,7 @@ def list_emulator_profiles(emulators_dir: str, skip_aliases: bool = True) -> Non
profiles = load_emulator_profiles(emulators_dir, skip_aliases=False)
for name in sorted(profiles):
p = profiles[name]
if p.get("type") in ("alias", "test"):
if p.get("type") == "alias" or p.get("role") == "test":
continue
display = p.get("emulator", name)
ptype = p.get("type", "libretro")
@@ -4014,7 +4014,7 @@ def system_index(profiles: dict) -> dict[str, tuple[str, set[str]]]:
"""
index: dict[str, tuple[str, set[str]]] = {}
for name, p in sorted(profiles.items()):
if p.get("type") in ("alias", "test", "launcher"):
if p.get("type") in ("alias", "launcher") or p.get("role") == "test":
continue
for sys_id in p.get("systems", []):
key = _norm_system_id(sys_id)
+20 -20
View File
@@ -1174,7 +1174,7 @@ def build_emulator_gap_report(
unique_profiles = {
k: v
for k, v in profiles.items()
if v.get("type") not in ("alias", "test")
if v.get("type") != "alias" and v.get("role") != "test"
}
from common import merged_standalone_set, platform_declarations
from validation import case_folded_slots
@@ -1306,7 +1306,7 @@ def _cross_reference_export_rows(coverages: dict, profiles: dict) -> list[dict]:
unique = {
key: value
for key, value in profiles.items()
if value.get("type") not in ("alias", "test")
if value.get("type") != "alias" and value.get("role") != "test"
}
rows: list[dict] = []
for platform_id, coverage in sorted(coverages.items()):
@@ -1922,7 +1922,7 @@ def system_index_items(db: dict, coverages: dict, profiles: dict) -> list[dict]:
if sha1:
held.setdefault(str(detail.get("system", "")), set()).add(sha1)
for profile in profiles.values():
if profile.get("type") in ("alias", "test"):
if profile.get("type") == "alias" or profile.get("role") == "test":
continue
own = [str(s) for s in profile.get("systems") or []]
for entry in profile.get("files") or []:
@@ -1973,7 +1973,7 @@ def _first_file_place(
return parts[1], _site_path(f"systems/{page}.md#sha1-{sha1[:12]}")
for key in sorted(profiles):
profile = profiles[key]
if profile.get("type") not in ("alias", "test") and sys_id in (profile.get("systems") or []):
if profile.get("type") != "alias" and profile.get("role") != "test" and sys_id in (profile.get("systems") or []):
return "", _site_path(f"emulators/{key}.md")
return "", None
@@ -1988,7 +1988,7 @@ def core_index_items(profiles: dict) -> list[dict]:
"""
from common import preferred_profile, upstream_profile_index
published = {name for name, p in profiles.items() if p.get("type") not in ("alias", "test")}
published = {name for name, p in profiles.items() if p.get("type") != "alias" and p.get("role") != "test"}
index = upstream_profile_index(profiles)
for name, profile in profiles.items():
target = str(profile.get("alias_of") or "")
@@ -3602,7 +3602,7 @@ def _emulator_named_link(key: str, prefix: str = "") -> str:
profile with no page (an alias, a test) is named only."""
profile = _profiles_on_disk().get(key, {})
label = _escape_tags(str(profile.get("emulator") or key))
if profile.get("type") in ("alias", "test"):
if profile.get("type") == "alias" or profile.get("role") == "test":
return label
return f"[{label}]({prefix}emulators/{key}.md)"
@@ -4087,7 +4087,7 @@ def _profiles_by_core_name() -> dict[str, list[str]]:
@functools.lru_cache(maxsize=None)
def _core_names_in(emulators_dir: str) -> dict[str, list[str]]:
profiles = _profiles_in(emulators_dir, skip_aliases=True)
with_page = {k for k, v in profiles.items() if v.get("type") not in ("alias", "test")}
with_page = {k for k, v in profiles.items() if v.get("type") != "alias" and v.get("role") != "test"}
return {
core: sorted(k for k in keys if k in with_page)
for core, keys in upstream_profile_index(profiles).items()
@@ -4920,7 +4920,7 @@ def _data_member_owners(profiles: dict) -> dict[str, str]:
"""
owners: dict[str, str] = {}
for name in sorted(profiles):
if profiles[name].get("type") in ("alias", "test"):
if profiles[name].get("type") == "alias" or profiles[name].get("role") == "test":
continue
for tree in page_data_directories(name, profiles):
owners.setdefault(str(tree.get("ref", "")), name)
@@ -5145,7 +5145,7 @@ def file_lookup_records(
**({"sha1": archive} if archive else {}),
})
for key, profile in sorted(profiles.items()):
if profile.get("type") in ("alias", "test"):
if profile.get("type") == "alias" or profile.get("role") == "test":
continue
entries = profile.get("files") or []
own_systems = [str(s) for s in profile.get("systems") or []]
@@ -5396,11 +5396,11 @@ def _folder_emulator(folder: str) -> tuple[str, str]:
named after, by its profile key or its name, else the folder and "".
"""
profiles = _profiles_on_disk()
if folder in profiles and profiles[folder].get("type") not in ("alias", "test"):
if folder in profiles and profiles[folder].get("type") != "alias" and profiles[folder].get("role") != "test":
return str(profiles[folder].get("emulator") or folder), folder
for key in sorted(profiles):
profile = profiles[key]
if profile.get("type") in ("alias", "test"):
if profile.get("type") == "alias" or profile.get("role") == "test":
continue
names = {str(profile.get(field) or "").casefold() for field in ("emulator", "display_name")}
if folder.casefold() in names:
@@ -7857,7 +7857,7 @@ def emulator_initial(display: str) -> str:
def generate_emulators_index(profiles: dict) -> str:
unique = {
k: v for k, v in profiles.items()
if v.get("type") not in ("alias", "test")
if v.get("type") != "alias" and v.get("role") != "test"
}
aliases = {k: v for k, v in profiles.items() if v.get("type") == "alias"}
@@ -8660,7 +8660,7 @@ def emulator_romset(profile: dict) -> str:
1.38.1 on MAME 0.289), else the version of a MAME build or of a
FinalBurn core, whose sets follow its own release.
"""
if profile.get("type") in ("alias", "launcher", "test"):
if profile.get("type") in ("alias", "launcher") or profile.get("role") == "test":
return ""
emulator = str(profile.get("emulator", ""))
version = str(profile.get("core_version") or "")
@@ -8939,7 +8939,7 @@ def emulator_install_route(name: str, profile: dict) -> tuple[str, list[str], tu
the systems both run on: RetroArch's list names AetherSX2, and the
command filled RetroArch's system folder, which the app never reads.
"""
if profile.get("type") in ("alias", "launcher", "test"):
if profile.get("type") in ("alias", "launcher") or profile.get("role") == "test":
return "", [], ()
if not [f for f in profile.get("files") or [] if isinstance(f, dict) and not comes_with_app(f)]:
return "", [], ()
@@ -9873,7 +9873,7 @@ def plan_emulator_pages(profiles: dict,
``available`` is the test the pages use for a held file."""
stems = set()
for name, profile in sorted(profiles.items()):
if profile.get("type") in ("alias", "test"):
if profile.get("type") == "alias" or profile.get("role") == "test":
continue
files = profile.get("files") or []
anchors = emulator_file_anchors(files)
@@ -11046,7 +11046,7 @@ def generate_gap_analysis(
unique_profiles = {
k: v
for k, v in profiles.items()
if v.get("type") not in ("alias", "test")
if v.get("type") != "alias" and v.get("role") != "test"
}
relevant_set: set[str] = set()
for _name, cov in coverages.items():
@@ -11139,7 +11139,7 @@ def generate_gap_analysis(
unique_profiles = {
k: v
for k, v in profiles.items()
if v.get("type") not in ("alias", "test")
if v.get("type") != "alias" and v.get("role") != "test"
}
for pname in coverages:
config = coverages[pname]["config"]
@@ -11547,7 +11547,7 @@ def generate_cross_reference(
``moved`` collects one page per platform with its core table.
"""
unique = {
k: v for k, v in profiles.items() if v.get("type") not in ("alias", "test")
k: v for k, v in profiles.items() if v.get("type") != "alias" and v.get("role") != "test"
}
total_cores = len(unique)
@@ -12591,7 +12591,7 @@ def _generate(args: argparse.Namespace) -> None:
_DATA_MEMBERS.update(data_directory_members())
_DATA_REVISIONS.update(data_directory_revisions())
for key in sorted(profiles):
if profiles[key].get("type") in ("alias", "test"):
if profiles[key].get("type") == "alias" or profiles[key].get("role") == "test":
continue
display = str(profiles[key].get("emulator", key))
for tree in page_data_directories(key, profiles):
@@ -12607,7 +12607,7 @@ def _generate(args: argparse.Namespace) -> None:
public_profiles = {
name: profile
for name, profile in profiles.items()
if profile.get("type") not in ("alias", "test")
if profile.get("type") != "alias" and profile.get("role") != "test"
}
(public / "api" / "v1" / "emulators").mkdir(parents=True, exist_ok=True)
for name, profile in public_profiles.items():
+1 -1
View File
@@ -627,7 +627,7 @@ def check_emulator_packs(
systems = sorted({
_norm_system_id(str(sid))
for profile in profiles.values()
if profile.get("type") not in ("launcher", "alias", "test")
if profile.get("type") not in ("launcher", "alias") and profile.get("role") != "test"
for sid in profile.get("systems") or []
})
systems_ok = True
+2 -2
View File
@@ -4453,7 +4453,7 @@ def select_profiles(profiles: dict, args: argparse.Namespace) -> dict:
# alias and test profiles too, "lists all 553" pointed at 530 lines.
listed = sorted(
name for name, profile in profiles.items()
if profile.get("type") not in ("alias", "test")
if profile.get("type") != "alias" and profile.get("role") != "test"
)
raise SystemExit(
f"ERROR: unknown profile {', '.join(unknown)}. "
@@ -4466,7 +4466,7 @@ def select_profiles(profiles: dict, args: argparse.Namespace) -> dict:
wanted_system = _norm_system_id(args.system) if args.system else None
selected = {}
for name, profile in sorted(profiles.items()):
if profile.get("type") in ("alias", "test"):
if profile.get("type") == "alias" or profile.get("role") == "test":
continue
if wanted_system and wanted_system not in {
_norm_system_id(str(s)) for s in profile.get("systems") or []
+1 -1
View File
@@ -97,7 +97,7 @@ def _engine_sections(manufacturers: dict[str, dict[str, list]]) -> dict[str, dic
from siterender import _PROGRAM_TOPS, SECTION_LABELS, record_card_move # noqa: PLC0415
profiles = {key: profile for key, profile in _profiles_on_disk().items()
if profile.get("type") not in ("alias", "test")}
if profile.get("type") != "alias" and profile.get("role") != "test"}
readers: dict[str, set[str]] = {}
for key, profile in profiles.items():
for entry in profile.get("files") or []:
+1 -1
View File
@@ -423,7 +423,7 @@ def _build_boards() -> None:
ids_by_name: dict[str, set[str]] = {}
for key in sorted(profiles):
profile = profiles[key]
if profile.get("type") == "test":
if profile.get("role") == "test":
continue
systems = [str(sys_id) for sys_id in profile.get("systems") or []]
for entry in profile.get("files") or []:
+4 -4
View File
@@ -21,10 +21,10 @@ from common import load_emulator_profiles # noqa: E402
STEM_NAMED = {
# Template and test cores, no emulator page.
"00_example": "template",
"test": "type test",
"testgl": "type test",
"testvulkan": "type test",
"vidtest": "type test",
"test": "role test",
"testgl": "role test",
"testvulkan": "role test",
"vidtest": "role test",
# Aliases, no emulator page.
"ep128emu": "alias",
"mednafen_gba": "alias",
+1 -1
View File
@@ -30,7 +30,7 @@ class SiteCoreCounts(unittest.TestCase):
def test_each_platform_count_is_the_resolved_set(self):
platforms = str(REPO_ROOT / "platforms")
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
unique = {k: v for k, v in profiles.items() if v.get("type") not in ("alias", "test")}
unique = {k: v for k, v in profiles.items() if v.get("type") != "alias" and v.get("role") != "test"}
coverages = {}
for name in list_registered_platforms(platforms):
config = load_platform_config(name, platforms)
+3 -3
View File
@@ -499,7 +499,7 @@ class TheTemplateIsNoEmulator(unittest.TestCase):
self.assertNotIn("00_example", index)
self.assertIn("melonds.md", index)
# A test profile gets no page at all (generate_site's public_profiles).
self.assertEqual(_profile("00_example")["type"], "test")
self.assertEqual(_profile("00_example")["role"], "test")
@unittest.skipUnless((DIST / "emulators" / "aethersx2" / "index.html").is_file(), "site not built")
@@ -517,7 +517,7 @@ class TheBuiltEmulatorPages(unittest.TestCase):
self.assertNotIn("RetroArch", page.split("Install this emulator", 1)[1].split("</details>", 1)[0])
def test_the_index_leaves_the_template_out(self):
# 00_example is `type: test`: no page, no index row.
# 00_example has `role: test`: no page, no index row.
self.assertNotIn("00_example", self._html("emulators"))
self.assertIn('data-pagefind-body', self._html("emulators/melonds"))
self.assertFalse((DIST / "emulators" / "00_example").exists())
@@ -630,7 +630,7 @@ class DataFolderPrinted(unittest.TestCase):
if not _generated():
self.skipTest("site pages not generated")
for name, profile in _profiles_with_data_root().items():
if profile.get("type") in ("alias", "test"):
if profile.get("type") == "alias" or profile.get("role") == "test":
continue
page = _built_page(name)
with self.subTest(profile=name):
+1 -1
View File
@@ -55,7 +55,7 @@ class EngineIntros(unittest.TestCase):
def test_every_intro_names_what_it_emulates(self):
for path in sorted(glob.glob(str(REPO_ROOT / "emulators" / "*.yml"))):
profile = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
if not isinstance(profile, dict) or profile.get("type") in ("alias", "test"):
if not isinstance(profile, dict) or profile.get("type") == "alias" or profile.get("role") == "test":
continue
name = Path(path).stem
with self.subTest(profile=name):
+1 -1
View File
@@ -747,7 +747,7 @@ class AnIdLeadsToItsData(unittest.TestCase):
"pcsx2": {"type": "standalone"},
"lrps2": {"type": "libretro", "cores": ["pcsx2"]},
"mednafen_psx_alias": {"type": "alias", "alias_of": "beetle_psx"},
"probe": {"type": "test", "cores": ["probe_core"]},
"probe": {"type": "libretro", "role": "test", "cores": ["probe_core"]},
}
items = generate_site.core_index_items(profiles)
by_name: dict[str, list[tuple[str, bool]]] = {}
+30
View File
@@ -21,6 +21,36 @@ sys.path.insert(0, str(REPO_ROOT / "scripts"))
from common import canonical_target_name # noqa: E402
class EveryBuildbotCoreIsNamed(unittest.TestCase):
"""A core RetroArch installs that its manifest does not name is "a core
this list does not know" to --installed-cores (issue 87). 2048, gong,
radio, video_processor and test were typed by their role, which hid
their libretro build from all_libretro."""
def test_retroarch_names_every_buildbot_core(self):
import yaml # noqa: PLC0415
from common import ( # noqa: PLC0415
load_emulator_profiles,
load_platform_config,
load_target_config,
resolve_platform_cores,
upstream_profile_index,
)
platforms = str(REPO_ROOT / "platforms")
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"))
runs = resolve_platform_cores(load_platform_config("retroarch", platforms), profiles)
index = upstream_profile_index(profiles, include_aliases=True)
targets = yaml.safe_load((REPO_ROOT / "platforms" / "targets" / "retroarch.yml").read_text())
names = {
name for target in targets["targets"]
for name in load_target_config("retroarch", target, platforms) or ()
}
unnamed = sorted(name for name in names if not (index.get(name, set()) & runs))
self.assertEqual(unnamed, [])
class AliasesResolveToOneName(unittest.TestCase):
def test_retroarch_switch_aliases(self):
platforms = str(REPO_ROOT / "platforms")
+7 -3
View File
@@ -365,9 +365,12 @@ A few field conventions that protect the toolchain:
- `type:` is operational. `resolve_platform_cores()` uses it to filter
which profiles apply to a platform. Valid values are `libretro`,
`standalone + libretro`, `standalone`, `alias`, `launcher`, `game`,
`utility`, `test`. Putting a classification concept here (like
`utility`. Putting a classification concept here (like
"bizhawk-native") breaks the filtering. A BizHawk core is
`type: standalone`.
`type: standalone`. A libretro core that emulates no machine stays
`type: libretro` and says what it is for in `role:` (`game`, `utility`
or `test`): typed by its role, 2048 fell out of every all_libretro
platform and RetroArch's manifest no longer named it.
- `core_classification:` is descriptive. It documents the relationship
between the core and the original emulator (pure_libretro,
@@ -513,7 +516,8 @@ reports the profile as uncheckable, and the CI job that runs
| Field | Required | Description |
|-------|----------|-------------|
| `emulator` | yes | display name |
| `type` | yes | `libretro`, `standalone`, `standalone + libretro`, `alias`, `launcher`, `game`, `utility`, `test` |
| `type` | yes | `libretro`, `standalone`, `standalone + libretro`, `alias`, `launcher`, `game`, `utility` |
| `role` | no | `game`, `utility`, `test`: what a libretro core that emulates no machine is for. The site and coverage leave `test` cores out |
| `core_classification` | no | `pure_libretro`, `official_port`, `community_fork`, `frozen_snapshot`, `enhanced_fork`, `game_engine`, `embedded_hle`, `launcher`, `alias`, `other` |
| `source` | no | libretro core repository URL. A dict when the core ships under several repos |
| `upstream` | no | original emulator repository URL |