tv-anarchy/tools/update.sh
Natalie 7ff780fe56 feat(apps/tv-anarchy): add restart command support
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-09 21:17:10 -07:00

171 lines
8.5 KiB
Bash
Executable file

#!/usr/bin/env bash
# Install or update TVAnarchy from the forge.black release channel — no Xcode
# toolchain or source needed. First run installs the latest release; later runs
# no-op when already current. This is how every node EXCEPT the build box (plum)
# gets the app; plum cuts releases with tools/release.sh.
#
# Per-OS behavior (the fleet: iOS, Android, macOS, Ubuntu, Bluefin, Windows):
# macOS → TVAnarchy-<tag>.zip → /Applications (admin) or ~/Applications
# Linux → TVAnarchy-<tag>-linux-<arch>.tar.gz → /opt/tv-anarchy (classic,
# writable) or ~/.local/opt/tv-anarchy (non-root; always on
# immutable/ostree systems like Bluefin, whose /usr is read-only)
# Windows → TVAnarchy-<tag>-windows-<arch>.zip → %LOCALAPPDATA%\Programs\TVAnarchy
# (per-user, no elevation; run under Git Bash/MSYS)
# Android → run under Termux (detected before generic Linux — Termux's uname
# says Linux): TVAnarchy-<tag>-android.apk → ~/storage/downloads,
# then handed to the system package installer via termux-open.
# Apps aren't folder-installs on Android, so "installed version" is
# a stamp recorded on successful handoff (--force re-downloads).
# iOS → not served here: no shell. Install via Xcode (TVAnarchyiOS scheme)
# or TestFlight/sideload — see docs/operations.md.
# A release that lacks the asset for this platform fails with the exact missing
# asset name (today only the macOS asset is published).
#
# Usage: tools/update.sh [--force]
# Needs: curl, python3 (JSON parsing). macOS also: ditto, PlistBuddy (built in).
# Config (env, all optional — defaults target the mesh Forgejo):
# FORGEJO_API=http://10.9.0.4:3000 FORGEJO_OWNER=lilith FORGEJO_REPO=tv-anarchy
# FORGEJO_TOKEN / ~/.config/tv-anarchy/forgejo-token (only if the repo is private)
# TVANARCHY_DEST=<path> explicit install destination override
set -euo pipefail
API="${FORGEJO_API:-http://10.9.0.4:3000}"
OWNER="${FORGEJO_OWNER:-lilith}"
REPO="${FORGEJO_REPO:-tv-anarchy}"
FORCE=""; [ "${1:-}" = "--force" ] && FORCE=1
TOKEN="${FORGEJO_TOKEN:-}"
TOKEN_FILE="$HOME/.config/tv-anarchy/forgejo-token"
[ -z "$TOKEN" ] && [ -f "$TOKEN_FILE" ] && TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
auth=(); [ -n "$TOKEN" ] && auth=(-H "Authorization: token $TOKEN")
# --- platform logic: ONE source of truth, tools/platform.sh -----------------
# From a checkout, source the sibling file. Standalone (curl-piped, no repo),
# fetch the same file from the same forge the releases come from — the logic is
# never duplicated.
lib="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)/platform.sh"
if [ -f "$lib" ]; then
. "$lib"
else
platform_src="$(curl -fsL "${auth[@]}" "$API/$OWNER/$REPO/raw/branch/main/tools/platform.sh" 2>/dev/null)" || {
echo "✗ couldn't fetch tools/platform.sh from $API (needed when run standalone)." >&2; exit 1; }
eval "$platform_src"
fi
OS="$(tva_os)"
[ "$OS" != unsupported ] || {
echo "✗ unsupported platform '$(uname -s)' — TVAnarchy ships for macOS, Linux, Windows, Android/Termux (iOS via Xcode/TestFlight)." >&2; exit 1; }
ARCH="$(uname -m)" # for messages; asset names resolve it themselves
DEST="$(tva_resolve_dest "$OS")"
# --- resolve the latest release ---------------------------------------------
latest="$(curl -fsL "${auth[@]}" "$API/api/v1/repos/$OWNER/$REPO/releases/latest" 2>/dev/null)" || {
code="$(curl -s -o /dev/null -m 8 -w '%{http_code}' "${auth[@]}" "$API/api/v1/repos/$OWNER/$REPO/releases/latest" || true)"
if [ "$code" = "404" ]; then
echo "$OWNER/$REPO has no releases yet — cut one on the build box with tools/release.sh." >&2
else
echo "✗ couldn't reach Forgejo at $API (HTTP ${code:-none} — mesh down, or repo private + no token?)." >&2
fi
exit 1; }
TAG="$(printf '%s' "$latest" | python3 -c 'import sys,json; print(json.load(sys.stdin)["tag_name"])')"
WANT="$(tva_asset_name "$OS" "$TAG")"
URL="$(printf '%s' "$latest" | python3 -c '
import sys, json
want = sys.argv[1]
r = json.load(sys.stdin)
a = next((a for a in r.get("assets", []) if a["name"] == want), None)
print(a["browser_download_url"] if a else "")
' "$WANT")"
if [ -z "$URL" ]; then
echo "✗ release $TAG has no asset '$WANT' for this platform ($OS/$ARCH)." >&2
echo " published assets:" >&2
printf '%s' "$latest" | python3 -c 'import sys,json; [print(" ", a["name"]) for a in json.load(sys.stdin).get("assets", [])]' >&2
exit 1
fi
# --- compare to the installed copy (releases are tagged v<marketing version>)
# macOS reads the bundle plist; Linux/Windows read the .release-tag stamp this
# script writes on install; Android reads the handoff stamp (Termux can't query
# another package's installed version without adb).
ANDROID_STAMP="$HOME/.local/state/tv-anarchy/android-release-tag"
installed_tag() {
case "$OS" in
mac) [ -d "$1" ] && printf 'v%s\n' "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$1/Contents/Info.plist" 2>/dev/null || echo '?')" ;;
android) [ -f "$ANDROID_STAMP" ] && cat "$ANDROID_STAMP" ;;
*) [ -f "$1/.release-tag" ] && cat "$1/.release-tag" ;;
esac
}
installed="$(installed_tag "$DEST" || true)"; installed="${installed:-none}"; stale=""
if [ "$installed" = "none" ] && [ "$OS" = mac ] && [ -z "${TVANARCHY_DEST:-}" ]; then
# An older mac install may sit at the other auto candidate (the layout moved
# from ~/Applications to /Applications). Count it for the version line and
# migrate it away after the install. Skipped under an explicit TVANARCHY_DEST
# (a test install must not touch the real one).
for other in "/Applications/TVAnarchy.app" "$HOME/Applications/TVAnarchy.app"; do
if [ "$other" != "$DEST" ] && [ -d "$other" ]; then
stale="$other"; installed="$(installed_tag "$other" || true) (at $other)"
fi
done
fi
if [ "$installed" = "$TAG" ] && [ -z "$FORCE" ]; then
echo "✓ already on $TAG — up to date (use --force to reinstall)."; exit 0
fi
echo "$installed$TAG ($OS/$ARCH)"
# --- download, unpack, swap in ----------------------------------------------
TMP="$(mktemp -d "${TMPDIR:-/tmp}/tvanarchy-update.XXXXXX")"
trap 'rm -rf "$TMP"' EXIT
echo "→ downloading $WANT"
curl -fsSL "${auth[@]}" -o "$TMP/asset" "$URL"
mkdir -p "$TMP/unpacked"
case "$WANT" in
*.apk) : ;; # nothing to unpack — the APK is the artifact
*.tar.gz) tar -xzf "$TMP/asset" -C "$TMP/unpacked" ;;
*.zip) if [ "$OS" = mac ]; then ditto -x -k "$TMP/asset" "$TMP/unpacked"
else unzip -q "$TMP/asset" -d "$TMP/unpacked"; fi ;;
esac
mkdir -p "$(dirname "$DEST")"
case "$OS" in
mac)
src="$(/usr/bin/find "$TMP/unpacked" -maxdepth 2 -name 'TVAnarchy.app' -print -quit)"
[ -n "$src" ] || { echo "✗ no TVAnarchy.app inside $WANT." >&2; exit 1; }
rm -rf "$DEST"; ditto "$src" "$DEST"
# Unsigned build copied across machines → clear Gatekeeper quarantine.
xattr -dr com.apple.quarantine "$DEST" 2>/dev/null || true
if [ -n "$stale" ]; then rm -rf "$stale" && echo " removed stale copy at $stale"; fi
relaunch="quit any running TVAnarchy and relaunch to pick this up."
;;
linux|windows)
# Convention: the archive holds a single top-level TVAnarchy/ directory.
src="$(find "$TMP/unpacked" -maxdepth 1 -mindepth 1 -type d -print -quit)"
[ -n "$src" ] || { echo "✗ no payload directory inside $WANT." >&2; exit 1; }
rm -rf "$DEST"; mv "$src" "$DEST"
printf '%s\n' "$TAG" > "$DEST/.release-tag"
[ "$OS" = linux ] && chmod +x "$DEST"/tvanarchy* 2>/dev/null || true
relaunch="restart TVAnarchy to pick this up."
;;
android)
# DEST is a download dir, not an install dir: drop the APK in shared
# storage and hand it to the system package installer. Android won't let a
# shell install packages directly (that's by design); the user confirms the
# installer prompt.
apk="$DEST/$WANT"
mv "$TMP/asset" "$apk"
if command -v termux-open >/dev/null; then
termux-open "$apk"
echo "→ handed $WANT to the package installer — confirm the install prompt."
else
echo "→ saved $apk — open it from the Files app to install."
[ -d "$HOME/storage" ] || echo " (tip: run termux-setup-storage so downloads land in shared storage)"
fi
mkdir -p "$(dirname "$ANDROID_STAMP")"
printf '%s\n' "$TAG" > "$ANDROID_STAMP"
DEST="$apk" # the summary line should point at the APK, not the folder
relaunch="reopen TVAnarchy after the installer finishes."
;;
esac
echo "✓ installed $TAG$DEST"
echo " $relaunch"