From c4a98e5d67d9c182fb52aae83eb267a98b25eadf Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Tue, 18 Aug 2026 10:00:02 +0200 Subject: [PATCH] Let a kind be a list, and carry the asset's catalog path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- uninstall.sh | 30 +++++++++++++++++++++++------- warpstore.py | 49 ++++++++++++++++++++++++++++++++++--------------- 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/uninstall.sh b/uninstall.sh index ac90f3d..b936818 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -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")" - FOUND=$((FOUND + 1)) - say "retroarch store '$id' in $home" - purge_store "${home%/}" retroarch_store.py RETROARCH_STORE_HOME "$extra" - run rm -f "$BIN_DIR/$id-retroarch-store" - run rm -rf "${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" + 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 [ "$DRY_RUN" = "1" ] || rmdir "$STORE_ROOT" 2>/dev/null || true fi diff --git a/warpstore.py b/warpstore.py index 4895af8..b582212 100644 --- a/warpstore.py +++ b/warpstore.py @@ -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 `:`. @@ -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 []: - if asset.get("kind") != kind: - continue - name = asset_basename(asset.get("path", "")) - if not name or (ext and not name.lower().endswith(ext.lower())): - continue - return release, name - return None, None + assets = release.get("assets") or [] + for kind in kinds: + for asset in assets: + if asset.get("kind") != kind: + continue + path = asset.get("path", "") + name = asset_basename(path) + 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): @@ -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(),