diff --git a/emulators/ymir.yml b/emulators/ymir.yml index 6f24b1b8..93b0fc7f 100644 --- a/emulators/ymir.yml +++ b/emulators/ymir.yml @@ -3,10 +3,12 @@ type: standalone + libretro core_classification: community_fork source: standalone: "https://github.com/StrikerX3/Ymir" - libretro: "https://github.com/warmenhoven/Ymir/tree/libretro" + libretro: "https://github.com/warmenhoven/Emir/tree/libretro" upstream: "https://github.com/StrikerX3/Ymir" profiled_date: "2026-03-26" -source_commit: "5e9310410baa47cb7da5b068f399c9e4a34eedbc" +source_commit: + standalone: "5e9310410baa47cb7da5b068f399c9e4a34eedbc" + libretro: "5d99cbbf813206736f38b3574ecca7a8aedc07e5" core_version: standalone: "v0.2.1" libretro: "PR#746" @@ -54,7 +56,7 @@ files: description: "Saturn IPL ROM (BIOS). Any region accepted." source_ref: standalone: "apps/ymir-sdl3/src/util/rom_loader.cpp:9-22, apps/ymir-sdl3/src/app/rom_manager.cpp:14-56, libs/ymir-core/include/ymir/sys/memory_defs.hpp:9" - libretro: "libretro.cpp:260-263,534-558" + libretro: "apps/ymir-libretro/src/libretro.cpp:260-263,534-558" - name: "cdb/cdb_rom.bin" system: sega-saturn @@ -65,7 +67,7 @@ files: description: "SH1 CD Block ROM for low-level CD drive emulation. Scans cdb/ subdirectory for any matching file." source_ref: standalone: "apps/ymir-sdl3/src/util/rom_loader.cpp:25-38, apps/ymir-sdl3/src/app/rom_manager.cpp:58-93, libs/ymir-core/include/ymir/hw/sh1/sh1_defs.hpp:7" - libretro: "libretro.cpp:567-591" + libretro: "apps/ymir-libretro/src/libretro.cpp:565-591" - name: "mpr-18811-mx.ic1" system: sega-saturn @@ -76,7 +78,7 @@ files: description: "ROM cartridge for The King of Fighters '95." source_ref: standalone: "apps/ymir-sdl3/src/app/rom_manager.cpp:96-131, libs/ymir-core/include/ymir/db/rom_cart_db.hpp:19-22" - libretro: "libretro.cpp:619-620,597-616" + libretro: "apps/ymir-libretro/src/libretro.cpp:621, 598-618" - name: "mpr-19367-mx.ic1" system: sega-saturn @@ -87,4 +89,4 @@ files: description: "ROM cartridge for Ultraman: Hikari no Kyojin Densetsu." source_ref: standalone: "apps/ymir-sdl3/src/app/rom_manager.cpp:96-131, libs/ymir-core/include/ymir/db/rom_cart_db.hpp:25-28" - libretro: "libretro.cpp:619,621,597-616" + libretro: "apps/ymir-libretro/src/libretro.cpp:622, 598-618" diff --git a/schemas/emulator.schema.json b/schemas/emulator.schema.json index e1e766ef..f05d3a27 100644 --- a/schemas/emulator.schema.json +++ b/schemas/emulator.schema.json @@ -84,10 +84,26 @@ ] }, "source_commit": { - "type": "string" + "oneOf": [ + { + "type": "string" + }, + { + "type": "object" + } + ], + "description": "Revision the profile was read at; an object keys it by build mode when the builds live in different repositories." }, "upstream_commit": { - "type": "string" + "oneOf": [ + { + "type": "string" + }, + { + "type": "object" + } + ], + "description": "Revision the profile was read at; an object keys it by build mode when the builds live in different repositories." }, "profiled_date": { "description": "Quote it: an unquoted YAML date parses to a date object.", diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index f6523d89..4086e00d 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -659,20 +659,28 @@ def select_repo(profile: dict) -> upstream.Repo | None: def resolve_pin( - profile: dict, repo, cache_dir: str, offline: bool, field: str = "source" + profile: dict, repo, cache_dir: str, offline: bool, field: str = "source", + mode: str = "", branch: str | None = None, ) -> tuple[str | None, str | None]: """Commit the profile was written at, and how it was obtained. `field` selects which declared pin applies: `source_commit` for the port the profile was read from, `upstream_commit` for the original project. + A profile whose builds live in different repositories keys its pin by + build mode, the way `ymir` keys `source` and `core_version`. """ pinned = profile.get(f"{field}_commit") + if isinstance(pinned, dict): + pinned = pinned.get(mode) + origin = f"{field}_commit[{mode}]" + else: + origin = f"{field}_commit" if pinned: - return str(pinned), f"{field}_commit" + return str(pinned), origin date = str(profile.get("profiled_date") or "") if not date: return None, None - sha = upstream.resolve_commit_at(repo, date, cache_dir, offline) + sha = upstream.resolve_commit_at(repo, date, cache_dir, offline, branch) return sha, (f"profiled_date {date}" if sha else None) @@ -689,17 +697,18 @@ def select_views( seen: set[tuple[str, str]] = set() # A port living on a branch of a fork is absent from the default tip, so # the branch it was read from is followed when the profile names one. - branch = str(profile.get("source_branch") or "") or None - for field in ("source", "upstream"): - repo = upstream.parse_repo(str(profile.get(field) or "")) + declared = str(profile.get("source_branch") or "") or None + for field, mode, url in declared_repositories(profile): + repo = upstream.parse_repo(url) if repo is None or (repo.host, repo.slug) in seen: continue - pin, origin = resolve_pin(profile, repo, cache_dir, offline, field) + branch = branch_in_url(url) or (declared if field == "source" else None) + pin, origin = resolve_pin( + profile, repo, cache_dir, offline, field, mode, branch + ) if not pin: continue - head = upstream.resolve_head( - repo, cache_dir, offline, branch if field == "source" else None - ) + head = upstream.resolve_head(repo, cache_dir, offline, branch) if not head: continue seen.add((repo.host, repo.slug)) @@ -707,6 +716,33 @@ def select_views( return views +def declared_repositories(profile: dict) -> list[tuple[str, str, str]]: + """Every repository URL the profile declares, source before upstream. + + `source` and `upstream` are usually plain strings. `ymir` keys them by + build mode instead, because its standalone and libretro builds live in + different repositories. + """ + urls: list[tuple[str, str, str]] = [] + for field in ("source", "upstream"): + value = profile.get(field) + if isinstance(value, dict): + urls.extend((field, str(k), str(v)) for k, v in value.items() if v) + elif value: + urls.append((field, "", str(value))) + return urls + + +def branch_in_url(url: str) -> str | None: + """Branch named by a forge URL pointing inside a repository. + + A profile can cite a fork as `.../Ymir/tree/libretro`, which names both + the repository and the branch the port lives on. + """ + _, sep, tail = url.partition("/tree/") + return tail.strip("/") or None if sep else None + + SELF_CHECK_CONTEXT = 2 diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index 312a1364..2c06db70 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -1242,7 +1242,7 @@ class TestRunResilience(unittest.TestCase): self._orig = profile_sync.upstream.resolve_commit_at self.seen: list[str] = [] - def flaky(repo, date, cache_dir, offline=False): + def flaky(repo, date, cache_dir, offline=False, branch=None): self.seen.append(repo.slug) if repo.slug == "o/bad": raise profile_sync.upstream.UpstreamError("HTTP 401") @@ -1265,7 +1265,7 @@ class TestRunResilience(unittest.TestCase): build_report("bad", profile, self.tmp.name) def test_rate_limit_still_propagates(self): - def limited(repo, date, cache_dir, offline=False): + def limited(repo, date, cache_dir, offline=False, branch=None): raise profile_sync.upstream.RateLimitError("HTTP 403") profile_sync.upstream.resolve_commit_at = limited