Files
warpstore/README.md
T
mr.zeroandClaude Opus 5 9c185e8fa9 Keep the titles a store cannot install
`select_games` answers what a store can install and throws the rest away, which is fine
for installing and wrong for showing: a client that hides what it cannot install leaves
the visitor wondering whether the catalog is small or their machine is unusual.

`survey_catalog` is the same walk with those titles kept — one record per title, with a
reason code (platform_off, host_asset, no_asset, vetoed), a sentence, and enough of the
catalog entry to draw a card. `select_games` is now a two-value wrapper around it, so an
adapter that only installs needs no change; the Batocera and RetroArch stores are
untouched.

Titles the store *chooses* not to offer stay invisible: the wrong status or an
only/exclude list is editorial, not a limitation of the machine. A platform switched off
is reported, because to somebody reading a catalog it says "not supported here".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:45:09 +02:00

7.8 KiB

warpstore — the shared core of the WarpEngine store engines

A store engine reads a WarpEngine catalog and writes some host platform's own game library format. Only that last step differs between hosts. Everything before it — talking to the API, picking which release to install, matching the machine, remembering what was put where — is this module.

                    ┌──────────────────────────────┐
                    │  warpstore.py                │
                    │  catalog · releases · host   │
                    │  state · config · HTTP       │
                    └──────────────┬───────────────┘
                    ┌──────────────┴───────────────┐
                    ▼                              ▼
    warp-engine-batocera-store        warp-engine-retroarch-store
    ROM folders + gamelist.xml        .lpl playlists + thumbnails
                    │                              │
                    ▼                              ▼
             EmulationStation                  RetroArch

Single file, Python 3 standard library only. The hosts range from a Batocera box (python3, no pip) to a desktop, and neither may be asked to install anything. An engine's installer drops warpstore.py next to the adapter script and the adapter does import warpstore.

What an adapter has to supply

Three things, and nothing else:

  1. A DEFAULT_CONFIG describing its host — where things live, which catalog platform maps to what.
  2. An accept(spec, sw) callback deciding which catalog entries that host can run, and how they are grouped.
  3. The code that writes the host's library format.
import warpstore as ws

def accept(spec, sw):
    """Return (scope, extras), or raise ws.Skip(reason) to reject the title."""
    if spec["system"] not in available_systems:
        raise ws.Skip(f"no '{spec['system']}' ROM folder on this box")
    return spec["system"], {"system": spec["system"]}

cfg = ws.load_config(path, DEFAULT_CONFIG, required=("paths.subfolder",))
ws.init(path)
games, skipped = ws.select_games(cfg, ws.fetch_catalog(cfg, use_cache=True), accept)

scope is how the host groups its library — a Batocera system, a RetroArch playlist. It is what state.json is keyed by (<scope>:<name>), so two hosts can carry a game of the same name without colliding.

Listing what cannot be installed

select_games answers what a store can install. survey_catalog answers the same question and keeps the rest, which is what a client needs to show a catalog honestly:

games, skipped, unavailable = ws.survey_catalog(cfg, catalog, accept)

Each unavailable record carries name, title, platform, desc, author, image_url, the latest version, a reason code and a detail sentence. The codes:

Code Meaning
platform_off this store does not carry that platform at all
host_asset the platform has no asset kind for this OS and architecture
no_asset no release carries the asset it would need
vetoed the adapter's accept refused it

Titles the store chooses not to offer — the wrong status, an only/exclude list — are in none of the three lists: that is editorial, not a limitation of the machine. A platform switched off is reported, because to somebody looking at a catalog it reads as "not supported here". select_games is now a two-value wrapper around this, so an adapter that only installs needs no change.

What is in here

Area Functions
Logging set_tag, set_verbose, log, debug, die
Files load_json, write_json, write_atomic, within, prune_empty_dirs
Config deep_merge, load_config
Store home init, then HOME, CONFIG_PATH, STATE_PATH, CATALOG_CACHE
HTTP http_get, http_download, api_url, download_url, user_agent
Host machine, host_os, host, resolve_for_host
Catalog fetch_catalog, select_games, survey_catalog, pick_release, asset_basename, download_image
State load_state, save_state, game_key, record_scope, records_by_scope, match_keys, limit_to_names

Four of them carry decisions worth knowing about.

resolve_for_host(value, host) — a config value that may depend on the machine. A plain string is the same everywhere (that is what a cartridge is: data for an emulator). Where it differs, the value is a map and the most specific key wins: {"linux-aarch64": …, "aarch64": …, "linux": …, "*": …}. With no matching key and no * the answer is None, and the caller reports the title as skipped rather than installing something that cannot run.

within(path, root) — every delete a store performs is guarded by this. A store may only remove files from the subtree it owns, never from the user's own library and never from another store's.

prune_empty_dirs(dirs, root) — the uninstall side of the same rule. A store that has removed everything should not leave its folders behind, but only empty directories go, and only inside root: one surprise file is enough to keep a directory.

write_atomic — nothing is ever written in place. A store interrupted mid-sync would otherwise leave a half-written playlist or gamelist, which is worse than an old one.

Taking every store off a machine

curl -fsSL https://git.teletypegames.org/engines/warpstore/raw/branch/master/uninstall.sh | sh

A single store is better removed by its own engine's uninstall.sh, which knows that host's extras — a Batocera Ports entry, say. This one is for taking the whole framework off a machine without having to remember what is installed: it finds every store home under the known roots, has each store's own engine purge what it installed, and then deletes the store, its launcher and the engine files.

Environment variable Meaning
DRY_RUN 1 to print what would go and remove nothing
KEEP_GAMES 1 to remove only the scripts and leave the installed games
FORCE 1 to purge even while RetroArch is running
BATOCERA_STORE_ROOT, BATOCERA_PORTS_DIR where to look on a Batocera box
STORE_ROOT, BIN_DIR where to look on a desktop

If any engine cannot finish — RetroArch running, a ROMs root unmounted — the run stops there rather than deleting the engine that knows what it installed.

The script is POSIX sh, not bash, so | sh works on a machine whose /bin/sh is dash. The engines' own installers and uninstallers are too.

State

state.json, version 2, keyed <scope>:<name>:

{
  "version": 2,
  "installed": {
    "c64:blessingofra": { "name": "blessingofra", "scope": "c64", "asset": "blessingofra-2.0.0.prg", "…": "…" }
  }
}

record_scope() falls back to a record's system field, which is what the Batocera store wrote before this module existed — and there the two were the same string, so an installed box keeps working with no migration. A version: 1 file, keyed by bare software name, is re-keyed on first run.

Users

Both pin nothing: they fetch warpstore.py from this repository's master at install time. A change here therefore reaches every store on its next install — which is the point, and also the reason to keep the surface above small.