feat: let --emulator take an alias name

--emulator mednafen_psx refused with 'use --emulator beetle_psx' although the profile names its target. The alias now selects that profile and says so on stderr, in verify, generate_pack and cross_reference alike. Launchers are still refused, and pipeline step 5 skips aliases since they compare under the profile they name.
This commit is contained in:
Abdessamad Derraz committed 2026-11-05 19:26:36 +01:00
1 parent 640331b159
commit fb9b5592a5
5 files changed
+66 -28

No files matched your search

+16 -10
View File
@@ -1870,12 +1870,12 @@ def select_emulator_profiles(
all_profiles: dict,
standalone: bool = False,
) -> list[tuple[str, dict]]:
"""Resolve named profiles, refusing the ones that answer for others.
"""Resolve named profiles, refusing the ones that answer for no core.
An alias is the same binary under another name and a launcher only
starts an emulator, so neither has requirements of its own: naming one
is a question about the wrong profile, and the error says which one to
ask instead. Raising rather than exiting lets the verifier stop and the
An alias is the same binary under another name, so naming one selects
the profile it is an alias of, said on stderr. A launcher only starts an
emulator and has no requirements of its own: naming one is a question
about the wrong profile, and the error says so. Raising rather than exiting lets the verifier stop and the
builder return empty-handed from one rule.
"""
selected: list[tuple[str, dict]] = []
@@ -1916,11 +1916,17 @@ def select_emulator_profiles(
),
None,
)
raise ProfileSelectionError(
f"{name} is an alias of {target}: use --emulator {target}"
if target
else f"{name} is an alias with no profile of its own to name"
)
if not target or target not in all_profiles:
raise ProfileSelectionError(
f"{name} is an alias with no profile of its own to name"
)
# The name a user reads in RetroArch or on the buildbot: the tool
# knows the profile, so it says which and carries on.
print(f"{name} is {target}: using its profile", file=sys.stderr)
name, profile = target, all_profiles[target]
kind = profile.get("type", "libretro")
if any(key == name for key, _ in selected):
continue
if kind == "launcher":
raise ProfileSelectionError(
f"{name} is a launcher: use the emulator it launches"
+2
View File
@@ -541,6 +541,8 @@ def check_emulator_packs(
all_ok = True
compared = 0
for name in sorted(profiles):
if profiles[name].get("type") == "alias":
continue # selects the profile it names, compared under that name
for standalone in (False, True):
try:
select_emulator_profiles([name], profiles, standalone)
+31 -10
View File
@@ -5610,12 +5610,17 @@ class ANamedPackThatIsAbsentFails(unittest.TestCase):
class AnAliasNamesItsProfile(unittest.TestCase):
""""flycast_gles2 is an alias of flycast -use --emulator flycast": the
dash read as an option, and an alias without alias_of advised
"--emulator ?"."""
class AnAliasSelectsItsProfile(unittest.TestCase):
"""`--emulator mednafen_psx` refused with "use --emulator beetle_psx",
a step the tool could take itself: the name a user reads in RetroArch
now selects the profile it is an alias of, and says so on stderr. An
alias without alias_of is found through the profile whose cores: names
it."""
def test_the_alias_selects_its_profile(self):
import contextlib as _contextlib
import io as _io
def test_the_message(self):
from scripts.common import ProfileSelectionError, select_emulator_profiles
profiles = {
@@ -5623,12 +5628,28 @@ class AnAliasNamesItsProfile(unittest.TestCase):
"flycast_gles2": {"type": "alias", "alias_of": "flycast"},
"orphan": {"type": "alias"},
"core": {"type": "libretro", "cores": ["orphan"]},
"lost": {"type": "alias"},
}
for name, expected in (("flycast_gles2", "alias of flycast: use --emulator flycast"),
("orphan", "alias of core: use --emulator core")):
with self.subTest(name=name), self.assertRaises(ProfileSelectionError) as caught:
select_emulator_profiles([name], profiles)
self.assertIn(expected, str(caught.exception))
for name, expected in (("flycast_gles2", "flycast"), ("orphan", "core")):
err = _io.StringIO()
with self.subTest(name=name), _contextlib.redirect_stderr(err):
selected = select_emulator_profiles([name], profiles)
self.assertEqual([key for key, _ in selected], [expected])
self.assertIn(f"{name} is {expected}", err.getvalue())
with _contextlib.redirect_stderr(_io.StringIO()):
both = select_emulator_profiles(["flycast", "flycast_gles2"], profiles)
self.assertEqual([key for key, _ in both], ["flycast"])
with self.assertRaises(ProfileSelectionError):
select_emulator_profiles(["lost"], profiles)
def test_verify_runs_the_alias(self):
result = subprocess.run(
[sys.executable, "scripts/verify.py", "--emulator", "mednafen_psx"],
cwd=ROOT, capture_output=True, text=True, timeout=300, check=False,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("beetle_psx", result.stdout)
self.assertIn("mednafen_psx is beetle_psx", result.stderr)
+9 -4
View File
@@ -17,7 +17,9 @@ Covers:
from __future__ import annotations
import contextlib
import hashlib
import io
import os
import shutil
import sys
@@ -1444,10 +1446,13 @@ class TestE2E(unittest.TestCase):
with self.assertRaises(SystemExit):
verify_emulator(["test_game"], self.emulators_dir, self.db, standalone=True)
def test_93_verify_emulator_alias_rejected(self):
"""Alias profile produces error with redirect message."""
with self.assertRaises(SystemExit):
verify_emulator(["test_alias"], self.emulators_dir, self.db)
def test_93_verify_emulator_alias_selects_its_profile(self):
"""An alias verifies the profile it is an alias of."""
with contextlib.redirect_stderr(io.StringIO()):
via_alias = verify_emulator(["test_alias"], self.emulators_dir, self.db)
direct = verify_emulator(["test_emu"], self.emulators_dir, self.db)
self.assertEqual(via_alias["emulators"], direct["emulators"])
self.assertEqual(via_alias["details"], direct["details"])
def test_94_verify_emulator_launcher_rejected(self):
"""Launcher profile produces error."""
+8 -4
View File
@@ -10,6 +10,8 @@ it, and these tests make the agreement checkable instead of reviewable.
from __future__ import annotations
import contextlib
import io
import json
import sys
import unittest
@@ -446,10 +448,12 @@ class OneProfileSelector(unittest.TestCase):
self.assertIsNone(error)
self.assertEqual([n for n, _p in selected], ["real"])
def test_an_alias_names_the_profile_to_ask_instead(self):
_selected, error = self._select(["ghost"])
self.assertIn("alias of real", error)
self.assertIn("--emulator real", error)
def test_an_alias_selects_the_profile_it_names(self):
with contextlib.redirect_stderr(io.StringIO()) as err:
selected, error = self._select(["ghost"])
self.assertIsNone(error)
self.assertEqual([n for n, _p in selected], ["real"])
self.assertIn("ghost is real", err.getvalue())
def test_a_launcher_is_refused(self):
_selected, error = self._select(["starter"])