#!/usr/bin/env bash set -euo pipefail fail() { printf 'Looper: %s\n' "$*" >&2; exit 1; } requested_version="" case "${1:-}" in --help|-h) printf 'Usage: bash install.sh [--version X.Y.Z]\nInstalls the published Linux x86_64 release for your user and restarts its user service.\n'; exit 0 ;; --version) [[ $# -eq 2 && "$2" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail 'Use --version X.Y.Z'; requested_version="$2" ;; '') [[ $# -eq 0 ]] || fail 'Unexpected argument' ;; *) fail 'Usage: bash install.sh [--version X.Y.Z]' ;; esac [[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || fail 'This installer supports Linux x86_64. See https://looper.fyi/docs/install' [[ "$(id -u)" != 0 ]] || fail 'Run as your own user, without sudo.' for command_name in curl python3 sha256sum tar systemctl; do command -v "$command_name" >/dev/null || fail "Required command missing: $command_name" done install_tmp="$(mktemp -d)" trap 'rm -rf -- "$install_tmp"' EXIT fetch() { curl --fail --show-error --silent --location --proto '=https' --proto-redir '=https' --connect-timeout 15 --max-time 300 "$1" -o "$2"; } fetch 'https://looper.fyi/release.json' "$install_tmp/release.json" release_fields="$(python3 - "$install_tmp/release.json" "$requested_version" <<'PY' import json, re, sys def fail(message): sys.exit('Looper: ' + message) try: data = json.load(open(sys.argv[1])) if data.get('schema_version') != 1 or data.get('repository') != 'heyAyushh/loopndroll': fail('Unrecognized release manifest.') if data.get('status') != 'published': fail('Public downloads are pending. See https://looper.fyi/docs/install') candidates = [a for a in data.get('assets', []) if a.get('os') == 'linux' and a.get('arch') == 'x86_64' and a.get('name', '').endswith('-linux-x86_64.tar.gz')] if len(candidates) != 1: fail('The verified Linux x86_64 asset is unavailable.') asset = candidates[0] version = asset.get('version', data.get('version')) if not isinstance(version, str) or not re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+', version): fail('Invalid published Linux version.') if asset.get('channel', data.get('channel', 'stable')) not in ('stable', 'preview'): fail('Invalid published Linux channel.') if sys.argv[2] and sys.argv[2] != version: fail('Requested version does not match the published Linux manifest.') name = 'looper-' + version + '-linux-x86_64.tar.gz' url = asset.get('url') github_url = 'https://github.com/heyAyushh/loopndroll/releases/download/v' + version + '/' + name blob_pattern = r'https://[a-z0-9]+\.public\.blob\.vercel-storage\.com/releases/(?:[A-Za-z0-9][A-Za-z0-9._-]*/)*' + re.escape(name) if asset.get('name') != name or not isinstance(url, str) or not (url == github_url or re.fullmatch(blob_pattern, url)): fail('The verified Linux x86_64 asset is unavailable.') digest = asset.get('sha256') if not isinstance(digest, str) or not re.fullmatch(r'[0-9a-f]{64}', digest): fail('Missing or invalid SHA-256 digest.') print(version + '\t' + url + '\t' + digest) except (ValueError, KeyError, TypeError, AttributeError): fail('Invalid release manifest.') PY )" IFS=$'\t' read -r release_version archive_url archive_sha256 <<< "$release_fields" printf 'Downloading Looper %s for Linux x86_64.\n' "$release_version" fetch "$archive_url" "$install_tmp/archive.tar.gz" printf '%s %s\n' "$archive_sha256" "$install_tmp/archive.tar.gz" | sha256sum --check --status || fail 'Archive checksum mismatch. Nothing was installed.' # ponytail: verify archive paths with Python's standard library before invoking the package helper. python3 - "$install_tmp/archive.tar.gz" <<'PY' import pathlib, sys, tarfile with tarfile.open(sys.argv[1], 'r:gz') as archive: for item in archive.getmembers(): path = pathlib.PurePosixPath(item.name) if not path.parts or path.parts[0] != 'looper-linux' or '..' in path.parts or not (item.isfile() or item.isdir()): sys.exit('Looper: Unsafe archive member; nothing was installed.') PY tar -xzf "$install_tmp/archive.tar.gz" -C "$install_tmp" --no-same-owner --no-same-permissions [[ -f "$install_tmp/looper-linux/looper-linux.sh" ]] || fail 'Package installer is missing.' printf 'Installing for your user and restarting looper.service.\n' bash "$install_tmp/looper-linux/looper-linux.sh" install