#!/bin/bash # SPARK Installer Script # Downloads and installs pre-compiled SPARK binary from releases # # Usage: # Direct piped installation (recommended): # curl -fsSL https://code.foss.global/serve.zone/spark/releases/download//install.sh | sudo bash # # With version specification: # curl -fsSL https://code.foss.global/serve.zone/spark/releases/download//install.sh | sudo bash -s -- --version v1.2.2 # # With major version channel: # curl -fsSL https://code.foss.global/serve.zone/spark/releases/download//install.sh | sudo bash -s -- --major 1 # # Options: # -h, --help Show this help message # --version VERSION Install specific version (e.g., v1.2.2) # --major MAJOR Install latest release for a major version (e.g., 1) # --install-dir DIR Installation directory (default: /opt/spark) # --verify-only Verify the selected release asset without installing it # Kept sourceable so the real installation phase can be tested using disposable # paths and a fixture service manager. The CLI alone performs privilege checks. install_verified_asset() { local verified_file="$1" local install_dir="$2" local bin_dir="$3" local service_load service_state alternate_load staging_dir SERVICE_UNIT="smartdaemon_spark.service" SERVICE_WAS_RUNNING=0 BINARY_PATH="$install_dir/spark" case "$install_dir" in /*) ;; *) echo "Error: Installation directory must be absolute" >&2; return 1 ;; esac case "$bin_dir" in /*) ;; *) echo "Error: Command directory must be absolute" >&2; return 1 ;; esac if [ ! -d "$bin_dir" ]; then echo "Error: Command directory does not exist" >&2 return 1 fi if [ "$install_dir" = / ] || [ -L "$install_dir" ] \ || [ -L "$BINARY_PATH" ] || { [ -e "$BINARY_PATH" ] && [ ! -f "$BINARY_PATH" ]; }; then echo "Error: Installation target must be a directory with a regular Spark binary" >&2 return 1 fi if [ -L "$bin_dir/spark" ]; then if [ "$(readlink "$bin_dir/spark")" != "$BINARY_PATH" ]; then echo "Error: Existing Spark command points to a different installation" >&2 return 1 fi elif [ -e "$bin_dir/spark" ]; then echo "Error: Existing Spark command is not an owned installation symlink" >&2 return 1 fi # SmartDaemon creates this exact unit. Never delete it, change its enablement, # or infer running intent from enabled state. Another unit is not ours to adopt. if command -v systemctl >/dev/null 2>&1; then if ! alternate_load=$(systemctl show spark.service --property=LoadState --value); then echo "Error: Could not inspect Spark service ownership" >&2 return 1 fi if [ "$alternate_load" != not-found ]; then echo "Error: Noncanonical spark.service exists; resolve its ownership before installing" >&2 return 1 fi if ! service_load=$(systemctl show "$SERVICE_UNIT" --property=LoadState --value); then echo "Error: Could not inspect $SERVICE_UNIT" >&2 return 1 fi case "$service_load" in not-found) ;; loaded) service_state=$(systemctl show "$SERVICE_UNIT" --property=ActiveState --value) || return 1 case "$service_state" in active) SERVICE_WAS_RUNNING=1 ;; inactive|failed) ;; *) echo "Error: Spark service is in a transitional or unknown state" >&2; return 1 ;; esac ;; *) echo "Error: Spark service is not in a supported load state" >&2; return 1 ;; esac fi mkdir -p "$install_dir" || return 1 staging_dir=$(mktemp -d "$install_dir/.spark-install-XXXXXXXX") || return 1 cp "$verified_file" "$staging_dir/spark" || return 1 chmod 0755 "$staging_dir/spark" || return 1 if [ -f "$BINARY_PATH" ]; then cp -p "$BINARY_PATH" "$staging_dir/spark.previous" || return 1 echo "Previous executable retained at: $staging_dir/spark.previous" fi # Establish the command path before stopping a running daemon, so a failed # symlink operation cannot strand that daemon between stop and restart. if [ ! -L "$bin_dir/spark" ]; then ln -s "$BINARY_PATH" "$bin_dir/spark" || return 1 fi if [ "$SERVICE_WAS_RUNNING" -eq 1 ]; then echo "Stopping $SERVICE_UNIT..." systemctl stop "$SERVICE_UNIT" || return 1 fi if ! mv -f "$staging_dir/spark" "$BINARY_PATH"; then echo "Error: Binary replacement failed; previous executable remains installed" >&2 if [ "$SERVICE_WAS_RUNNING" -eq 1 ]; then systemctl start "$SERVICE_UNIT" || return 1 fi return 1 fi if [ "$SERVICE_WAS_RUNNING" -eq 1 ]; then echo "Restarting $SERVICE_UNIT..." if ! systemctl start "$SERVICE_UNIT" || ! systemctl is-active --quiet "$SERVICE_UNIT"; then echo "Error: New Spark executable is installed but the service did not become active" >&2 echo "The unit, configuration, other assets and previous executable are preserved" >&2 return 1 fi fi if [ ! -f "$staging_dir/spark.previous" ]; then rmdir "$staging_dir" fi } run_verified_node_bundle() { # Execute only this verified disposable bootstrap. Preserve the old bare CLI, # service definition, enablement, mask and application data. chmod 700 "$1" "$1" installnodebundle "$2" & NODE_BOOTSTRAP_PID=$! trap 'kill -INT "$NODE_BOOTSTRAP_PID" 2>/dev/null || true; wait "$NODE_BOOTSTRAP_PID" || true; exit 130' INT trap 'kill -TERM "$NODE_BOOTSTRAP_PID" 2>/dev/null || true; wait "$NODE_BOOTSTRAP_PID" || true; exit 143' TERM local bootstrap_status=0 wait "$NODE_BOOTSTRAP_PID" || bootstrap_status=$? trap - INT TERM return "$bootstrap_status" } spark_installer_main() { set -e # Default values SHOW_HELP=0 SPECIFIED_VERSION="" SPECIFIED_MAJOR="" INSTALL_DIR="/opt/spark" VERIFY_ONLY=0 NODE_BUNDLE=0 INSTALL_DIR_SPECIFIED=0 GITEA_BASE_URL="https://code.foss.global" GITEA_REPO="serve.zone/spark" # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in -h|--help) SHOW_HELP=1 shift ;; --version) SPECIFIED_VERSION="$2" shift 2 ;; --major) SPECIFIED_MAJOR="${2#v}" shift 2 ;; --install-dir) INSTALL_DIR="$2" INSTALL_DIR_SPECIFIED=1 shift 2 ;; --verify-only) VERIFY_ONLY=1 shift ;; --node-bundle) NODE_BUNDLE=1 shift ;; *) echo "Unknown option: $1" echo "Use -h or --help for usage information" exit 1 ;; esac done if [ $SHOW_HELP -eq 1 ]; then echo "SPARK Installer Script" echo "Downloads and installs pre-compiled SPARK binary" echo "" echo "Usage: $0 [options]" echo "" echo "Options:" echo " -h, --help Show this help message" echo " --version VERSION Install specific version (e.g., v1.2.2)" echo " --major MAJOR Install latest release for a major version (e.g., 1)" echo " --install-dir DIR Installation directory (default: /opt/spark)" echo " --verify-only Verify the selected release asset without installing it" echo " --node-bundle Bootstrap the complete Linux node bundle; requires --version" echo "" echo "Examples:" echo " # Install latest version" echo " curl -fsSL https://code.foss.global/serve.zone/spark/releases/download//install.sh | sudo bash" echo "" echo " # Install specific version" echo " curl -fsSL https://code.foss.global/serve.zone/spark/releases/download//install.sh | sudo bash -s -- --version v1.2.2" echo "" echo " # Install latest v1.x release" echo " curl -fsSL https://code.foss.global/serve.zone/spark/releases/download//install.sh | sudo bash -s -- --major 1" exit 0 fi if [ "$NODE_BUNDLE" -eq 1 ]; then if [ "$VERIFY_ONLY" -eq 1 ] || [ "$INSTALL_DIR_SPECIFIED" -eq 1 ] || [ -n "$SPECIFIED_MAJOR" ] || ! [[ "${SPECIFIED_VERSION#v}" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then echo "Error: --node-bundle requires one exact --version and rejects --major, --install-dir and --verify-only" exit 1 fi if [ "$(uname -s)" != "Linux" ]; then echo "Error: node bundles require Linux" exit 1 fi SPECIFIED_VERSION="v${SPECIFIED_VERSION#v}" fi if [ -n "$SPECIFIED_VERSION" ] && [ -n "$SPECIFIED_MAJOR" ]; then echo "Error: --version and --major are mutually exclusive" exit 1 fi if [ -n "$SPECIFIED_MAJOR" ] && ! [[ "$SPECIFIED_MAJOR" =~ ^[0-9]+$ ]]; then echo "Error: --major must be a numeric major version, for example: --major 1" exit 1 fi # Check if running as root for installation. Verification does not mutate the host. if [ "$VERIFY_ONLY" -ne 1 ] && [ "$EUID" -ne 0 ]; then echo "Please run as root (sudo bash install.sh or pipe to sudo bash)" exit 1 fi # Helper function to detect OS and architecture detect_platform() { local os=$(uname -s) local arch=$(uname -m) # Map OS case "$os" in Linux) os_name="linux" ;; Darwin) os_name="macos" ;; MINGW*|MSYS*|CYGWIN*) os_name="windows" ;; *) echo "Error: Unsupported operating system: $os" echo "Supported: Linux, macOS, Windows" exit 1 ;; esac # Map architecture case "$arch" in x86_64|amd64) arch_name="x64" ;; aarch64|arm64) arch_name="arm64" ;; *) echo "Error: Unsupported architecture: $arch" echo "Supported: x86_64/amd64 (x64), aarch64/arm64 (arm64)" exit 1 ;; esac # Construct binary name if [ "$os_name" = "windows" ]; then echo "spark-${os_name}-${arch_name}.exe" else echo "spark-${os_name}-${arch_name}" fi } # Get latest release version from Gitea API get_latest_version() { echo "Fetching latest release version from Gitea..." >&2 local api_url="${GITEA_BASE_URL}/api/v1/repos/${GITEA_REPO}/releases/latest" local response if ! response=$(curl --fail --silent --show-error --location "$api_url"); then echo "Error: Failed to fetch latest release information from Gitea API" >&2 echo "URL: $api_url" >&2 exit 1 fi if [ -z "$response" ]; then echo "Error: Gitea API returned an empty latest release response" >&2 exit 1 fi # Extract tag_name from JSON response local version=$(echo "$response" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4) if [ -z "$version" ]; then echo "Error: Could not determine latest version from API response" >&2 exit 1 fi echo "$version" } # Get latest release version for a major version from Gitea API get_latest_major_version() { local major="$1" echo "Fetching latest v${major}.x release version from Gitea..." >&2 local api_url="${GITEA_BASE_URL}/api/v1/repos/${GITEA_REPO}/releases?limit=50" local response if ! response=$(curl --fail --silent --show-error --location "$api_url"); then echo "Error: Failed to fetch release list from Gitea API" >&2 echo "URL: $api_url" >&2 exit 1 fi if [ -z "$response" ]; then echo "Error: Gitea API returned an empty release list response" >&2 exit 1 fi # Gitea returns releases newest-first, so the first matching tag is the channel head. local version=$(printf '%s' "$response" | grep -o '"tag_name":"v'"${major}"'\.[^"]*"' | cut -d'"' -f4 | head -n 1) if [ -z "$version" ]; then echo "Error: Could not determine latest v${major}.x version from API response" >&2 exit 1 fi echo "$version" } # Main installation process echo "================================================" echo " SPARK Installation Script" echo "================================================" echo "" # Detect platform BINARY_NAME=$(detect_platform) echo "Detected platform: $BINARY_NAME" echo "" # Determine version to install if [ -n "$SPECIFIED_VERSION" ]; then VERSION="$SPECIFIED_VERSION" echo "Installing specified version: $VERSION" elif [ -n "$SPECIFIED_MAJOR" ]; then VERSION=$(get_latest_major_version "$SPECIFIED_MAJOR") echo "Installing latest v${SPECIFIED_MAJOR}.x version: $VERSION" else VERSION=$(get_latest_version) echo "Installing latest version: $VERSION" fi echo "" # Construct download URL DOWNLOAD_URL="${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/${VERSION}/${BINARY_NAME}" CHECKSUM_URL="${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/${VERSION}/SHA256SUMS.txt" echo "Download URL: $DOWNLOAD_URL" echo "" # Download and verify before stopping services or replacing an existing installation. TEMP_DIR=$(mktemp -d) cleanup() { rm -rf "$TEMP_DIR" } trap cleanup EXIT TEMP_FILE="$TEMP_DIR/$BINARY_NAME" CHECKSUM_FILE="$TEMP_DIR/SHA256SUMS.txt" echo "Downloading SPARK binary and checksums..." if ! curl --fail --silent --show-error --location "$DOWNLOAD_URL" -o "$TEMP_FILE"; then echo "Error: Failed to download binary from $DOWNLOAD_URL" exit 1 fi if ! curl --fail --silent --show-error --location "$CHECKSUM_URL" -o "$CHECKSUM_FILE"; then echo "Error: Failed to download checksums from $CHECKSUM_URL" exit 1 fi if [ ! -s "$TEMP_FILE" ] || [ ! -s "$CHECKSUM_FILE" ]; then echo "Error: Downloaded binary or checksum file is empty" exit 1 fi CHECKSUM_MATCHES=$(awk -v binary="$BINARY_NAME" 'NF == 2 && ($2 == binary || $2 == "*" binary) { print $1 }' "$CHECKSUM_FILE") CHECKSUM_COUNT=$(printf '%s\n' "$CHECKSUM_MATCHES" | awk 'NF { count++ } END { print count + 0 }') if [ "$CHECKSUM_COUNT" -ne 1 ]; then echo "Error: Expected exactly one checksum for $BINARY_NAME, found $CHECKSUM_COUNT" exit 1 fi EXPECTED_CHECKSUM=$(printf '%s\n' "$CHECKSUM_MATCHES" | awk 'NF { print; exit }') if ! [[ "$EXPECTED_CHECKSUM" =~ ^[[:xdigit:]]{64}$ ]]; then echo "Error: Published checksum for $BINARY_NAME is malformed" exit 1 fi if command -v sha256sum >/dev/null 2>&1; then ACTUAL_CHECKSUM=$(sha256sum "$TEMP_FILE" | awk '{ print $1 }') elif command -v shasum >/dev/null 2>&1; then ACTUAL_CHECKSUM=$(shasum -a 256 "$TEMP_FILE" | awk '{ print $1 }') else echo "Error: sha256sum or shasum is required to verify the SPARK binary" exit 1 fi EXPECTED_CHECKSUM=$(printf '%s' "$EXPECTED_CHECKSUM" | tr '[:upper:]' '[:lower:]') ACTUAL_CHECKSUM=$(printf '%s' "$ACTUAL_CHECKSUM" | tr '[:upper:]' '[:lower:]') if [ "$ACTUAL_CHECKSUM" != "$EXPECTED_CHECKSUM" ]; then echo "Error: SHA256 checksum mismatch for $BINARY_NAME" exit 1 fi echo "Checksum verified: $BINARY_NAME" echo "" if [ "$VERIFY_ONLY" -eq 1 ]; then echo "Release asset verification completed successfully." exit 0 fi if [ "$NODE_BUNDLE" -eq 1 ]; then run_verified_node_bundle "$TEMP_FILE" "${VERSION#v}" echo "Complete node bundle selected. Enrollment and systemd definition installation remain explicit steps." return 0 fi # Check if /usr/local/bin is in PATH if [[ ":$PATH:" == *":/usr/local/bin:"* ]]; then BIN_DIR="/usr/local/bin" else BIN_DIR="/usr/bin" fi install_verified_asset "$TEMP_FILE" "$INSTALL_DIR" "$BIN_DIR" echo "Binary installed successfully to: $BINARY_PATH" echo "Symlink created: $BIN_DIR/spark -> $BINARY_PATH" echo "" echo "================================================" echo " SPARK Installation Complete!" echo "================================================" echo "" echo "Installation details:" echo " Binary location: $BINARY_PATH" echo " Symlink location: $BIN_DIR/spark" echo " Version: $VERSION" echo "" echo "Existing configuration and service enablement were left unchanged." if [ "$SERVICE_WAS_RUNNING" -eq 1 ]; then echo "$SERVICE_UNIT was restarted with its existing settings." else echo "Get started:" echo " spark --version" echo " spark help" echo " spark installdaemon # Install as system daemon" fi echo "" } # BASH_SOURCE is empty for the supported curl | bash entry point. if [ -z "${BASH_SOURCE[0]}" ] || [ "${BASH_SOURCE[0]}" = "$0" ]; then spark_installer_main "$@" fi