Let a kind be a list, and carry the asset's catalog path

Both changes are what the third adapter asked for, the way the second one asked
for resolve_for_host.

`pick_release` now takes several kinds in order of preference. A desktop store on
Apple Silicon wants `mac_universal`, will settle for `mac_arm64`, and takes
`mac_x64` only as a last resort because that one runs under Rosetta. Release
order still wins over kind order: the newest release with any acceptable asset
beats an older release with a better one.

Records also carry `asset_path`, the catalog-side path, because not every asset
is a download — an `html` build is a hosted directory, and the desktop store
needs its URL rather than a file name.

The machine-wide uninstaller learned the third engine. It also had to stop
assuming one store per directory name: the RetroArch and desktop engines share a
store root, so a home is now identified by which engine file is in it, and the
launcher name is derived from the home rather than guessed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-18 10:00:02 +02:00
co-authored by Claude Opus 5
parent a39a29ffde
commit c4a98e5d67
2 changed files with 57 additions and 22 deletions
+20 -4
View File
@@ -10,6 +10,9 @@
# it finds every store home under the known roots, has each store's own engine
# purge what it installed, and then deletes the store.
#
# Three engines exist today — Batocera, RetroArch and desktop — and the last two
# share a store root, so a home is identified by which engine file is in it.
#
# DRY_RUN=1 print what would go, remove nothing
# KEEP_GAMES=1 leave the installed games alone, remove only the scripts
# FORCE=1 purge even while RetroArch is running
@@ -86,18 +89,31 @@ if [ -d "$BATOCERA_STORE_ROOT" ]; then
[ "$DRY_RUN" = "1" ] || rmdir "$BATOCERA_STORE_ROOT" 2>/dev/null || true
fi
# --- RetroArch stores ------------------------------------------------------
# --- RetroArch and desktop stores -----------------------------------------
# Both live under the same root, one directory per store id, told apart by the
# engine file inside. A store id can even carry both.
if [ -d "$STORE_ROOT" ]; then
extra=""
[ "$FORCE" = "1" ] && extra="--force"
for home in "$STORE_ROOT"/*/; do
[ -f "$home/config.json" ] || continue
id="$(basename "$home")"
name="$(basename "$home")"
home="${home%/}"
if [ -f "$home/retroarch_store.py" ]; then
id="${name%-retroarch}"
FOUND=$((FOUND + 1))
say "retroarch store '$id' in $home"
purge_store "${home%/}" retroarch_store.py RETROARCH_STORE_HOME "$extra"
purge_store "$home" retroarch_store.py RETROARCH_STORE_HOME "$extra"
run rm -f "$BIN_DIR/$id-retroarch-store"
run rm -rf "${home%/}"
fi
if [ -f "$home/desktop_store.py" ]; then
id="${name%-desktop}"
FOUND=$((FOUND + 1))
say "desktop store '$id' in $home"
purge_store "$home" desktop_store.py DESKTOP_STORE_HOME ""
run rm -f "$BIN_DIR/$id-desktop-store"
fi
run rm -rf "$home"
done
[ "$DRY_RUN" = "1" ] || rmdir "$STORE_ROOT" 2>/dev/null || true
fi
+30 -11
View File
@@ -28,7 +28,7 @@ import urllib.error
import urllib.parse
import urllib.request
VERSION = "1.1.0"
VERSION = "1.2.0"
# Bumped when the on-disk shape of state.json changes. v1 keyed `installed` by
# bare software name; v2 keys by `<scope>:<name>`.
@@ -390,13 +390,25 @@ def asset_basename(path):
return os.path.basename((path or "").rstrip("/"))
def pick_release(entry, kind, ext):
"""Newest non-dev release that carries the asset kind we need.
def pick_release(entry, kinds, ext):
"""Newest non-dev release that carries an asset kind we can use.
`releases` comes back from the API already sorted newest-first, and
`latestRelease` is the newest stable one — check that first, then walk the
rest so a game whose newest build is missing the asset still installs.
`kinds` may be one kind or several in order of preference. Release order
wins over kind order: the newest release that has *any* acceptable kind is
taken, and within it the most preferred kind. That is what a desktop host
wants — on Apple Silicon `mac_universal` beats `mac_x64`, which would need
Rosetta, but not at the price of installing an older release.
Returns (release, asset_name, kind, asset_path).
"""
if isinstance(kinds, str) or kinds is None:
kinds = [kinds]
kinds = [k for k in kinds if k]
candidates = []
latest = entry.get("latestRelease")
if latest:
@@ -411,14 +423,17 @@ def pick_release(entry, kind, ext):
seen.add(rid)
if str(release.get("version", "")).startswith("dev-"):
continue
for asset in release.get("assets") or []:
assets = release.get("assets") or []
for kind in kinds:
for asset in assets:
if asset.get("kind") != kind:
continue
name = asset_basename(asset.get("path", ""))
path = asset.get("path", "")
name = asset_basename(path)
if not name or (ext and not name.lower().endswith(ext.lower())):
continue
return release, name
return None, None
return release, name, kind, path
return None, None, None, None
def select_games(cfg, catalog, accept, host_info=None):
@@ -461,16 +476,17 @@ def select_games(cfg, catalog, accept, host_info=None):
skipped.append(f"{name}: {reason}")
continue
kind = resolve_for_host(spec.get("kind"), host_info)
if not kind:
wanted = resolve_for_host(spec.get("kind"), host_info)
if not wanted:
skipped.append(f"{name}: {sw.get('platform')} has no asset kind for "
f"{host_info['os']}/{host_info['arch']}")
continue
ext = resolve_for_host(spec.get("ext", ""), host_info) or ""
release, file_name = pick_release(entry, kind, ext)
release, file_name, kind, asset_path = pick_release(entry, wanted, ext)
if not release:
skipped.append(f"{name}: no '{kind}' asset in any release")
listed = wanted if isinstance(wanted, str) else "/".join(wanted)
skipped.append(f"{name}: no '{listed}' asset in any release")
continue
record = {
@@ -480,6 +496,9 @@ def select_games(cfg, catalog, accept, host_info=None):
"kind": kind,
"version": str(release.get("version", "")),
"asset": file_name,
# The catalog-side path, kept because not every asset is a download:
# an `html` build is a hosted directory, and a store may want its URL.
"asset_path": asset_path,
"title": sw.get("title") or name,
"desc": (sw.get("desc") or "").strip(),
"author": (sw.get("author") or "").strip(),