Add purge and an uninstaller, and move to the stores org
`purge` is what `remove` does for one game, for all of them at once, plus the directories the store made: the shell script that wraps it has no way of knowing which ROMs, box art, Ports payloads and gamelist entries were ours, and state.json does. uninstall.sh runs it first and only then removes the Ports entry, the launcher and the store home; if the engine cannot finish, nothing else is touched. The gamelists are merged rather than deleted — they belong to the box, and the user's own games, play counts and favourites stay in them. Two things the tests caught: - The backup was being made on the *uninstall's* first touch of a gamelist we had created ourselves, so a purge produced a backup of our own file. It is now skipped when purging: by then any copy worth having was made on the first sync. - With that fixed, "no backup next to it" means we created the file, so a gamelist that is left empty is deleted too. The box ends up as it was. - The Ports entry cannot be derived from the config: a store repository sets BATOCERA_PORT_NAME to whatever it likes (ours is "Teletype Games Store" while store.name is "Teletype Games"), so the uninstaller looked for a file that was never there. It now finds the entry by looking inside the Ports scripts for this store's home — which also works for boxes installed before this existed, and picks up entries left over from an earlier name. install.sh and uninstall.sh are POSIX sh now, checked with dash, so `curl … | sh` works where /bin/sh is not bash. The repository moved from the tools org to stores; the old raw URLs still redirect, but every reference is updated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+131
@@ -0,0 +1,131 @@
|
||||
#!/bin/sh
|
||||
# Remove a WarpEngine store from a Batocera box.
|
||||
#
|
||||
# The engine does the removing, not this script: it is the only thing that knows
|
||||
# which ROMs, box art, Ports payloads and gamelist entries were the store's — so
|
||||
# it runs `purge` first, and only then do the launcher, the Ports entry and the
|
||||
# store home go.
|
||||
#
|
||||
# ./uninstall.sh # the only store installed, or say which one
|
||||
# STORE_ID=example ./uninstall.sh
|
||||
# DRY_RUN=1 ./uninstall.sh # show what would go, remove nothing
|
||||
# ssh root@batocera 'curl -fsSL https://git.teletypegames.org/stores/warp-engine-batocera-store/raw/branch/master/uninstall.sh | sh'
|
||||
#
|
||||
set -eu
|
||||
|
||||
STORE_ROOT="${BATOCERA_STORE_ROOT:-/userdata/system/batocera-store}"
|
||||
PORTS_DIR="${BATOCERA_PORTS_DIR:-/userdata/roms/ports}"
|
||||
STORE_ID="${STORE_ID:-}"
|
||||
STORE_CONFIG="${STORE_CONFIG:-}"
|
||||
DRY_RUN="${DRY_RUN:-0}"
|
||||
# Keep the store home — its state.json, catalog cache and log — and take only the
|
||||
# games, the launcher and the Ports entry. Useful before a reinstall.
|
||||
KEEP_HOME="${KEEP_HOME:-0}"
|
||||
|
||||
say() { echo "[uninstall] $*"; }
|
||||
die() { echo "[uninstall] error: $*" >&2; exit 1; }
|
||||
run() {
|
||||
if [ "$DRY_RUN" = "1" ]; then echo "[uninstall] would: $*"; else "$@"; fi
|
||||
}
|
||||
|
||||
command -v python3 >/dev/null 2>&1 || die "python3 is required"
|
||||
|
||||
# Which store? An explicit id wins, then the id in a config, then — if exactly
|
||||
# one store is installed — that one. Guessing between several would be worse than
|
||||
# asking.
|
||||
if [ -z "$STORE_ID" ] && [ -n "$STORE_CONFIG" ]; then
|
||||
TMP_CFG="$(mktemp)"
|
||||
trap 'rm -f "$TMP_CFG"' EXIT
|
||||
case "$STORE_CONFIG" in
|
||||
http://*|https://*) curl -fsSL "$STORE_CONFIG" -o "$TMP_CFG" ;;
|
||||
*) [ -f "$STORE_CONFIG" ] || die "no such file: $STORE_CONFIG"; cp "$STORE_CONFIG" "$TMP_CFG" ;;
|
||||
esac
|
||||
STORE_ID="$(python3 -c 'import json,sys; print((json.load(open(sys.argv[1])).get("store") or {}).get("id") or "")' "$TMP_CFG")"
|
||||
[ -n "$STORE_ID" ] || die "no store.id in $STORE_CONFIG"
|
||||
fi
|
||||
|
||||
if [ -z "$STORE_ID" ]; then
|
||||
[ -d "$STORE_ROOT" ] || die "nothing is installed under $STORE_ROOT"
|
||||
FOUND=""
|
||||
COUNT=0
|
||||
for d in "$STORE_ROOT"/*/; do
|
||||
if [ -f "$d/config.json" ]; then
|
||||
FOUND="$FOUND $(basename "$d")"
|
||||
COUNT=$((COUNT + 1))
|
||||
fi
|
||||
done
|
||||
case "$COUNT" in
|
||||
0) die "nothing is installed under $STORE_ROOT" ;;
|
||||
1) STORE_ID="${FOUND# }"; say "the only store installed is '$STORE_ID'" ;;
|
||||
*) die "several stores are installed ($FOUND) — pick one: STORE_ID=<id> $0" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
STORE_HOME="$STORE_ROOT/$STORE_ID"
|
||||
LAUNCHER="$STORE_ROOT/$STORE_ID-store"
|
||||
[ -d "$STORE_HOME" ] || die "no store '$STORE_ID' in $STORE_ROOT"
|
||||
|
||||
# The Ports entry cannot be derived from the config: a store repository may set
|
||||
# BATOCERA_PORT_NAME to something else entirely (ours is "Teletype Games Store",
|
||||
# while store.name is "Teletype Games"). What is certain is that the entry runs
|
||||
# this store's launcher, so the entry is found by looking inside the scripts —
|
||||
# which also works for stores installed before this uninstaller existed.
|
||||
PORTS_ENTRIES=""
|
||||
if [ -n "${BATOCERA_PORT_NAME:-}" ]; then
|
||||
[ -f "$PORTS_DIR/$BATOCERA_PORT_NAME.sh" ] && PORTS_ENTRIES="$PORTS_DIR/$BATOCERA_PORT_NAME.sh"
|
||||
elif [ -d "$PORTS_DIR" ]; then
|
||||
for entry in "$PORTS_DIR"/*.sh; do
|
||||
[ -f "$entry" ] || continue
|
||||
if grep -q -F "$STORE_HOME" "$entry" 2>/dev/null; then
|
||||
PORTS_ENTRIES="$PORTS_ENTRIES
|
||||
$entry"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
say "removing the store '$STORE_ID' from $STORE_HOME"
|
||||
|
||||
# The games first, through the engine. If it cannot finish — an unmounted ROMs
|
||||
# root, say — stop here rather than delete the engine that knows what it
|
||||
# installed and leave the files orphaned.
|
||||
if [ -f "$STORE_HOME/store.py" ] && [ -f "$STORE_HOME/config.json" ]; then
|
||||
DRY=""
|
||||
[ "$DRY_RUN" = "1" ] && DRY="--dry-run"
|
||||
# shellcheck disable=SC2086 — the flag is ours and deliberately word-split
|
||||
BATOCERA_STORE_HOME="$STORE_HOME" python3 "$STORE_HOME/store.py" \
|
||||
--config "$STORE_HOME/config.json" $DRY purge \
|
||||
|| die "the engine could not remove the games (see above) — nothing else was touched."
|
||||
else
|
||||
say "warning: no engine in $STORE_HOME — the ROMs and gamelist entries have to go by hand"
|
||||
fi
|
||||
|
||||
# Newline-separated so a Ports entry with spaces in its name survives the loop.
|
||||
if [ -n "$PORTS_ENTRIES" ]; then
|
||||
printf '%s\n' "$PORTS_ENTRIES" | while IFS= read -r entry; do
|
||||
[ -n "$entry" ] || continue
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
echo "[uninstall] would: rm -f $entry"
|
||||
else
|
||||
rm -f "$entry"
|
||||
say "removed the Ports entry $entry"
|
||||
fi
|
||||
done
|
||||
else
|
||||
say "no Ports entry found in $PORTS_DIR"
|
||||
fi
|
||||
run rm -f "$LAUNCHER"
|
||||
|
||||
if [ "$KEEP_HOME" = "1" ]; then
|
||||
say "keeping $STORE_HOME (KEEP_HOME=1)"
|
||||
else
|
||||
run rm -rf "$STORE_HOME"
|
||||
# Only if this was the last store on the box.
|
||||
rmdir "$STORE_ROOT" 2>/dev/null && say "removed the now-empty $STORE_ROOT" || true
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
say "dry run — nothing was removed."
|
||||
else
|
||||
say "done. EmulationStation was restarted if it was running; if not, the games are"
|
||||
say "already gone from the gamelists."
|
||||
fi
|
||||
Reference in New Issue
Block a user