tv-anarchy/tools/update.sh
Natalie ef3ed6dcfe feat(@applications/tv-anarchy): update release update script to multi-platform
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-09 21:04:19 -07:00

164 lines
7.6 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, 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)
# 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
# --- platform --------------------------------------------------------------
case "$(uname -s)" in
Darwin) OS=mac ;;
Linux) OS=linux ;;
MINGW*|MSYS*|CYGWIN*) OS=windows ;;
*) echo "✗ unsupported platform '$(uname -s)' — TVAnarchy ships for macOS, Linux, Windows (iOS via Xcode/TestFlight)." >&2; exit 1 ;;
esac
ARCH="$(uname -m)" # arm64 / x86_64 / aarch64
# True on image-based (ostree/bootc) Linux — Bluefin, Silverblue, etc. Their
# /usr is read-only and /opt is machine-local; user-scope installs are the norm.
is_immutable_linux() { [ -e /run/ostree-booted ] || [ -e /usr/lib/bootc ]; }
# Where TVAnarchy installs on THIS node. Mirrors build-install.sh's macOS logic
# (kept in sync) so this script stays curl-able standalone.
resolve_dest() {
if [ -n "${TVANARCHY_DEST:-}" ]; then printf '%s\n' "$TVANARCHY_DEST"; return; fi
case "$OS" in
mac)
if [ -w /Applications ]; then printf '/Applications/TVAnarchy.app\n'
else printf '%s/Applications/TVAnarchy.app\n' "$HOME"; fi ;;
linux)
if ! is_immutable_linux && [ -w /opt ]; then printf '/opt/tv-anarchy\n'
else printf '%s/.local/opt/tv-anarchy\n' "$HOME"; fi ;;
windows)
printf '%s/Programs/TVAnarchy\n' "${LOCALAPPDATA:-$HOME/AppData/Local}" ;;
esac
}
DEST="$(resolve_dest)"
# The release asset this platform consumes.
asset_name() {
case "$OS" in
mac) printf 'TVAnarchy-%s.zip\n' "$1" ;;
linux) printf 'TVAnarchy-%s-linux-%s.tar.gz\n' "$1" "$ARCH" ;;
windows) printf 'TVAnarchy-%s-windows-%s.zip\n' "$1" "$ARCH" ;;
esac
}
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")
# --- 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="$(asset_name "$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.
installed_tag() {
if [ "$OS" = mac ]; then
[ -d "$1" ] && printf 'v%s\n' "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$1/Contents/Info.plist" 2>/dev/null || echo '?')"
else
[ -f "$1/.release-tag" ] && cat "$1/.release-tag"
fi
}
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
*.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."
;;
esac
echo "✓ installed $TAG$DEST"
echo " $relaunch"