#!/bin/bash set -euo pipefail # Single temp dir for all downloads/intermediates — wiped on exit. # Using a directory (not a per-file array) avoids losing track of files # created inside command substitutions, since subshells can't propagate # array mutations back to the parent. TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/perfbase-install.XXXXXX") || { echo "[PERFBASE] [ERROR] Failed to create temporary directory" >&2 exit 1 } cleanup() { [ -n "${TEMP_DIR:-}" ] && [ -d "$TEMP_DIR" ] && rm -rf "$TEMP_DIR" } # EXIT handles any normal exit. The signal traps below convert each # signal into a regular exit (with the conventional 128+signo code), # which then triggers the EXIT trap and runs cleanup exactly once. trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM trap 'exit 129' HUP # Color output (disable if not a terminal) if [ -t 2 ]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' NC='\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' NC='' fi log_info() { echo -e "${YELLOW}[PERFBASE] ${GREEN}[INFO]${NC} $1" >&2 } log_warn() { echo -e "${YELLOW}[PERFBASE] ${YELLOW}[WARN]${NC} $1" >&2 } log_error() { echo -e "${YELLOW}[PERFBASE] ${RED}[ERROR]${NC} $1" >&2 exit 1 } show_help() { echo -e "${BLUE}Perfbase PHP Extension Installer${NC}" echo "" echo "Usage: $0 [options]" echo "" echo "Options:" echo " -d, --dev Install development version" echo " -v, --version NUM Install specific version (overrides default)" echo " --no-verify Skip checksum verification" echo " -h, --help Show this help message" echo "" echo "Exit codes:" echo " 0 Installation successful and verified" echo " 1 Installation failed" echo " 2 Installation completed but verification failed (e.g. container builds)" echo "" echo "Examples:" echo " $0 # Install stable release version" echo " $0 --dev # Install latest development version" echo " $0 --version v142 # Install specific version v142" } # Detect the architecture. # Returns: "amd64" | "arm64". Errors on unsupported. detect_arch() { local arch arch=$(uname -m) case $arch in aarch64|arm64) echo "arm64" ;; x86_64|amd64) echo "amd64" ;; *) log_error "Unsupported architecture: $arch" ;; esac } # Detect the operating system. # Returns: "linux" | "darwin". Errors on unsupported. detect_os() { local os os=$(uname -s | tr '[:upper:]' '[:lower:]') case $os in linux) echo "linux" ;; darwin) echo "darwin" ;; *) log_error "Unsupported operating system: $os" ;; esac } # Detect the C library on Linux. Probes loader glob first, then ldd, then # the Alpine release file — covers minimal images that lack ldd. # Returns: "musl" | "gnu" | "" (non-Linux). detect_libc() { local os=$1 if [ "$os" != "linux" ]; then echo "" return fi if find /lib -maxdepth 1 -name 'ld-musl-*' -print -quit 2>/dev/null | grep -q .; then echo "musl" elif command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi "musl"; then echo "musl" elif [ -f /etc/alpine-release ]; then echo "musl" else echo "gnu" fi } # Detect PHP version detect_php_version() { if ! command -v php >/dev/null 2>&1; then log_error "PHP is not installed or not in PATH. Troubleshooting: - Install PHP: apt-get install php-cli (Ubuntu/Debian) or yum install php-cli (CentOS/RHEL) - Ensure PHP is in your PATH: which php - Try specifying the full path to PHP if installed in a non-standard location" fi local php_version php_version=$(php -r "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;" 2>/dev/null) || true if [ -z "$php_version" ]; then log_error "Failed to detect PHP version. Ensure PHP is working correctly: php --version" fi echo "$php_version" } # Validate PHP version is supported validate_php_version() { local version=$1 local supported_versions=("7.4" "8.0" "8.1" "8.2" "8.3" "8.4" "8.5") for supported in "${supported_versions[@]}"; do if [ "$version" = "$supported" ]; then return 0 fi done log_error "Unsupported PHP version: $version. Supported versions: ${supported_versions[*]}" } # Find the PHP extension directory find_extension_dir() { local ext_dir ext_dir=$(php -r "echo ini_get('extension_dir');" 2>/dev/null) || true if [ -z "$ext_dir" ]; then log_error "Unable to determine PHP extension directory. Troubleshooting: - Verify PHP is properly installed: php --version - Check PHP configuration: php --ini - Ensure php-dev package is installed for development headers" fi echo "$ext_dir" } # Compute SHA-256 hash portably (works on Linux and macOS) compute_sha256() { local file=$1 if command -v sha256sum >/dev/null 2>&1; then sha256sum "$file" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$file" | awk '{print $1}' else log_error "No SHA-256 tool available. Install coreutils (sha256sum) or use macOS shasum." fi } # Find all PHP configuration directories for all SAPIs find_config_dirs() { local php_version=$1 local config_dirs=() local seen="" # Helper to add a directory if it exists and hasn't been seen _add_if_exists() { local dir=$1 if [ -d "$dir" ]; then # Dedup via string matching (portable, no associative arrays needed) case "$seen" in *"|${dir}|"*) return ;; esac config_dirs+=("$dir") seen="${seen}|${dir}|" fi } # Common SAPI names to check local sapis=("cli" "fpm" "apache2" "cgi" "phpdbg" "embed") # Check versioned SAPI directories (e.g., /etc/php/8.3/fpm/conf.d) for sapi in "${sapis[@]}"; do _add_if_exists "/etc/php/${php_version}/${sapi}/conf.d" done # Check generic locations as fallbacks _add_if_exists "/etc/php/${php_version}/conf.d" _add_if_exists "/etc/php/conf.d" # macOS Homebrew: versioned formulae (`php@8.3`) put conf.d under the # versioned dir on both architectures. /usr/local is the Intel prefix, # /opt/homebrew is the Apple Silicon prefix. _add_if_exists "/usr/local/etc/php/${php_version}/conf.d" _add_if_exists "/usr/local/etc/php/conf.d" _add_if_exists "/opt/homebrew/etc/php/${php_version}/conf.d" _add_if_exists "/opt/homebrew/etc/php/conf.d" # If no directories found, try PHP_CONFIG_FILE_SCAN_DIR if [ ${#config_dirs[@]} -eq 0 ]; then local ini_dir ini_dir=$(php -r "echo PHP_CONFIG_FILE_SCAN_DIR;" 2>/dev/null) || true if [ -n "$ini_dir" ] && [ -d "$ini_dir" ]; then config_dirs+=("$ini_dir") fi fi # If still no directories found, error out if [ ${#config_dirs[@]} -eq 0 ]; then log_error "Unable to determine PHP configuration directory. Tried common locations for PHP ${php_version}." fi # Return as newline-separated list printf "%s\n" "${config_dirs[@]}" } # Get version from remote file fetch_version() { local is_dev=$1 local file file=$([ "$is_dev" = "true" ] && echo "latest.txt" || echo "stable.txt") local url="https://cdn.perfbase.com/extension/${file}" local temp_path temp_path=$(create_temp_file "perfbase-version") download_file "$url" "$temp_path" local version version=$(tr -d '\r\n\t ' < "$temp_path" | head -c 40) # Clean and limit input # Validate version format: v-prefixed run number (current), numeric (legacy), or hex SHA (legacy) if [[ ! "$version" =~ ^(v[0-9]+|[0-9a-f]+)$ ]]; then log_error "Invalid version format from $url. Expected v, numeric, or hex version, got: '$version'" fi if [ -z "$version" ]; then log_error "Empty version from $url" fi echo "$version" } # Construct download URL based on system information. One artifact per # (php × os × arch × libc) — no microarchitecture variants. construct_url() { local php_version=$1 local os=$2 local arch=$3 local libc=$4 local version=$5 local is_dev=$6 local build_type build_type=$([ "$is_dev" = "true" ] && echo "debug" || echo "release") local url if [ "$os" = "darwin" ]; then url="https://cdn.perfbase.com/extension/${version}/perfbase-${php_version}-${os}-${arch}-${build_type}.dylib" elif [ "$os" = "linux" ]; then url="https://cdn.perfbase.com/extension/${version}/perfbase-${php_version}-${os}-${arch}-${libc}-${build_type}.so" else log_error "Unsupported OS for URL construction: $os" fi echo "$url" } # Create a temp file inside TEMP_DIR — cleaned up by the parent trap. create_temp_file() { local prefix="$1" local temp_file temp_file=$(mktemp "${TEMP_DIR}/${prefix}.XXXXXX") || log_error "Failed to create temporary file" echo "$temp_file" } # Detect which download tool to use (called once, cached) _DOWNLOAD_TOOL="" detect_download_tool() { if [ -n "$_DOWNLOAD_TOOL" ]; then echo "$_DOWNLOAD_TOOL" return fi if command -v curl >/dev/null 2>&1; then _DOWNLOAD_TOOL="curl" elif command -v wget >/dev/null 2>&1; then _DOWNLOAD_TOOL="wget" else log_error "Neither curl nor wget is available. Cannot download perfbase extension. Troubleshooting: - Install curl: apt-get install curl (Ubuntu/Debian) or yum install curl (CentOS/RHEL) - Install wget: apt-get install wget (Ubuntu/Debian) or yum install wget (CentOS/RHEL)" fi echo "$_DOWNLOAD_TOOL" } # Download a file with validation and retry download_file() { local url="$1" local output="$2" local max_retries=3 local retry_delay=2 local max_size_bytes=52428800 # 50MB limit (50 * 1024 * 1024) local tool tool=$(detect_download_tool) for ((i=1; i<=max_retries; i++)); do if [ $i -eq 1 ]; then log_info "Downloading $url..." else log_info "Downloading $url (attempt $i/$max_retries)..." fi local download_ok=false if [ "$tool" = "curl" ]; then if curl -fsSL \ --max-filesize "$max_size_bytes" \ --proto '=https' \ --tlsv1.2 \ --connect-timeout 30 \ --max-time 300 \ --retry 0 \ -H "User-Agent: Perfbase-Installer/1.0" \ -o "$output" \ "$url"; then download_ok=true fi else if wget --quiet \ --timeout=30 \ --tries=1 \ --secure-protocol=TLSv1_2 \ --user-agent="Perfbase-Installer/1.0" \ -O "$output" \ "$url" 2>/dev/null; then download_ok=true fi fi if [ "$download_ok" = "true" ]; then # Validate download if [ -f "$output" ] && [ -s "$output" ]; then return 0 fi log_warn "Downloaded file is empty or missing" fi # Retry logic if [ $i -lt $max_retries ]; then log_warn "Download failed, retrying in $((retry_delay * i)) seconds..." sleep $((retry_delay * i)) fi done log_error "Failed to download $url after $max_retries attempts. Please check your internet connection and try again." } # Main installation function install_perfbase() { local is_dev=$1 local specific_version=$2 local no_verify=$3 # Get version based on flags local version if [ -z "$specific_version" ]; then log_info "Fetching latest $([ "$is_dev" = "true" ] && echo "development" || echo "stable") version..." version=$(fetch_version "$is_dev") log_info "Found version: $version" else version=$specific_version fi # Get system information local php_version os arch libc php_version=$(detect_php_version) os=$(detect_os) arch=$(detect_arch) libc=$(detect_libc "$os") # Validate PHP version validate_php_version "$php_version" # Find directories local ext_dir ext_dir=$(find_extension_dir) local config_dirs=() while IFS= read -r line; do config_dirs+=("$line") done < <(find_config_dirs "$php_version") # Verify directories exist if [ ! -d "$ext_dir" ]; then log_error "PHP extension directory $ext_dir does not exist." fi if [ ${#config_dirs[@]} -eq 0 ]; then log_error "No PHP configuration directories found." fi # Construct file paths local file_ext file_ext=$([ "$os" = "darwin" ] && echo "dylib" || echo "so") local install_path="${ext_dir}/perfbase.${file_ext}" # Construct URL local build_type build_type=$([ "$is_dev" = "true" ] && echo "Development" || echo "Release") local download_url download_url=$(construct_url "$php_version" "$os" "$arch" "$libc" "$version" "$is_dev") # Display installation information log_info "Installing Perfbase extension ${version} (${build_type})" log_info "System detected: PHP ${php_version}, ${os} (${arch})" if [ "$os" = "linux" ]; then log_info "C library: ${libc}" fi log_info "Download URL: ${download_url}" log_info "Installation path: ${install_path}" log_info "Configuration directories found: ${#config_dirs[@]}" for config_dir in "${config_dirs[@]}"; do log_info " - ${config_dir}" done # Create secure temporary files local download_path download_path=$(create_temp_file "perfbase-extension") # Download the extension download_file "$download_url" "$download_path" if [ "$no_verify" = "true" ]; then log_info "Skipping checksum verification." else log_info "Verifying checksum..." local checksum_url="${download_url}.sha256sum.txt" local checksum_file checksum_file=$(create_temp_file "perfbase-checksum") download_file "$checksum_url" "$checksum_file" local actual_hash expected_hash actual_hash=$(compute_sha256 "$download_path") expected_hash=$(cut -d ' ' -f1 "$checksum_file") # Validate expected hash format if [[ ! "$expected_hash" =~ ^[a-f0-9]{64}$ ]]; then log_error "Invalid checksum format from ${checksum_url}. Expected 64-character hex string, got: '$expected_hash'" fi if [ "$actual_hash" != "$expected_hash" ]; then log_error "Checksum verification failed for ${download_path} Expected: ${expected_hash} Actual: ${actual_hash} Troubleshooting: - Verify your internet connection is stable - Check if you're behind a proxy that might modify downloads - Try running with --no-verify flag to skip verification (not recommended) - Report this issue if the problem persists" else log_info "Checksum verification passed." fi fi # Back up existing extension if present if [ -f "$install_path" ]; then local backup_path="${install_path}.bak" log_info "Existing extension found, backing up to ${backup_path}" if [ -w "$ext_dir" ]; then cp "$install_path" "$backup_path" elif command -v sudo >/dev/null 2>&1; then sudo cp "$install_path" "$backup_path" fi fi # Install the extension log_info "Installing extension..." if [ -w "$ext_dir" ]; then if ! cp "$download_path" "$install_path"; then log_error "Failed to install extension to $install_path. Troubleshooting: - Check available disk space: df -h $ext_dir - Verify directory permissions: ls -ld $ext_dir - Ensure you have write access to the PHP extension directory" fi else if command -v sudo >/dev/null 2>&1; then if ! sudo cp "$download_path" "$install_path"; then log_error "Failed to install extension to $install_path using sudo. Troubleshooting: - Check available disk space: df -h $ext_dir - Verify sudo permissions: sudo -l - Ensure the target directory exists and is accessible" fi else log_error "No write permissions to $ext_dir and sudo is not available. Troubleshooting: - Install sudo or run this script as root - Change ownership of $ext_dir to your user - Use a package manager to install the extension instead" fi fi # Set permissions if [ -w "$(dirname "$install_path")" ]; then chmod 644 "$install_path" elif command -v sudo >/dev/null 2>&1; then sudo chmod 644 "$install_path" else log_error "Cannot set permissions for $install_path; no write permissions and sudo is not available." fi # Create the configuration file in all found directories log_info "Creating configuration files..." local config_installed=0 local config_failed=0 local ini_content ini_content="extension=perfbase.${file_ext}" for config_dir in "${config_dirs[@]}"; do local config_path="${config_dir}/perfbase.ini" # Skip if config already has the correct content if [ -f "$config_path" ] && [ "$(cat "$config_path" 2>/dev/null)" = "$ini_content" ]; then log_info " [ok] Config already correct: ${config_path}" config_installed=$((config_installed + 1)) continue fi if [ -w "$config_dir" ]; then if printf "%s\n" "$ini_content" > "$config_path"; then log_info " [ok] Created config: ${config_path}" config_installed=$((config_installed + 1)) else log_warn " [fail] Failed to create config: ${config_path}" config_failed=$((config_failed + 1)) fi elif command -v sudo >/dev/null 2>&1; then if printf "%s\n" "$ini_content" | sudo tee "$config_path" > /dev/null; then log_info " [ok] Created config (sudo): ${config_path}" config_installed=$((config_installed + 1)) else log_warn " [fail] Failed to create config with sudo: ${config_path}" config_failed=$((config_failed + 1)) fi else log_warn " [fail] No write permissions for ${config_dir} and sudo not available" config_failed=$((config_failed + 1)) fi done # Check if at least one config was created if [ $config_installed -eq 0 ]; then log_error "Failed to create configuration file in any directory. Troubleshooting: - Check available disk space: df -h - Verify directory permissions for found config directories - Install sudo or run this script as root - Manually create perfbase.ini with content: extension=perfbase.${file_ext}" fi if [ $config_failed -gt 0 ]; then log_warn "Created ${config_installed} config file(s), but ${config_failed} failed" else log_info "Successfully created ${config_installed} config file(s)" fi # Verify installation log_info "Verifying installation..." # Capture PHP errors during module load local php_errors php_errors=$(php -d display_errors=stderr -d error_reporting=E_ALL -m 2>&1 >/dev/null) || true if (php -m 2>/dev/null || true) | grep -q "perfbase"; then log_info "Perfbase extension ${version} (${build_type}) installed successfully!" log_info "The extension is now active and ready to use." else # Extension not loaded - provide detailed diagnostics log_warn "Extension installed but not loaded by PHP." # Show PHP errors if any if [ -n "$php_errors" ]; then log_warn "PHP errors detected:" echo "$php_errors" | head -20 >&2 fi # Check if extension file exists and has correct permissions if [ ! -f "$install_path" ]; then log_error "Extension file missing: $install_path" elif [ ! -r "$install_path" ]; then log_error "Extension file not readable: $install_path" fi # List installed config files for verification log_info "Configuration files created:" for config_dir in "${config_dirs[@]}"; do local config_path="${config_dir}/perfbase.ini" if [ -f "$config_path" ]; then log_info " [ok] ${config_path}: $(cat "$config_path")" else log_warn " [missing] ${config_path}" fi done log_warn "Extension installed but verification failed. Note: This may be normal in containerized environments where the extension will be loaded when the final container runs. The extension files are installed correctly at: - Extension: ${install_path} - Config files: ${config_installed} location(s) To verify manually, run: php -m | grep perfbase If the extension still doesn't load: - Check PHP error logs for detailed errors - Verify architecture compatibility (${arch}) - Try loading explicitly: php -d extension=${install_path} -m For additional help, visit: https://docs.perfbase.com/installation" # Exit 2 to distinguish "installed but unverifiable" from hard failures (exit 1) exit 2 fi } # Parse command line arguments IS_DEV="false" NO_VERIFY="false" SPECIFIC_VERSION="" while [[ $# -gt 0 ]]; do case $1 in -d|--dev) IS_DEV="true" shift ;; -v|--version) if [ -z "${2:-}" ]; then log_error "--version requires a version number argument." fi SPECIFIC_VERSION="$2" # Validate version argument: v-prefixed run number (current), numeric (legacy), or hex SHA (legacy) if [[ ! "$SPECIFIC_VERSION" =~ ^(v[0-9]+|[0-9a-f]+)$ ]]; then log_error "Invalid version argument: '$SPECIFIC_VERSION'. Must be v, numeric, or a hex commit SHA." fi shift 2 ;; --no-verify) NO_VERIFY="true" shift ;; -h|--help) show_help exit 0 ;; *) log_warn "Unknown option: $1" show_help exit 1 ;; esac done # Run the installation install_perfbase "$IS_DEV" "$SPECIFIC_VERSION" "$NO_VERIFY"