#!/usr/bin/env bash # shellcheck shell=bash # shellcheck disable=SC2039 # Arcium installer: minimal, professional UX (color + spinner on TTY) set -euo pipefail # ═══════════════════════════════════════════════════════════════ # Constants & Configuration # ═══════════════════════════════════════════════════════════════ ARCUP_BASE_URL="${ARCUP_BASE_URL:-https://bin.arcium.com/download}" INSTALL_DIR="$HOME/.cargo/bin" MAX_RETRIES=3 RETRY_DELAY=2 QUIET="no" ARCUP_VERSION="" # Check if terminal supports unicode (UTF-8 locale) supports_unicode() { case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in *[Uu][Tt][Ff]8*|*[Uu][Tt][Ff]-8*) return 0 ;; esac return 1 } # Colored status symbols (green OK, red FAIL) # Uses unicode symbols when supported, falls back to [OK]/[FAIL] ok_sym() { if supports_unicode; then printf '%s✓%s' "$(ansi 32)" "$(reset)" else printf '%s[OK]%s' "$(ansi 32)" "$(reset)" fi } fail_sym() { if supports_unicode; then printf '%s✗%s' "$(ansi 31)" "$(reset)" else printf '%s[FAIL]%s' "$(ansi 31)" "$(reset)" fi } # Step progress tracking TOTAL_STEPS=4 CURRENT_STEP=0 # ═══════════════════════════════════════════════════════════════ # TTY & Output Utilities # ═══════════════════════════════════════════════════════════════ # Check if stderr is a TTY (terminal) has_tty() { [ -t 2 ] } # Check if we should use color output # Respects NO_COLOR env var and dumb terminals use_color() { has_tty && [ -z "${NO_COLOR:-}" ] && [ "${TERM-}" != "dumb" ] } # Print ANSI escape code for color/style # Usage: ansi 31 (red), ansi 1 (bold), ansi "1;31" (bold red) ansi() { use_color && printf '\033[%sm' "$1" } # Reset all ANSI formatting reset() { use_color && printf '\033[0m' } say() { if [ "$QUIET" != "yes" ]; then printf '%s\n' "$1" >&2; fi } # Print numbered step header: [1/4] Step description step() { CURRENT_STEP=$((CURRENT_STEP + 1)) say "" say "[$CURRENT_STEP/$TOTAL_STEPS] $1" } warn() { printf '%s%swarning:%s %s\n' "$(ansi 1)" "$(ansi 33)" "$(reset)" "$1" >&2 } err() { printf '%s%serror:%s %s\n' "$(ansi 1)" "$(ansi 31)" "$(reset)" "$1" >&2 } need_cmd() { if ! command -v "$1" >/dev/null 2>&1; then printf '%s ' "$(fail_sym)" >&2 err "need '$1' (command not found)" exit 1 fi } # Display ASCII art banner on TTY show_banner() { has_tty || return 0 [ "$QUIET" = "yes" ] && return 0 cat <<'BANNER' >&2 _ ____ ____ ___ _ _ __ __ / \ | _ \ / ___|_ _| | | | \/ | / _ \ | |_) | | | || | | | |\/| | / ___ \| _ <| |___ | || |_| | | | | /_/ \_\_| \_\____|___|\___/|_| |_| BANNER } # ═══════════════════════════════════════════════════════════════ # Cleanup & Signal Handling # ═══════════════════════════════════════════════════════════════ # Global cleanup state (bash 3.2 compatible - no arrays) _CLEANUP_TEMP_DIR="" _CLEANUP_SPINNER_PID="" _CLEANUP_CURSOR_HIDDEN="no" # Single global cleanup function - handles all resources _run_cleanup() { local exit_code=$? # Restore cursor if hidden [ "$_CLEANUP_CURSOR_HIDDEN" = "yes" ] && _show_cursor # Kill spinner process if running [ -n "$_CLEANUP_SPINNER_PID" ] && kill "$_CLEANUP_SPINNER_PID" 2>/dev/null || true # Remove temp directory if set [ -n "$_CLEANUP_TEMP_DIR" ] && rm -rf "$_CLEANUP_TEMP_DIR" return $exit_code } # Single trap for both INT and EXIT - preserve exit code trap '_run_cleanup; printf "\n"; exit 130' INT trap 'exit_code=$?; _run_cleanup; exit $exit_code' EXIT # ═══════════════════════════════════════════════════════════════ # Spinner & Progress Display # ═══════════════════════════════════════════════════════════════ _hide_cursor() { command -v tput >/dev/null 2>&1 && tput civis || true } _show_cursor() { command -v tput >/dev/null 2>&1 && tput cnorm || true } show_spinner() { local pid=$1 msg="$2" delay=0.1 frames='|/-\' i=0 [ "$QUIET" = "yes" ] && { wait "$pid"; return $?; } has_tty || { wait "$pid"; return $?; } _hide_cursor _CLEANUP_CURSOR_HIDDEN="yes" _CLEANUP_SPINNER_PID="$pid" while kill -0 "$pid" 2>/dev/null; do printf '\r%s%s %s %s' "$(ansi 36)" "$msg" "${frames:i%4:1}" "$(reset)" >&2 i=$(( (i+1) % 4 )) sleep "$delay" done _CLEANUP_SPINNER_PID="" _show_cursor _CLEANUP_CURSOR_HIDDEN="no" wait "$pid"; rc=$? printf '\r\033[K' >&2 return $rc } run_step() { local present="$1" done_msg="$2"; shift 2 local size_file="" start end dur size_str="" # If first remaining arg looks like a file path, treat as size_file case "${1:-}" in /*) size_file="$1"; shift ;; esac start=$(date +%s) if [ "$QUIET" != "yes" ]; then printf '%s' "$present" >&2; fi "$@" & local pid=$! if ! show_spinner "$pid" "$present"; then if [ "$QUIET" != "yes" ]; then printf '%s %s %s\n' "$(fail_sym)" "Failed to" "$done_msg" >&2 fi exit 1 fi end=$(date +%s); dur=$(( end - start )) [ -n "$size_file" ] && [ -f "$size_file" ] && size_str=" ($(get_file_size "$size_file")," if [ "$QUIET" != "yes" ]; then if [ -n "$size_str" ]; then printf '%s %s%s %ss)\n' "$(ok_sym)" "$done_msg" "$size_str" "$dur" >&2 else printf '%s %s (%ss)\n' "$(ok_sym)" "$done_msg" "$dur" >&2 fi fi } # ═══════════════════════════════════════════════════════════════ # Platform Detection # ═══════════════════════════════════════════════════════════════ get_architecture() { local _ostype _cputype _arch _ostype="$(uname -s)"; _cputype="$(uname -m)" if [ "$_ostype" = Darwin ]; then if [ "$_cputype" = x86_64 ]; then # macOS Rosetta 2: x86_64 process on Apple Silicon # Prefer native arm64 binary if the chip supports it if sysctl hw.optional.arm64 2>/dev/null | grep -q ': 1'; then _cputype=arm64 fi fi _ostype=macos elif [ "$_ostype" = Linux ]; then _ostype=linux else printf '%s ' "$(fail_sym)" >&2; err "unsupported OS: $_ostype"; exit 1 fi case "$_cputype" in aarch64|arm64) _cputype=aarch64 ;; x86_64|x86-64|x64|amd64) _cputype=x86_64 ;; *) printf '%s ' "$(fail_sym)" >&2; err "unsupported CPU type: $_cputype"; exit 1 ;; esac _arch="${_cputype}_${_ostype}"; echo "$_arch" } # ═══════════════════════════════════════════════════════════════ # Dependency Checks # ═══════════════════════════════════════════════════════════════ check_rust() { if ! command -v rustc >/dev/null 2>&1 || ! command -v cargo >/dev/null 2>&1; then printf ' %-14s %s %s\n' "Rust" "$(fail_sym)" "not found" >&2 printf ' Install: %s\n' "rust-lang.org/tools/install" >&2 return 1 fi local ver ver=$(rustc --version 2>/dev/null | awk '{print $2}') ver="${ver:-(unknown)}" printf ' %-14s %s %s\n' "Rust" "$(ok_sym)" "$ver" >&2 } check_solana() { if ! command -v solana >/dev/null 2>&1; then printf ' %-14s %s %s\n' "Solana" "$(fail_sym)" "not found" >&2 printf ' Install: %s\n' "solana.com/docs/intro/installation" >&2 return 1 fi local ver ver=$(solana --version 2>/dev/null | awk '{print $2}') ver="${ver:-(unknown)}" printf ' %-14s %s %s\n' "Solana" "$(ok_sym)" "$ver" >&2 } check_yarn() { if ! command -v yarn >/dev/null 2>&1; then printf ' %-14s %s %s\n' "Yarn" "$(fail_sym)" "not found" >&2 printf ' Install: %s\n' "yarnpkg.com/getting-started/install" >&2 return 1 fi local ver ver=$(yarn --version 2>/dev/null) ver="${ver:-(unknown)}" printf ' %-14s %s %s\n' "Yarn" "$(ok_sym)" "$ver" >&2 } check_anchor() { if ! command -v anchor >/dev/null 2>&1; then printf ' %-14s %s %s\n' "Anchor" "$(fail_sym)" "not found" >&2 printf ' Install: %s\n' "anchor-lang.com/docs" >&2 return 1 fi local ver ver=$(anchor --version 2>/dev/null | awk '{print $2}') ver="${ver:-(unknown)}" printf ' %-14s %s %s\n' "Anchor" "$(ok_sym)" "$ver" >&2 } check_docker() { if ! command -v docker >/dev/null 2>&1; then printf ' %-14s %s %s\n' "Docker" "$(fail_sym)" "not found" >&2 printf ' Install: %s\n' "docs.docker.com/get-started/get-docker" >&2 return 1 fi if ! docker compose version >/dev/null 2>&1; then printf ' %-14s %s %s\n' "Docker Compose" "$(fail_sym)" "not found" >&2 printf ' Install: %s\n' "docs.docker.com/get-started/get-docker" >&2 return 1 fi local ver ver=$(docker --version 2>/dev/null | awk '{print $3}' | tr -d ',') ver="${ver:-(unknown)}" printf ' %-14s %s %s\n' "Docker" "$(ok_sym)" "$ver" >&2 } install_linux_deps() { if [ "$(uname -s)" != "Linux" ]; then return 0; fi if command -v apt-get >/dev/null 2>&1; then if ! sudo -n true 2>/dev/null; then printf '%s ' "$(fail_sym)" >&2; err "Needs sudo to install Linux deps (apt). Or install: pkg-config build-essential libudev-dev libssl-dev"; exit 1; fi sudo apt-get update sudo apt-get install -y pkg-config build-essential libudev-dev libssl-dev fi } check_path() { # Check if INSTALL_DIR is an exact component of PATH (not substring) local found=false # Save/restore IFS to avoid side effects local old_ifs="$IFS" IFS=':' set -f # Disable globbing for p in $PATH; do if [ "$p" = "$INSTALL_DIR" ]; then found=true break fi done set +f IFS="$old_ifs" if [ "$found" = "false" ]; then warn "~/.cargo/bin not in PATH. Add: export PATH=\"\$HOME/.cargo/bin:\$PATH\"" fi } # ═══════════════════════════════════════════════════════════════ # HTTP & Download # ═══════════════════════════════════════════════════════════════ # Get human-readable file size (works on macOS and Linux) get_file_size() { local file="$1" size if [ ! -f "$file" ]; then echo "0 B"; return; fi # macOS stat vs Linux stat size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null || echo 0) if [ "$size" -ge 1048576 ]; then printf '%.1f MB' "$(echo "scale=1; $size/1048576" | bc 2>/dev/null || echo "$((size/1048576))")" elif [ "$size" -ge 1024 ]; then printf '%.0f KB' "$(echo "scale=0; $size/1024" | bc 2>/dev/null || echo "$((size/1024))")" else printf '%d B' "$size" fi } _get_http_tool() { if command -v curl >/dev/null 2>&1; then echo curl elif command -v wget >/dev/null 2>&1; then echo wget else printf '%s ' "$(fail_sym)" >&2; err "need curl or wget"; exit 1 fi } http_get() { local url="$1" output="${2:-}" tool tool=$(_get_http_tool) || return 1 case "$tool" in curl) if [ -n "$output" ]; then curl --proto '=https' --tlsv1.2 --silent --fail --location --max-time 30 "$url" --output "$output" else curl --proto '=https' --tlsv1.2 --silent --fail --location --max-time 30 "$url" fi ;; wget) if [ -n "$output" ]; then wget --https-only --secure-protocol=TLSv1_2 --quiet --timeout=30 "$url" -O "$output" else wget --https-only --secure-protocol=TLSv1_2 -q --timeout=30 -O - "$url" fi ;; esac } run_with_retry() { local attempt=1 rc=0 while [ "$attempt" -le "$MAX_RETRIES" ]; do if "$@"; then return 0; fi rc=$? [ "$rc" -eq 130 ] && exit 130 [ "$attempt" -lt "$MAX_RETRIES" ] && sleep "$RETRY_DELAY" attempt=$((attempt + 1)) done printf '%s ' "$(fail_sym)" >&2; err "Command failed after $MAX_RETRIES attempts: $*"; return 1 } # Validate version format (security: prevent path traversal) validate_version() { local version="$1" pattern="$2" # Use grep for proper regex validation instead of shell glob if ! printf '%s' "$version" | grep -qE "^${pattern}$"; then printf '%s ' "$(fail_sym)" >&2 err "Invalid version format: $version" exit 1 fi } fetch_latest_version() { local major_minor_url="${ARCUP_BASE_URL}/versions/latest-tooling" local major_minor patch_version full_version_url major_minor=$(http_get "$major_minor_url" | tr -d '\n\r') [ -z "$major_minor" ] && { printf '%s ' "$(fail_sym)" >&2; err "Failed to fetch version at $major_minor_url"; exit 1; } # Validate format: X.Y (digits only) validate_version "$major_minor" '[0-9]+\.[0-9]+' full_version_url="${ARCUP_BASE_URL}/versions/latest-tooling-${major_minor}" patch_version=$(http_get "$full_version_url" | tr -d '\n\r') [ -z "$patch_version" ] && { printf '%s ' "$(fail_sym)" >&2; err "Failed to fetch patch at $full_version_url"; exit 1; } # Validate format: digits only validate_version "$patch_version" '[0-9]+' ARCUP_VERSION="${major_minor}.${patch_version}" if [ "$QUIET" != "yes" ]; then printf '%s Found arcup %s\n' "$(ok_sym)" "$ARCUP_VERSION" >&2; fi } # ═══════════════════════════════════════════════════════════════ # Installation # ═══════════════════════════════════════════════════════════════ download_arcup() { local target="$1" local url="${ARCUP_BASE_URL}/arcup_${target}_${ARCUP_VERSION}" local temp_dir temp_binary # First check if directory exists and we can write if [ ! -d "$INSTALL_DIR" ]; then mkdir -p "$INSTALL_DIR" || { printf '%s ' "$(fail_sym)" >&2 err "Cannot create $INSTALL_DIR - check permissions" exit 1 } fi if [ ! -w "$INSTALL_DIR" ]; then printf '%s ' "$(fail_sym)" >&2 err "Cannot write to $INSTALL_DIR - check permissions" exit 1 fi temp_dir="$(mktemp -d)" if [ -z "$temp_dir" ] || [ ! -d "$temp_dir" ]; then printf '%s ' "$(fail_sym)" >&2 err "Failed to create temporary directory" exit 1 fi _CLEANUP_TEMP_DIR="$temp_dir" temp_binary="${temp_dir}/arcup" # Download binary run_step "Downloading arcup" "Downloaded arcup" "$temp_binary" run_with_retry http_get "$url" "$temp_binary" # Validate downloaded file has content if [ ! -s "$temp_binary" ]; then printf '%s ' "$(fail_sym)" >&2 err "Download produced empty file" exit 1 fi if ! mv "$temp_binary" "$INSTALL_DIR/arcup"; then printf '%s ' "$(fail_sym)" >&2 err "Failed to move arcup to $INSTALL_DIR" exit 1 fi if ! chmod +x "$INSTALL_DIR/arcup"; then printf '%s ' "$(fail_sym)" >&2 err "Failed to make arcup executable" exit 1 fi # Cleanup successful - clear temp dir from cleanup handler _CLEANUP_TEMP_DIR="" rm -rf "$temp_dir" } install_arcium_cli() { run_step "Installing Arcium CLI" "Installed Arcium CLI" run_with_retry "$INSTALL_DIR/arcup" install } verify_installation() { command -v arcup >/dev/null 2>&1 || { printf ' %-14s %s %s\n' "arcup" "$(fail_sym)" "not found" >&2; check_path; exit 1; } command -v arcium >/dev/null 2>&1 || { printf ' %-14s %s %s\n' "arcium" "$(fail_sym)" "not found (try: arcup install)" >&2; exit 1; } printf ' %-14s %s %s\n' "arcup" "$(ok_sym)" "ready" >&2 printf ' %-14s %s %s\n' "arcium" "$(ok_sym)" "ready" >&2 } show_success() { local arcium_ver arcium_ver=$(arcium --version 2>/dev/null | awk '{print $2}') arcium_ver="${arcium_ver:-(unknown)}" say "" say "Installation complete" say " arcup $ARCUP_VERSION" say " arcium $arcium_ver" say "" say "Get started: arcium init hello-world" say " Tutorial: docs.arcium.com/developers/hello-world" } # ═══════════════════════════════════════════════════════════════ # Main # ═══════════════════════════════════════════════════════════════ main() { if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then cat <&2 Arcium installation script Usage: $0 [OPTIONS] -q, --quiet Suppress non-error output -h, --help Show this help EOF exit 0 fi if [ "${1:-}" = "-q" ] || [ "${1:-}" = "--quiet" ]; then QUIET=yes; shift || true; fi show_banner say "Installing Arcium tooling" need_cmd uname; need_cmd mktemp; need_cmd chmod; need_cmd mkdir; need_cmd mv install_linux_deps step "Checking dependencies" local ok=true check_rust || ok=false check_solana || ok=false check_yarn || ok=false check_anchor || ok=false check_docker || ok=false if [ "$ok" = false ]; then printf '%s ' "$(fail_sym)" >&2; err "Missing dependencies. Install the tools above and re-run."; exit 1; fi step "Downloading arcup" fetch_latest_version local target target="$(get_architecture)"; say "Detected platform: $target" download_arcup "$target" step "Installing Arcium CLI" install_arcium_cli step "Verifying" verify_installation check_path show_success } main "$@"