|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +REPO="reoring/roboppi" |
| 5 | +GITHUB="https://github.com/${REPO}" |
| 6 | +API="https://api.github.com/repos/${REPO}" |
| 7 | + |
| 8 | +log() { |
| 9 | + printf '%s\n' "$*" >&2 |
| 10 | +} |
| 11 | + |
| 12 | +die() { |
| 13 | + log "roboppi install: $*" |
| 14 | + exit 1 |
| 15 | +} |
| 16 | + |
| 17 | +need_cmd() { |
| 18 | + command -v "$1" >/dev/null 2>&1 |
| 19 | +} |
| 20 | + |
| 21 | +usage() { |
| 22 | + cat >&2 <<'EOF' |
| 23 | +Install Roboppi from GitHub Releases. |
| 24 | +
|
| 25 | +USAGE |
| 26 | + curl -fsSL https://raw.githubusercontent.com/reoring/roboppi/main/install.sh | bash |
| 27 | +
|
| 28 | +OPTIONS |
| 29 | + --tag <vX.Y.Z> Install a specific release tag (default: latest) |
| 30 | + --version <X.Y.Z> Shorthand for --tag vX.Y.Z |
| 31 | + --install-dir <dir> Install directory for the roboppi binary |
| 32 | + --prefix <dir> Install to <dir>/bin |
| 33 | + --no-verify Skip sha256 verification |
| 34 | + --dry-run Print actions without installing |
| 35 | + --help Show this help |
| 36 | +
|
| 37 | +ENV |
| 38 | + ROBOPPI_INSTALL_DIR Same as --install-dir |
| 39 | +
|
| 40 | +NOTES |
| 41 | + - Supported: macOS (x64/arm64), Linux (x64/arm64) |
| 42 | + - This script downloads the prebuilt binary from GitHub Releases. |
| 43 | +EOF |
| 44 | +} |
| 45 | + |
| 46 | +fetch() { |
| 47 | + local url="$1" |
| 48 | + if need_cmd curl; then |
| 49 | + curl -fsSL "$url" |
| 50 | + return 0 |
| 51 | + fi |
| 52 | + if need_cmd wget; then |
| 53 | + wget -qO- "$url" |
| 54 | + return 0 |
| 55 | + fi |
| 56 | + die "need curl or wget" |
| 57 | +} |
| 58 | + |
| 59 | +download_to() { |
| 60 | + local url="$1" |
| 61 | + local out="$2" |
| 62 | + if need_cmd curl; then |
| 63 | + curl -fSL "$url" -o "$out" |
| 64 | + return 0 |
| 65 | + fi |
| 66 | + if need_cmd wget; then |
| 67 | + wget -qO "$out" "$url" |
| 68 | + return 0 |
| 69 | + fi |
| 70 | + die "need curl or wget" |
| 71 | +} |
| 72 | + |
| 73 | +sha256_file() { |
| 74 | + local file="$1" |
| 75 | + if need_cmd sha256sum; then |
| 76 | + sha256sum "$file" | awk '{print $1}' |
| 77 | + return 0 |
| 78 | + fi |
| 79 | + if need_cmd shasum; then |
| 80 | + shasum -a 256 "$file" | awk '{print $1}' |
| 81 | + return 0 |
| 82 | + fi |
| 83 | + die "need sha256sum or shasum" |
| 84 | +} |
| 85 | + |
| 86 | +resolve_latest_tag() { |
| 87 | + # Extract "tag_name" from GitHub API JSON without jq. |
| 88 | + fetch "${API}/releases/latest" | |
| 89 | + awk -F'"' '/"tag_name"[[:space:]]*:/ {print $4; exit}' |
| 90 | +} |
| 91 | + |
| 92 | +map_platform() { |
| 93 | + local os arch |
| 94 | + os="$(uname -s 2>/dev/null || true)" |
| 95 | + arch="$(uname -m 2>/dev/null || true)" |
| 96 | + |
| 97 | + case "$os" in |
| 98 | + Linux) os="linux" ;; |
| 99 | + Darwin) os="macos" ;; |
| 100 | + *) die "unsupported OS: ${os:-unknown}" ;; |
| 101 | + esac |
| 102 | + |
| 103 | + case "$arch" in |
| 104 | + x86_64|amd64) arch="x64" ;; |
| 105 | + arm64|aarch64) arch="arm64" ;; |
| 106 | + *) die "unsupported architecture: ${arch:-unknown}" ;; |
| 107 | + esac |
| 108 | + |
| 109 | + printf '%s-%s' "$os" "$arch" |
| 110 | +} |
| 111 | + |
| 112 | +main() { |
| 113 | + local tag="" |
| 114 | + local install_dir="${ROBOPPI_INSTALL_DIR:-}" |
| 115 | + local verify=1 |
| 116 | + local dry_run=0 |
| 117 | + |
| 118 | + while [[ $# -gt 0 ]]; do |
| 119 | + case "$1" in |
| 120 | + --tag) |
| 121 | + [[ $# -ge 2 ]] || die "--tag requires a value" |
| 122 | + tag="$2" |
| 123 | + shift 2 |
| 124 | + ;; |
| 125 | + --version) |
| 126 | + [[ $# -ge 2 ]] || die "--version requires a value" |
| 127 | + tag="$2" |
| 128 | + if [[ "$tag" != v* ]]; then tag="v${tag}"; fi |
| 129 | + shift 2 |
| 130 | + ;; |
| 131 | + --install-dir) |
| 132 | + [[ $# -ge 2 ]] || die "--install-dir requires a value" |
| 133 | + install_dir="$2" |
| 134 | + shift 2 |
| 135 | + ;; |
| 136 | + --prefix) |
| 137 | + [[ $# -ge 2 ]] || die "--prefix requires a value" |
| 138 | + install_dir="$2/bin" |
| 139 | + shift 2 |
| 140 | + ;; |
| 141 | + --no-verify) |
| 142 | + verify=0 |
| 143 | + shift |
| 144 | + ;; |
| 145 | + --dry-run) |
| 146 | + dry_run=1 |
| 147 | + shift |
| 148 | + ;; |
| 149 | + --help|-h) |
| 150 | + usage |
| 151 | + exit 0 |
| 152 | + ;; |
| 153 | + *) |
| 154 | + die "unknown argument: $1 (try --help)" |
| 155 | + ;; |
| 156 | + esac |
| 157 | + done |
| 158 | + |
| 159 | + if [[ -z "$tag" ]]; then |
| 160 | + tag="$(resolve_latest_tag || true)" |
| 161 | + [[ -n "$tag" ]] || die "failed to resolve latest release tag; use --tag vX.Y.Z" |
| 162 | + else |
| 163 | + if [[ "$tag" != v* ]]; then tag="v${tag}"; fi |
| 164 | + fi |
| 165 | + |
| 166 | + local platform asset base_url |
| 167 | + platform="$(map_platform)" |
| 168 | + asset="roboppi-${tag}-${platform}.tar.gz" |
| 169 | + base_url="${GITHUB}/releases/download/${tag}" |
| 170 | + |
| 171 | + if [[ -z "$install_dir" ]]; then |
| 172 | + if [[ -w "/usr/local/bin" ]]; then |
| 173 | + install_dir="/usr/local/bin" |
| 174 | + else |
| 175 | + install_dir="${HOME}/.local/bin" |
| 176 | + fi |
| 177 | + fi |
| 178 | + |
| 179 | + local tmp |
| 180 | + tmp="$(mktemp -d 2>/dev/null || mktemp -d -t roboppi)" |
| 181 | + trap 'rm -rf "$tmp"' EXIT |
| 182 | + |
| 183 | + log "roboppi install: tag=${tag} platform=${platform}" |
| 184 | + log "roboppi install: downloading ${asset}" |
| 185 | + |
| 186 | + if [[ "$dry_run" -eq 1 ]]; then |
| 187 | + log "dry-run: would download ${base_url}/${asset}" |
| 188 | + log "dry-run: would install to ${install_dir}/roboppi" |
| 189 | + return 0 |
| 190 | + fi |
| 191 | + |
| 192 | + download_to "${base_url}/${asset}" "${tmp}/${asset}" || die "download failed: ${base_url}/${asset}" |
| 193 | + |
| 194 | + if [[ "$verify" -eq 1 ]]; then |
| 195 | + local sha_url sha_file expected got |
| 196 | + sha_url="${base_url}/${asset}.sha256" |
| 197 | + sha_file="${tmp}/${asset}.sha256" |
| 198 | + download_to "$sha_url" "$sha_file" || die "download failed: ${sha_url}" |
| 199 | + expected="$(awk 'NR==1 {print $1; exit}' "$sha_file")" |
| 200 | + [[ -n "$expected" ]] || die "could not read expected sha256" |
| 201 | + got="$(sha256_file "${tmp}/${asset}")" |
| 202 | + if [[ "$got" != "$expected" ]]; then |
| 203 | + die "sha256 mismatch for ${asset} (expected ${expected}, got ${got})" |
| 204 | + fi |
| 205 | + fi |
| 206 | + |
| 207 | + mkdir -p "${tmp}/stage" |
| 208 | + tar -xzf "${tmp}/${asset}" -C "${tmp}/stage" |
| 209 | + |
| 210 | + local src_bin |
| 211 | + src_bin="${tmp}/stage/roboppi" |
| 212 | + [[ -f "$src_bin" ]] || die "archive did not contain roboppi binary" |
| 213 | + |
| 214 | + if [[ -w "$install_dir" ]]; then |
| 215 | + mkdir -p "$install_dir" |
| 216 | + if need_cmd install; then |
| 217 | + install -m 755 "$src_bin" "${install_dir}/roboppi" |
| 218 | + else |
| 219 | + cp "$src_bin" "${install_dir}/roboppi" |
| 220 | + chmod 755 "${install_dir}/roboppi" |
| 221 | + fi |
| 222 | + else |
| 223 | + if need_cmd sudo; then |
| 224 | + sudo mkdir -p "$install_dir" |
| 225 | + if need_cmd install; then |
| 226 | + sudo install -m 755 "$src_bin" "${install_dir}/roboppi" |
| 227 | + else |
| 228 | + sudo cp "$src_bin" "${install_dir}/roboppi" |
| 229 | + sudo chmod 755 "${install_dir}/roboppi" |
| 230 | + fi |
| 231 | + else |
| 232 | + die "install dir not writable (${install_dir}); set --install-dir or install sudo" |
| 233 | + fi |
| 234 | + fi |
| 235 | + |
| 236 | + log "roboppi install: installed to ${install_dir}/roboppi" |
| 237 | + |
| 238 | + if command -v roboppi >/dev/null 2>&1; then |
| 239 | + roboppi --version || true |
| 240 | + elif [[ -x "${install_dir}/roboppi" ]]; then |
| 241 | + "${install_dir}/roboppi" --version || true |
| 242 | + log "roboppi install: ${install_dir} is not on PATH" |
| 243 | + fi |
| 244 | +} |
| 245 | + |
| 246 | +main "$@" |
0 commit comments