171 lines
8.5 KiB
Bash
Executable file
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"
|