#!/bin/sh # Remote-RED Agent install script (AGENT-KONZEPT.md Kap. 8.1). # # curl -fsSL https://get.remote-red.com | sh # # POSIX sh only (no bashisms): this runs via `sh` on whatever shell that # happens to be (dash on Debian/Ubuntu, busybox ash on Alpine, ...), and is # frequently piped directly from curl, so it must not rely on features a # strict POSIX shell lacks. # # Binaries and checksums.txt are served from our own server, not a code # forge: REMOTERED_BASE_URL defaults to https://get.remote-red.com and is # overridable (e.g. for a staging host or the local server used by the # install.sh end-to-end test). set -eu REMOTERED_BASE_URL="${REMOTERED_BASE_URL:-https://get.remote-red.com}" REMOTERED_VERSION="${REMOTERED_VERSION:-latest}" INSTALL_DIR="${REMOTERED_INSTALL_DIR:-/usr/local/bin}" BINARY_NAME="remotered" # --- output helpers --------------------------------------------------------- info() { printf '%s\n' "$*" } err() { printf 'error: %s\n' "$*" >&2 } die() { err "$*" exit 1 } # --- platform detection ------------------------------------------------------ detect_os() { os="$(uname -s)" case "$os" in Linux) echo "linux" ;; *) die "Remote-RED Agent v1 only supports Linux (detected: $os). See AGENT-KONZEPT.md chapter 10.1 for the Windows/macOS roadmap." ;; esac } # Maps `uname -m` to the release asset architecture suffix (Konzept Kap. 8.1: # amd64/arm64/armv7). Go has no "armv7" GOARCH (it is GOARCH=arm, GOARM=7), but # the release assets are named remotered-linux- with "armv7" as that # arch suffix (see the Makefile's `release` target), so the naming matches on # both ends. detect_arch() { machine="$(uname -m)" case "$machine" in x86_64|amd64) echo "amd64" ;; aarch64|arm64) echo "arm64" ;; armv7l|armv7) echo "armv7" ;; armv6l|armv6) die "32-bit ARMv6 ($machine) is not a supported v1 target (only armv7 and up). See AGENT-KONZEPT.md chapter 8.1." ;; *) die "unsupported CPU architecture: $machine" ;; esac } # --- download ----------------------------------------------------------------- # fetch downloads url to the given destination path using whichever of # curl/wget is available, so the script does not hard-depend on curl even # though the documented invocation pipes from it. fetch() { url="$1" dest="$2" if command -v curl >/dev/null 2>&1; then curl -fsSL -o "$dest" "$url" elif command -v wget >/dev/null 2>&1; then wget -q -O "$dest" "$url" else die "neither curl nor wget is available; install one and re-run" fi } # --- checksum verification ---------------------------------------------------- # verify_checksum checks that file matches the entry for filename in # checksums_file (the "sha256sum -c"-compatible format the Makefile's # `make release` target produces). Downloading a binary without verifying it # against the published checksums is not acceptable (Konzept Kap. 8.1), so # there is no flag to skip this. verify_checksum() { file="$1" filename="$2" checksums_file="$3" expected="$(awk -v f="$filename" '$2 == f { print $1 }' "$checksums_file")" if [ -z "$expected" ]; then die "no checksum entry for $filename in checksums.txt — refusing to install an unverified binary" fi if command -v sha256sum >/dev/null 2>&1; then actual="$(sha256sum "$file" | awk '{ print $1 }')" elif command -v shasum >/dev/null 2>&1; then actual="$(shasum -a 256 "$file" | awk '{ print $1 }')" else die "neither sha256sum nor shasum is available to verify the download; install one and re-run" fi if [ "$expected" != "$actual" ]; then die "checksum mismatch for $filename: expected $expected, got $actual — refusing to install (corrupted download or tampering)" fi } # --- main --------------------------------------------------------------------- main() { os="$(detect_os)" arch="$(detect_arch)" info "Detected platform: $os/$arch" # Layout on get.remote-red.com mirrors dist// produced by # `make release` exactly: ...//remotered-linux- and # ...//checksums.txt, with .../latest/ an alias for whichever # version is current (see the Makefile's top-of-file comment). base_url="$REMOTERED_BASE_URL/$REMOTERED_VERSION" asset_name="remotered-${os}-${arch}" workdir="$(mktemp -d "${TMPDIR:-/tmp}/remotered-install.XXXXXX")" trap 'rm -rf "$workdir"' EXIT INT TERM binary_tmp="$workdir/$asset_name" checksums_tmp="$workdir/checksums.txt" info "Downloading $asset_name..." fetch "$base_url/$asset_name" "$binary_tmp" \ || die "download failed: $base_url/$asset_name (check REMOTERED_BASE_URL/REMOTERED_VERSION, or that a release for $os/$arch exists)" info "Downloading checksums.txt..." fetch "$base_url/checksums.txt" "$checksums_tmp" \ || die "download failed: $base_url/checksums.txt" info "Verifying checksum..." verify_checksum "$binary_tmp" "$asset_name" "$checksums_tmp" info "Checksum OK." chmod 0755 "$binary_tmp" dest="$INSTALL_DIR/$BINARY_NAME" dest_tmp="$dest.new.$$" if [ ! -d "$INSTALL_DIR" ]; then mkdir -p "$INSTALL_DIR" 2>/dev/null || true fi if [ ! -w "$INSTALL_DIR" ]; then die "$INSTALL_DIR is not writable — re-run as root (sudo), or set REMOTERED_INSTALL_DIR to a writable directory" fi # Atomic install: copy into the target directory under a temp name, then # rename into place. A `mv` across the same filesystem is atomic, so a # concurrent `remotered` invocation never observes a half-written binary; # this also means a failed download never corrupts an existing install. cp "$binary_tmp" "$dest_tmp" mv "$dest_tmp" "$dest" info "Installed $dest" "$dest" version 2>/dev/null | sed 's/^/remotered version: /' || true info "" info "Next step: run 'remotered setup' to register this instance and pair the app." # `curl -fsSL ... | sh` leaves this script's own stdin attached to the # pipe from curl, not the terminal — starting an interactive wizard would # either block forever waiting on input that is never coming, or silently # consume whatever curl still has buffered. Only offer to run setup when # stdin can be redirected from the real terminal (/dev/tty); otherwise # just point the user at the command to run themselves. if [ -t 1 ] && [ -r /dev/tty ]; then printf 'Run it now? [y/N]: ' read -r reply < /dev/tty || reply="" case "$reply" in [Yy]|[Yy][Ee][Ss]) "$dest" setup < /dev/tty ;; *) info "Skipped. Run 'remotered setup' whenever you're ready." ;; esac else info "(Not running it automatically: this script's input is not a terminal, e.g. because" info "it was invoked as 'curl | sh'. Run 'remotered setup' yourself when ready.)" fi } main "$@"