Files
warp-engine-batocera-store/install.sh
T
mr.zeroandClaude Opus 5 0f1045b59e Move the site-agnostic half into the shared warpstore core
The engine was 1073 lines, of which the catalog, release selection,
architecture matching, state and HTTP had nothing to do with Batocera. A
second engine now needs exactly that code, so it moved to engines/warpstore
and this repository keeps only the Batocera adapter: ROM folders, Ports
launchers, gamelist.xml, EmulationStation. 426 lines went, 115 came back.

Nothing about the CLI or the on-disk layout changes. The installer now
places two files side by side, store.py and warpstore.py, and the engine
imports the core from its own directory.

Behaviour was checked against the live catalog with a fake ROMs root: the
cartridge path, the Ports path, box art, gamelist merging, idempotent
re-sync, prune and uninstall all produce what they did before. A state.json
written by the previous version — records with `system` but no `scope` —
keys the same way, so an installed box neither re-downloads nor prunes
anything on the first sync after the upgrade.

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

128 lines
4.7 KiB
Bash
Executable File

#!/bin/bash
# Install a WarpEngine store on a Batocera box.
#
# This installer belongs to the engine, so it takes the store as a parameter:
# point STORE_CONFIG at a config.json — a local path or a URL — and it sets up
# that store. A store repository's own install.sh is a three-line wrapper that
# supplies its config; see README.
#
# STORE_CONFIG=./config.json ./install.sh
# STORE_CONFIG=https://git.example.org/tools/my-store/raw/branch/master/config.json \
# bash <(curl -fsSL https://git.teletypegames.org/tools/warp-engine-batocera-store/raw/branch/master/install.sh)
#
set -euo pipefail
STORE_ROOT="${BATOCERA_STORE_ROOT:-/userdata/system/batocera-store}"
PORTS_DIR="${BATOCERA_PORTS_DIR:-/userdata/roms/ports}"
ENGINE_RAW_BASE="${ENGINE_RAW_BASE:-https://git.teletypegames.org/tools/warp-engine-batocera-store/raw/branch/master}"
# The shared core lives in its own repository, because both store engines use it.
WARPSTORE_RAW_BASE="${WARPSTORE_RAW_BASE:-https://git.teletypegames.org/engines/warpstore/raw/branch/master}"
STORE_CONFIG="${STORE_CONFIG:-}"
# Only set when run as a file; piped from curl "$0" is `bash`, and then the
# engine has to come from the forge rather than from whatever the cwd holds.
SRC_DIR=""
if [ -f "$0" ]; then
SRC_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
fi
say() { echo "[install] $*"; }
die() { echo "[install] error: $*" >&2; exit 1; }
command -v python3 >/dev/null 2>&1 || die "python3 is required"
[ -n "$STORE_CONFIG" ] || die "STORE_CONFIG is required (path or URL of a store config.json)"
[ -d /userdata ] || say "warning: /userdata not found — this does not look like a Batocera box"
fetch() { # fetch <source> <dest>; source may be a local path or a URL
case "$1" in
http://*|https://*) curl -fsSL "$1" -o "$2" ;;
*) [ -f "$1" ] || die "no such file: $1"; cp "$1" "$2" ;;
esac
}
# Stage the config first: the store id in it decides where everything lands.
TMP_CFG="$(mktemp)"
trap 'rm -f "$TMP_CFG"' EXIT
say "reading store config from $STORE_CONFIG"
fetch "$STORE_CONFIG" "$TMP_CFG"
STORE_META="$(python3 -c '
import json, sys
cfg = json.load(open(sys.argv[1])).get("store") or {}
sid = cfg.get("id") or ""
if not sid or "/" in sid:
sys.exit("store.id is required and must not contain a slash")
print(sid, cfg.get("name") or sid)
' "$TMP_CFG")" || die "cannot read $STORE_CONFIG"
STORE_ID="${STORE_META%% *}"
STORE_NAME="${STORE_META#* }"
STORE_HOME="$STORE_ROOT/$STORE_ID"
PORT_NAME="${BATOCERA_PORT_NAME:-$STORE_NAME}"
LAUNCHER="$STORE_ROOT/$STORE_ID-store"
mkdir -p "$STORE_HOME" "$PORTS_DIR"
install_file() { # install_file <name> <raw base> [local override]
if [ -n "${3:-}" ] && [ -f "${3:-}" ]; then
say "installing $1 from $3"
install -m 0755 "$3" "$STORE_HOME/$1"
elif [ -n "$SRC_DIR" ] && [ -f "$SRC_DIR/$1" ]; then
say "installing $1 from $SRC_DIR"
install -m 0755 "$SRC_DIR/$1" "$STORE_HOME/$1"
else
say "downloading $1 from $2"
curl -fsSL "$2/$1" -o "$STORE_HOME/$1"
chmod 0755 "$STORE_HOME/$1"
fi
}
install_file store.py "$ENGINE_RAW_BASE"
# The shared core: one file next to the engine, fetched from its own repository.
install_file warpstore.py "$WARPSTORE_RAW_BASE" "${WARPSTORE_SRC:-}"
# The config is the store's identity, so it is always refreshed; state.json,
# catalog.json and the log stay put, which makes re-running the upgrade path.
if [ -f "$STORE_HOME/config.json" ]; then
say "updating $STORE_HOME/config.json (previous copy kept as config.json.bak)"
cp "$STORE_HOME/config.json" "$STORE_HOME/config.json.bak"
fi
install -m 0644 "$TMP_CFG" "$STORE_HOME/config.json"
# A short launcher so the CLI is one path, not a python invocation.
cat > "$LAUNCHER" <<EOF
#!/bin/bash
# $STORE_NAME — CLI wrapper around the shared batocera-store engine.
export BATOCERA_STORE_HOME="$STORE_HOME"
exec "$STORE_HOME/store.py" --config "$STORE_HOME/config.json" "\$@"
EOF
chmod 0755 "$LAUNCHER"
# The Ports entry: EmulationStation gives launched scripts no console, so send
# everything to a log file the user can read over SSH.
cat > "$PORTS_DIR/$PORT_NAME.sh" <<EOF
#!/bin/bash
# $STORE_NAME — syncs the catalog into this box's ROM folders.
exec >>"$STORE_HOME/store.log" 2>&1
echo "=== \$(date -Iseconds) sync started ==="
"$LAUNCHER" sync-from-es
echo "=== \$(date -Iseconds) sync finished (exit \$?) ==="
EOF
chmod 0755 "$PORTS_DIR/$PORT_NAME.sh"
say "ports entry: $PORTS_DIR/$PORT_NAME.sh"
say "running the first sync"
"$LAUNCHER" --verbose sync --no-restart
cat <<EOF
[install] done.
Menu: Ports -> "$PORT_NAME" (re-run any time to pull new releases)
CLI: $LAUNCHER list|sync|remove
Config: $STORE_HOME/config.json
Log: $STORE_HOME/store.log
Restart EmulationStation to see the games: batocera-es-swissknife --restart
EOF