Commit Graph
4 Commits
Author SHA1 Message Date
mr.zeroandClaude Opus 5 10f7e88434 Let an engine move its log off stdout
A graphical client needs data on stdout and nothing else, so `set_log_stream()`
lets an adapter send the human-readable log to stderr instead. `die()` already
wrote there; this is the rest of it.

One function and one module variable — the smallest thing that makes the stores
scriptable rather than only readable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 10:54:00 +02:00
mr.zeroandClaude Opus 5 c4a98e5d67 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>
2026-08-18 10:00:02 +02:00
mr.zeroandClaude Opus 5 a39a29ffde Add a machine-wide uninstaller and the empty-directory rule
A store's own engine can take that store away. What was missing was a way to
take the framework off a machine without having to remember what is on it:
uninstall.sh finds every store home under both known roots, has each store's
own engine purge what it installed, and then removes the store, its launcher,
its Ports entry and the engine files.

It delegates the removing rather than repeating it, because only the engine's
state.json knows which ROMs, playlists, thumbnails or gamelist entries were a
store's. If an engine cannot finish — RetroArch running, a ROMs root unmounted
— the run stops there instead of deleting the engine that knew what it had
installed.

POSIX sh, not bash, so `curl … | sh` works on a machine whose /bin/sh is dash;
checked with dash. `prune_empty_dirs` moves here for the same reason the delete
guard `within()` did: both engines need it, and both need it to be careful. It
removes only directories that are actually empty, and only inside the subtree
the store owns, so one surprise file is enough to keep a directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 08:31:52 +02:00
mr.zeroandClaude Opus 5 5b02633fb8 Extract the store engines' shared core
A store engine reads a WarpEngine catalog and writes some host's own game
library format. Only that last step is about the host: the catalog, the
release selection, the machine matching, the state and the HTTP were the
same code in every engine, and until now there was one engine, so they
lived inside it.

A second engine — RetroArch — made the seam visible, so the shared part
moves here as a single file both engines fetch at install time. An adapter
now supplies three things: a DEFAULT_CONFIG describing its host, an
accept() callback deciding which catalog entries that host can run, and
the code that writes the host's library format.

Two generalisations came out of having two hosts rather than one:

- `resolve_for_arch` becomes `resolve_for_host`. Batocera only ever
  answered "linux", so architecture was the only variable; a RetroArch
  host can be Windows, macOS or Android, and that decides such things as
  what a libretro core file is called. Keys are now os-arch, arch, os, *.
- state records are keyed by `scope`, not by ES `system`. For Batocera the
  two are the same string, so an installed box keeps working without a
  migration — `record_scope()` falls back to `system` for records written
  before this existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:59:53 +02:00