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:
+23
-7
@@ -10,6 +10,9 @@
|
|||||||
# it finds every store home under the known roots, has each store's own engine
|
# it finds every store home under the known roots, has each store's own engine
|
||||||
# purge what it installed, and then deletes the store.
|
# 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
|
# DRY_RUN=1 print what would go, remove nothing
|
||||||
# KEEP_GAMES=1 leave the installed games alone, remove only the scripts
|
# KEEP_GAMES=1 leave the installed games alone, remove only the scripts
|
||||||
# FORCE=1 purge even while RetroArch is running
|
# 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
|
[ "$DRY_RUN" = "1" ] || rmdir "$BATOCERA_STORE_ROOT" 2>/dev/null || true
|
||||||
fi
|
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
|
if [ -d "$STORE_ROOT" ]; then
|
||||||
extra=""
|
extra=""
|
||||||
[ "$FORCE" = "1" ] && extra="--force"
|
[ "$FORCE" = "1" ] && extra="--force"
|
||||||
for home in "$STORE_ROOT"/*/; do
|
for home in "$STORE_ROOT"/*/; do
|
||||||
[ -f "$home/config.json" ] || continue
|
[ -f "$home/config.json" ] || continue
|
||||||
id="$(basename "$home")"
|
name="$(basename "$home")"
|
||||||
FOUND=$((FOUND + 1))
|
home="${home%/}"
|
||||||
say "retroarch store '$id' in $home"
|
if [ -f "$home/retroarch_store.py" ]; then
|
||||||
purge_store "${home%/}" retroarch_store.py RETROARCH_STORE_HOME "$extra"
|
id="${name%-retroarch}"
|
||||||
run rm -f "$BIN_DIR/$id-retroarch-store"
|
FOUND=$((FOUND + 1))
|
||||||
run rm -rf "${home%/}"
|
say "retroarch store '$id' in $home"
|
||||||
|
purge_store "$home" retroarch_store.py RETROARCH_STORE_HOME "$extra"
|
||||||
|
run rm -f "$BIN_DIR/$id-retroarch-store"
|
||||||
|
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
|
done
|
||||||
[ "$DRY_RUN" = "1" ] || rmdir "$STORE_ROOT" 2>/dev/null || true
|
[ "$DRY_RUN" = "1" ] || rmdir "$STORE_ROOT" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|||||||
+34
-15
@@ -28,7 +28,7 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
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
|
# Bumped when the on-disk shape of state.json changes. v1 keyed `installed` by
|
||||||
# bare software name; v2 keys by `<scope>:<name>`.
|
# bare software name; v2 keys by `<scope>:<name>`.
|
||||||
@@ -390,13 +390,25 @@ def asset_basename(path):
|
|||||||
return os.path.basename((path or "").rstrip("/"))
|
return os.path.basename((path or "").rstrip("/"))
|
||||||
|
|
||||||
|
|
||||||
def pick_release(entry, kind, ext):
|
def pick_release(entry, kinds, ext):
|
||||||
"""Newest non-dev release that carries the asset kind we need.
|
"""Newest non-dev release that carries an asset kind we can use.
|
||||||
|
|
||||||
`releases` comes back from the API already sorted newest-first, and
|
`releases` comes back from the API already sorted newest-first, and
|
||||||
`latestRelease` is the newest stable one — check that first, then walk the
|
`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.
|
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 = []
|
candidates = []
|
||||||
latest = entry.get("latestRelease")
|
latest = entry.get("latestRelease")
|
||||||
if latest:
|
if latest:
|
||||||
@@ -411,14 +423,17 @@ def pick_release(entry, kind, ext):
|
|||||||
seen.add(rid)
|
seen.add(rid)
|
||||||
if str(release.get("version", "")).startswith("dev-"):
|
if str(release.get("version", "")).startswith("dev-"):
|
||||||
continue
|
continue
|
||||||
for asset in release.get("assets") or []:
|
assets = release.get("assets") or []
|
||||||
if asset.get("kind") != kind:
|
for kind in kinds:
|
||||||
continue
|
for asset in assets:
|
||||||
name = asset_basename(asset.get("path", ""))
|
if asset.get("kind") != kind:
|
||||||
if not name or (ext and not name.lower().endswith(ext.lower())):
|
continue
|
||||||
continue
|
path = asset.get("path", "")
|
||||||
return release, name
|
name = asset_basename(path)
|
||||||
return None, None
|
if not name or (ext and not name.lower().endswith(ext.lower())):
|
||||||
|
continue
|
||||||
|
return release, name, kind, path
|
||||||
|
return None, None, None, None
|
||||||
|
|
||||||
|
|
||||||
def select_games(cfg, catalog, accept, host_info=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}")
|
skipped.append(f"{name}: {reason}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
kind = resolve_for_host(spec.get("kind"), host_info)
|
wanted = resolve_for_host(spec.get("kind"), host_info)
|
||||||
if not kind:
|
if not wanted:
|
||||||
skipped.append(f"{name}: {sw.get('platform')} has no asset kind for "
|
skipped.append(f"{name}: {sw.get('platform')} has no asset kind for "
|
||||||
f"{host_info['os']}/{host_info['arch']}")
|
f"{host_info['os']}/{host_info['arch']}")
|
||||||
continue
|
continue
|
||||||
ext = resolve_for_host(spec.get("ext", ""), host_info) or ""
|
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:
|
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
|
continue
|
||||||
|
|
||||||
record = {
|
record = {
|
||||||
@@ -480,6 +496,9 @@ def select_games(cfg, catalog, accept, host_info=None):
|
|||||||
"kind": kind,
|
"kind": kind,
|
||||||
"version": str(release.get("version", "")),
|
"version": str(release.get("version", "")),
|
||||||
"asset": file_name,
|
"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,
|
"title": sw.get("title") or name,
|
||||||
"desc": (sw.get("desc") or "").strip(),
|
"desc": (sw.get("desc") or "").strip(),
|
||||||
"author": (sw.get("author") or "").strip(),
|
"author": (sw.get("author") or "").strip(),
|
||||||
|
|||||||
Reference in New Issue
Block a user