Files
orca/scripts/install.sh
T
Jon Chery 5600531bd7 fix(install): default to /usr/local/bin (on PATH for all users)
Root or writable /usr/local/bin: install there (no PATH edits needed).
Non-root without sudo: fall back to ~/.local/bin + auto-add to .bashrc.
This eliminates the 'NOTE: not on your PATH' message for the common case.
2026-08-10 17:22:42 +00:00

285 lines
10 KiB
Bash
Executable File

#!/bin/bash
# install.sh — 1-liner installer for orca
#
# Usage:
# curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash
# curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash -s -- --system
# curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash -s -- --version v0.4.2
#
# Options:
# --system Install at system level (/usr/local/bin/orca, namespace /root/.orca). Requires root.
# --version <tag> Pin a specific version (e.g. v0.4.2). Default: latest release.
# --check Dry-run: print the version + asset URL + install path without writing.
# --help, -h Show this help.
#
# Behavior:
# - Downloads the release tarball from the public Gitea release URL.
# - Extracts the orca binary to the install path.
# - If the resolved release (latest or pinned) has no matching binary
# asset, walks backward through recent releases to find one that does,
# and prints a warning. (REQ-098 — the v0.8.x releases shipped with
# zero binary assets, causing install to resolve to v0.4.5.)
# - If an existing orca binary is found, reads its version and prints
# "updated from X to Y" (in-place update; preserves config/db/certs).
# - Idempotent: re-running with the same version reinstalls the binary.
# - Never touches the namespace dir (~/.orca or /root/.orca) — that's user state.
set -euo pipefail
GITEA_URL="${GITEA_URL:-https://git.cloudinit.dev}"
GITEA_OWNER="${GITEA_OWNER:-coreci}"
GITEA_REPO="${GITEA_REPO:-orca}"
SYSTEM=false
VERSION=""
CHECK=false
INSTALL_BIN=""
NAMESPACE_DIR=""
warn() {
printf " \033[1;33m!\033[0m %s\n" "$*" >&2
}
err() { echo "install: error: $*" >&2; exit 1; }
info() { echo "install: $*"; }
usage() {
sed -n '2,/^$/p' "$0" | sed 's/^# \?//' >&2
exit 0
}
# --- parse args ------------------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
--system) SYSTEM=true; shift ;;
--version) VERSION="${2:-}"; shift 2 ;;
--version=*) VERSION="${1#*=}"; shift ;;
--check) CHECK=true; shift ;;
--help|-h) usage ;;
*) err "unknown argument: $1 (try --help)" ;;
esac
done
# --- determine install paths ----------------------------------------------
if [ "$SYSTEM" = "true" ]; then
if [ "$(id -u)" -ne 0 ]; then
err "--system requires root (uid 0). Re-run with sudo or drop --system for user-level install."
fi
INSTALL_BIN="/usr/local/bin/orca"
NAMESPACE_DIR="/root/.orca"
elif [ -w /usr/local/bin ] || [ "$(id -u)" -eq 0 ]; then
INSTALL_BIN="/usr/local/bin/orca"
NAMESPACE_DIR="${HOME}/.orca"
else
INSTALL_BIN="${HOME}/.local/bin/orca"
NAMESPACE_DIR="${HOME}/.orca"
fi
INSTALL_DIR="$(dirname "$INSTALL_BIN")"
# --- determine version ----------------------------------------------------
if [ -z "$VERSION" ]; then
info "querying latest release from ${GITEA_URL}..."
VERSION="$(curl -fsSL "${GITEA_URL}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/latest" \
| sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
| head -1)"
if [ -z "$VERSION" ]; then
err "could not determine latest release version from Gitea API"
fi
fi
info "version: ${VERSION}"
# --- detect arch ----------------------------------------------------------
ARCH="$(uname -m)"
case "$ARCH" in
x86_64) ARCH=amd64 ;;
aarch64|arm64) ARCH=arm64 ;;
armv7l) ARCH=armv7 ;;
*) err "unsupported architecture: ${ARCH} (supported: amd64, arm64, armv7)" ;;
esac
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
TARBALL="orca-${VERSION}-${OS}-${ARCH}.tar.gz"
# --- find asset download URL (with fallback walk — REQ-098) --------------
#
# The v0.8.x releases shipped with zero binary assets attached, causing
# install to error out on the latest release. If the resolved release
# (latest or --version) lacks the matching tarball, walk backward through
# recent releases to find one that carries it, and print a warning.
find_asset_url() {
# $1 = tag. Prints the browser_download_url for the matching tarball, or empty.
# Match by asset NAME (not URL path) — Gitea attachment URLs are opaque
# UUIDs that don't contain the tarball name.
local tag="$1"
local json
json="$(curl -fsSL "${GITEA_URL}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/tags/${tag}" 2>/dev/null)" || return 1
if command -v python3 >/dev/null 2>&1; then
echo "$json" | python3 -c "
import json,sys
r=json.load(sys.stdin)
for a in r.get('assets',[]):
if a.get('name')=='$TARBALL':
print(a.get('browser_download_url',''))
break
" 2>/dev/null || true
else
echo "$json" | sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"$TARBALL".*"browser_download_url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1 || true
fi
}
find_asset_in_releases() {
# Walk recent releases, find the newest with a matching asset name.
# Outputs two lines: URL and VERSION (caller captures both).
local json
json="$(curl -fsSL "${GITEA_URL}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}/releases?limit=50" 2>/dev/null)" || return 1
if command -v python3 >/dev/null 2>&1; then
echo "$json" | python3 -c "
import json,sys,re
rels=json.load(sys.stdin)
for r in rels:
tag=r.get('tag_name','')
for a in r.get('assets',[]):
name=a.get('name','')
m=re.match(r'orca-(v[0-9.]+)-' + '${OS}' + '-' + '${ARCH}' + r'\.tar\.gz$', name)
if m:
print(a.get('browser_download_url',''))
print(m.group(1))
sys.exit(0)
"
fi
}
info "locating asset ${TARBALL} in release ${VERSION}..."
ASSET_URL="$(find_asset_url "$VERSION" 2>/dev/null)" || true
if [ -z "$ASSET_URL" ]; then
info "WARNING: release ${VERSION} has no ${TARBALL} asset. Walking back through recent releases..."
FALLBACK_OUT="$(find_asset_in_releases || true)"
ASSET_URL="$(echo "$FALLBACK_OUT" | head -1)"
if [ -n "$ASSET_URL" ]; then
VERSION="$(echo "$FALLBACK_OUT" | tail -1)"
info "WARNING: falling back to ${VERSION} which has orca-${VERSION}-${OS}-${ARCH}.tar.gz."
else
err "could not find any release with a ${OS}-${ARCH} tarball in the last 50 releases. Check that a release exists with a linux-${ARCH} binary."
fi
fi
info "asset: ${ASSET_URL}"
# --- in-place update detection -------------------------------------------
OLD_VERSION=""
if [ -x "$INSTALL_BIN" ]; then
OLD_VERSION="$("$INSTALL_BIN" version --json 2>/dev/null | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1 || echo "")"
fi
# --- --check dry-run (D-194) ---------------------------------------------
# Print what would be installed without writing anything.
if [ "$CHECK" = "true" ]; then
info "dry-run (--check): no files will be written"
info " would install: orca ${VERSION}"
info " asset: ${ASSET_URL}"
info " binary path: ${INSTALL_BIN}"
info " namespace root: ${NAMESPACE_DIR}"
if [ -n "$OLD_VERSION" ]; then
info " current: ${OLD_VERSION} (would update to ${VERSION})"
else
info " current: (not installed)"
fi
exit 0
fi
# --- download + extract ---------------------------------------------------
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
info "downloading..."
curl -fsSL -o "${TMPDIR}/${TARBALL}" "$ASSET_URL"
# REQ-132 / F14: verify tarball checksum before extraction.
# Fetch SHA256SUMS from the same release. For Gitea release-download
# URLs (e.g. /releases/download/vX.Y.Z/...) the SHA256SUMS is a sibling.
# For Gitea attachment URLs (e.g. /attachments/<uuid>) we must look up
# the SHA256SUMS asset by name from the release API.
SHA256SUMS_URL=""
if echo "$ASSET_URL" | grep -q "/releases/download/"; then
SHA256SUMS_URL="$(dirname "$ASSET_URL")/SHA256SUMS"
elif command -v python3 >/dev/null 2>&1; then
# Look up SHA256SUMS asset by name from the release API.
SHA256SUMS_URL="$(curl -fsSL \
"${GITEA_URL}/api/v1/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/tags/${VERSION}" 2>/dev/null \
| python3 -c "
import json,sys
r=json.load(sys.stdin)
for a in r.get('assets',[]):
if a.get('name')=='SHA256SUMS':
print(a.get('browser_download_url',''))
break
" 2>/dev/null || true)"
fi
if [ -n "$SHA256SUMS_URL" ] && curl -fsSL -o "${TMPDIR}/SHA256SUMS" "$SHA256SUMS_URL" 2>/dev/null; then
info "verifying checksum..."
(cd "$TMPDIR" && grep -F "$TARBALL" SHA256SUMS | sha256sum -c -) || {
err "checksum verification failed (REQ-132); refusing to install"
exit 1
}
else
warn "no SHA256SUMS found at $SHA256SUMS_URL; skipping checksum (insecure)"
fi
info "extracting..."
tar -xzf "${TMPDIR}/${TARBALL}" -C "$TMPDIR"
if [ ! -f "${TMPDIR}/orca" ]; then
err "tarball did not contain an 'orca' binary"
fi
# --- install --------------------------------------------------------------
mkdir -p "$INSTALL_DIR"
install -m 0755 "${TMPDIR}/orca" "$INSTALL_BIN"
# --- report ---------------------------------------------------------------
if [ -n "$OLD_VERSION" ]; then
if [ "$OLD_VERSION" = "$VERSION" ]; then
info "✓ reinstalled orca ${VERSION} at ${INSTALL_BIN}"
else
info "✓ updated orca from ${OLD_VERSION} to ${VERSION} at ${INSTALL_BIN}"
fi
else
info "✓ installed orca ${VERSION} to ${INSTALL_BIN}"
fi
if [ "$SYSTEM" = "true" ]; then
info " namespace root: ${NAMESPACE_DIR} (use 'orca --system init' to initialize)"
else
info " namespace root: ${NAMESPACE_DIR} (use 'orca init' to initialize)"
# If the install dir is not on PATH, add it to .bashrc automatically.
if ! echo "$PATH" | grep -q "$INSTALL_DIR"; then
info " Adding $INSTALL_DIR to PATH via ~/.bashrc..."
SHELL_RC="${HOME}/.bashrc"
if [ -f "$SHELL_RC" ]; then
# Append only if not already present (idempotent).
if ! grep -qF "$INSTALL_DIR" "$SHELL_RC"; then
echo "" >> "$SHELL_RC"
echo "# Added by orca install.sh" >> "$SHELL_RC"
echo "export PATH=\"\$PATH:$INSTALL_DIR\"" >> "$SHELL_RC"
info " ✓ Added 'export PATH=\"\$PATH:$INSTALL_DIR\"' to ~/.bashrc"
info " Run 'source ~/.bashrc' or start a new shell to pick up the change."
fi
else
info " NOTE: ~/.bashrc not found. Add to PATH manually:"
info " export PATH=\"\$PATH:$INSTALL_DIR\""
fi
fi
fi
info " verify: ${INSTALL_BIN} version"