#!/usr/bin/env bash set -euo pipefail # TrueAsync PHP — Build from Source for Linux (Ubuntu/Debian) # # Interactive: curl -fsSL https://raw.githubusercontent.com/true-async/releases/master/installer/build-linux.sh | bash # Non-interactive: curl -fsSL ... | EXTENSIONS=all NO_INTERACTIVE=true bash # # Options (CLI args or environment variables): # --prefix DIR Installation directory (INSTALL_DIR, default: $HOME/.php-trueasync) # --set-default Add to PATH as default php (SET_DEFAULT=true, default: false) # --debug Build with debug symbols (DEBUG_BUILD=true, default: false) # --extensions PRESET Extension preset (EXTENSIONS: standard|xdebug|all, default: standard) # --no-xdebug Exclude Xdebug (NO_XDEBUG=true) # --jobs N Parallel make jobs (BUILD_JOBS, default: nproc) # --branch NAME Override php-src branch (PHP_BRANCH) # --no-interactive Skip interactive wizard (NO_INTERACTIVE=true or CI=true) # --help Show this help # ═══════════════════════════════════════════════════════════════════════════════ # Self-relaunch: when piped via curl, save to temp file and re-exec with tty # ═══════════════════════════════════════════════════════════════════════════════ if [[ -z "${__TRUEASYNC_RELAUNCHED:-}" ]] && ! [[ -t 0 ]]; then _tmpscript=$(mktemp) # bash buffers stdin in large chunks when piped, so `cat` would only capture # the tail of the script that bash hasn't yet consumed. Re-download instead. curl -fsSL "https://raw.githubusercontent.com/true-async/releases/master/installer/build-linux.sh" \ -o "$_tmpscript" export __TRUEASYNC_RELAUNCHED=1 exec bash "$_tmpscript" "$@" < /dev/tty fi # ═══════════════════════════════════════════════════════════════════════════════ # Configuration defaults # ═══════════════════════════════════════════════════════════════════════════════ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]+${BASH_SOURCE[0]}}")" 2>/dev/null && pwd || echo /tmp)" CONFIG_FILE="${SCRIPT_DIR}/../build-config.json" INSTALL_DIR="${INSTALL_DIR:-$HOME/.php-trueasync}" SET_DEFAULT="${SET_DEFAULT:-false}" DEBUG_BUILD="${DEBUG_BUILD:-false}" EXTENSIONS="${EXTENSIONS:-standard}" NO_XDEBUG="${NO_XDEBUG:-false}" BUILD_JOBS="${BUILD_JOBS:-$(nproc 2>/dev/null || echo 4)}" PHP_BRANCH="${PHP_BRANCH:-}" NO_INTERACTIVE="${NO_INTERACTIVE:-${CI:-false}}" BUILD_FRANKENPHP="${BUILD_FRANKENPHP:-false}" BUILD_HTTP3="${BUILD_HTTP3:-}" GO_VERSION="1.26.0" LIBUV_VERSION="1.52.1" OPENSSL_VERSION="3.5.0" NGHTTP2_VERSION="1.59.0" NGHTTP3_VERSION="1.15.0" NGTCP2_VERSION="1.22.1" CURL_VERSION="8.12.1" BUILD_LATEST_CURL="${BUILD_LATEST_CURL:-true}" # ═══════════════════════════════════════════════════════════════════════════════ # Colors and UI # ═══════════════════════════════════════════════════════════════════════════════ if [[ -t 1 ]] && [[ "${TERM:-}" != "dumb" ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' MAGENTA='' CYAN='' BOLD='' DIM='' NC='' fi STEP_CURRENT=0 STEP_TOTAL=0 info() { echo -e " ${CYAN}→${NC} $*"; } success() { echo -e " ${GREEN}✓${NC} $*"; } warn() { echo -e " ${YELLOW}⚠${NC} $*"; } error() { echo -e " ${RED}✗${NC} $*" >&2; exit 1; } dimtext() { echo -e " ${DIM}$*${NC}"; } step() { STEP_CURRENT=$((STEP_CURRENT + 1)) echo "" echo -e "${BOLD}${BLUE}[$STEP_CURRENT/$STEP_TOTAL]${NC} ${BOLD}$*${NC}" } spinner() { local pid=$1 local msg="${2:-}" local chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' local i=0 while kill -0 "$pid" 2>/dev/null; do printf "\r ${CYAN}%s${NC} %s" "${chars:i++%${#chars}:1}" "$msg" sleep 0.1 done printf "\r" } run_with_spinner() { local msg="$1" shift local logfile logfile=$(mktemp) "$@" > "$logfile" 2>&1 & local pid=$! spinner "$pid" "$msg" if wait "$pid"; then success "$msg" rm -f "$logfile" return 0 else echo "" error "$msg — failed! Log:\n$(tail -20 "$logfile")" fi } show_banner() { echo "" echo -e "${CYAN}${BOLD}" echo " ╔══════════════════════════════════════════════════════╗" echo " ║ ║" echo " ║ ⚡ TrueAsync PHP — Build from Source ║" echo " ║ Linux (Ubuntu/Debian) ║" echo " ║ ║" echo " ╚══════════════════════════════════════════════════════╝" echo -e "${NC}" } show_summary_box() { local prefix="$1" debug="$2" extensions="$3" xdebug="$4" set_default="$5" jobs="$6" frankenphp="${7:-${DIM}No${NC}}" http3="${8:-${DIM}No${NC}}" local curl_label="${GREEN}Yes (${CURL_VERSION})${NC}" [[ "$BUILD_LATEST_CURL" != "true" ]] && curl_label="${YELLOW}No (system version)${NC}" echo "" echo -e " ${BOLD}Build Configuration Summary${NC}" echo -e " ─────────────────────────────────────" echo -e " Install prefix: ${CYAN}${prefix}${NC}" echo -e " Debug build: ${debug}" echo -e " Latest libcurl: ${curl_label}" echo -e " HTTP/3 support: ${http3}" echo -e " Extensions: ${extensions}" echo -e " Xdebug: ${xdebug}" echo -e " FrankenPHP: ${frankenphp}" echo -e " Set as default: ${set_default}" echo -e " Parallel jobs: ${jobs}" echo "" } # ═══════════════════════════════════════════════════════════════════════════════ # Argument parsing # ═══════════════════════════════════════════════════════════════════════════════ show_help() { echo "TrueAsync PHP — Build from Source (Linux)" echo "" echo "Usage: $0 [options]" echo "" echo "Options:" echo " --prefix DIR Installation directory (default: \$HOME/.php-trueasync)" echo " --set-default Add to PATH as default php" echo " --debug Build with debug symbols" echo " --extensions PRESET Extension preset: standard, xdebug, all (default: standard)" echo " --no-xdebug Exclude Xdebug from build" echo " --jobs N Parallel make jobs (default: $(nproc 2>/dev/null || echo 4))" echo " --branch NAME Override php-src branch" echo " --frankenphp Build FrankenPHP binary (Caddy-based async PHP server, requires Go 1.26+)" echo " --http3 Build HTTP/3 support (OpenSSL 3.5+, nghttp3, ngtcp2)" echo " --no-http3 Skip HTTP/3 support (HTTP/1+2 only)" echo " --no-latest-curl Skip building latest libcurl (async uploads will be synchronous)" echo " --no-interactive Skip interactive wizard" echo " --help Show this help" echo "" echo "Environment variables:" echo " INSTALL_DIR, SET_DEFAULT, DEBUG_BUILD, EXTENSIONS," echo " NO_XDEBUG, BUILD_FRANKENPHP, BUILD_HTTP3, BUILD_JOBS, BUILD_LATEST_CURL, PHP_BRANCH, NO_INTERACTIVE, CI" exit 0 } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --prefix) INSTALL_DIR="$2"; shift 2 ;; --set-default) SET_DEFAULT="true"; shift ;; --debug) DEBUG_BUILD="true"; shift ;; --extensions) EXTENSIONS="$2"; shift 2 ;; --no-xdebug) NO_XDEBUG="true"; shift ;; --jobs) BUILD_JOBS="$2"; shift 2 ;; --branch) PHP_BRANCH="$2"; shift 2 ;; --frankenphp) BUILD_FRANKENPHP="true"; shift ;; --http3) BUILD_HTTP3="true"; shift ;; --no-http3) BUILD_HTTP3="false"; shift ;; --no-latest-curl) BUILD_LATEST_CURL="false"; shift ;; --no-interactive) NO_INTERACTIVE="true"; shift ;; --help|-h) show_help ;; *) error "Unknown option: $1. Use --help for usage." ;; esac done } # ═══════════════════════════════════════════════════════════════════════════════ # Interactive wizard # ═══════════════════════════════════════════════════════════════════════════════ ask_choice() { local prompt="$1" shift local options=("$@") local count=${#options[@]} echo "" echo -e " ${BOLD}${prompt}${NC}" for i in "${!options[@]}"; do echo -e " ${CYAN}$((i + 1))${NC}) ${options[$i]}" done local default="${ASK_CHOICE_DEFAULT:-}" while true; do if [[ -n "$default" ]]; then printf " ${BOLD}▸${NC} Your choice [1-%d] (default: %d): " "$count" "$default" else printf " ${BOLD}▸${NC} Your choice [1-%d]: " "$count" fi read -r choice [[ -z "$choice" && -n "$default" ]] && choice="$default" if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= count )); then return $((choice - 1)) fi echo -e " ${RED}Invalid choice. Please enter 1-${count}${NC}" done } ask_yesno() { local prompt="$1" local default="${2:-n}" local hint="y/N" default_hint="N" if [[ "$default" == "y" ]]; then hint="Y/n" default_hint="Y" fi while true; do printf " ${BOLD}%s${NC} [%s] (default: %s): " "$prompt" "$hint" "$default_hint" read -r answer answer="${answer:-$default}" case "${answer,,}" in y|yes) return 0 ;; n|no) return 1 ;; *) echo -e " ${RED}Please answer y or n${NC}" ;; esac done } ask_input() { local prompt="$1" local default="$2" local __result_var="$3" printf " ${BOLD}%s${NC} [${DIM}%s${NC}]: " "$prompt" "$default" read -r answer printf -v "$__result_var" '%s' "${answer:-$default}" } run_wizard() { echo -e " ${MAGENTA}${BOLD}Build Configuration Wizard${NC}" echo -e " ${DIM}Configure your TrueAsync PHP build${NC}" # 1. Extensions ASK_CHOICE_DEFAULT=2 ask_choice "Which extensions to build?" \ "Standard — async + core extensions" \ "Standard + Xdebug — adds debugging support" \ "Standard + FrankenPHP — adds Caddy-based async server" \ "All — Xdebug + FrankenPHP" \ "Custom — choose manually" local ext_choice=$? case $ext_choice in 0) EXTENSIONS="standard"; NO_XDEBUG="true"; BUILD_FRANKENPHP="false" ;; 1) EXTENSIONS="standard"; NO_XDEBUG="false"; BUILD_FRANKENPHP="false" ;; 2) EXTENSIONS="standard"; NO_XDEBUG="true"; BUILD_FRANKENPHP="true" ;; 3) EXTENSIONS="all"; NO_XDEBUG="false"; BUILD_FRANKENPHP="true" ;; 4) EXTENSIONS="all" echo "" if ask_yesno "Include Xdebug?" "y"; then NO_XDEBUG="false" else NO_XDEBUG="true" fi echo "" if ask_yesno "Build FrankenPHP? (Caddy-based async PHP server, requires Go 1.26+)" "n"; then BUILD_FRANKENPHP="true" else BUILD_FRANKENPHP="false" fi ;; esac # 2. Debug build echo "" if ask_yesno "Enable debug build? (slower, useful for development)" "n"; then DEBUG_BUILD="true" else DEBUG_BUILD="false" fi # 3. HTTP/3 support echo "" if ask_yesno "Build HTTP/3 support? (OpenSSL ${OPENSSL_VERSION}, nghttp3 ${NGHTTP3_VERSION}, ngtcp2 ${NGTCP2_VERSION} — adds ~10 min to build)" "n"; then BUILD_HTTP3="true" else BUILD_HTTP3="false" fi # 4. Build latest libcurl echo "" if ask_yesno "Build latest libcurl ${CURL_VERSION}? (required for fully async file uploads)" "y"; then BUILD_LATEST_CURL="true" else BUILD_LATEST_CURL="false" fi # 5. Install prefix echo "" ask_input "Installation directory" "$INSTALL_DIR" INSTALL_DIR # Check for existing installation if [[ -x "${INSTALL_DIR}/bin/php" ]]; then echo "" local existing_ver existing_ver=$("${INSTALL_DIR}/bin/php" -v 2>/dev/null | head -1 || echo "unknown") warn "Existing PHP found in ${INSTALL_DIR}:" echo -e " ${DIM}${existing_ver}${NC}" echo "" ask_choice "What would you like to do?" \ "Rebuild — remove and build fresh" \ "Cancel — keep existing installation" local overwrite_choice=$? case $overwrite_choice in 1) info "Build cancelled."; exit 0 ;; esac fi # PATH echo "" if ask_yesno "Add to PATH as default php?" "n"; then SET_DEFAULT="true" else SET_DEFAULT="false" fi # Summary local debug_label="${RED}No${NC}" [[ "$DEBUG_BUILD" == "true" ]] && debug_label="${YELLOW}Yes${NC}" local xdebug_label="${DIM}No${NC}" [[ "$NO_XDEBUG" != "true" ]] && xdebug_label="${GREEN}Yes${NC}" local default_label="${DIM}No${NC}" [[ "$SET_DEFAULT" == "true" ]] && default_label="${GREEN}Yes${NC}" local frankenphp_label="${DIM}No${NC}" [[ "$BUILD_FRANKENPHP" == "true" ]] && frankenphp_label="${GREEN}Yes${NC}" local http3_label="${DIM}No (HTTP/1+2 only)${NC}" [[ "$BUILD_HTTP3" == "true" ]] && http3_label="${GREEN}Yes (OpenSSL ${OPENSSL_VERSION} + nghttp3 + ngtcp2)${NC}" show_summary_box "$INSTALL_DIR" "$debug_label" "$EXTENSIONS" "$xdebug_label" "$default_label" "$BUILD_JOBS" "$frankenphp_label" "$http3_label" if ! ask_yesno "Proceed with build?" "y"; then echo "" info "Build cancelled." exit 0 fi } # ═══════════════════════════════════════════════════════════════════════════════ # Config reader # ═══════════════════════════════════════════════════════════════════════════════ read_config() { if [[ ! -f "$CONFIG_FILE" ]]; then # Download config if running from curl pipe CONFIG_FILE=$(mktemp) local config_url="https://raw.githubusercontent.com/true-async/releases/master/build-config.json" if command -v curl &>/dev/null; then curl -fsSL "$config_url" -o "$CONFIG_FILE" elif command -v wget &>/dev/null; then wget -qO "$CONFIG_FILE" "$config_url" else error "Cannot download build-config.json. Install curl or wget." fi fi if ! command -v jq &>/dev/null; then # Minimal JSON parsing without jq PHP_SRC_REPO=$(grep -o '"repo"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_FILE" | head -1 | sed 's/.*"repo"[[:space:]]*:[[:space:]]*"//;s/"//') PHP_SRC_BRANCH=$(grep -o '"branch"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_FILE" | head -1 | sed 's/.*"branch"[[:space:]]*:[[:space:]]*"//;s/"//') ASYNC_REPO=$(sed -n '/"async"/,/}/p' "$CONFIG_FILE" | grep '"repo"' | sed 's/.*"repo"[[:space:]]*:[[:space:]]*"//;s/".*//') ASYNC_BRANCH=$(sed -n '/"async"/,/}/p' "$CONFIG_FILE" | grep '"branch"' | sed 's/.*"branch"[[:space:]]*:[[:space:]]*"//;s/".*//') XDEBUG_REPO=$(sed -n '/"xdebug"/,/}/p' "$CONFIG_FILE" | grep '"repo"' | sed 's/.*"repo"[[:space:]]*:[[:space:]]*"//;s/".*//') XDEBUG_BRANCH=$(sed -n '/"xdebug"/,/}/p' "$CONFIG_FILE" | grep '"branch"' | sed 's/.*"branch"[[:space:]]*:[[:space:]]*"//;s/".*//') FRANKENPHP_REPO=$(sed -n '/"frankenphp"/,/}/p' "$CONFIG_FILE" | grep '"repo"' | head -1 | sed 's/.*"repo"[[:space:]]*:[[:space:]]*"//;s/".*//') FRANKENPHP_BRANCH=$(sed -n '/"frankenphp"/,/}/p' "$CONFIG_FILE" | grep '"branch"' | head -1 | sed 's/.*"branch"[[:space:]]*:[[:space:]]*"//;s/".*//') SERVER_REPO=$(sed -n '/"server"/,/}/p' "$CONFIG_FILE" | grep '"repo"' | head -1 | sed 's/.*"repo"[[:space:]]*:[[:space:]]*"//;s/".*//') SERVER_BRANCH=$(sed -n '/"server"/,/}/p' "$CONFIG_FILE" | grep '"branch"' | head -1 | sed 's/.*"branch"[[:space:]]*:[[:space:]]*"//;s/".*//') else PHP_SRC_REPO=$(jq -r '.php_src.repo' "$CONFIG_FILE") PHP_SRC_BRANCH=$(jq -r '.php_src.branch' "$CONFIG_FILE") ASYNC_REPO=$(jq -r '.extensions.async.repo' "$CONFIG_FILE") ASYNC_BRANCH=$(jq -r '.extensions.async.branch' "$CONFIG_FILE") XDEBUG_REPO=$(jq -r '.extensions.xdebug.repo' "$CONFIG_FILE") XDEBUG_BRANCH=$(jq -r '.extensions.xdebug.branch' "$CONFIG_FILE") FRANKENPHP_REPO=$(jq -r '.frankenphp.repo' "$CONFIG_FILE") FRANKENPHP_BRANCH=$(jq -r '.frankenphp.branch' "$CONFIG_FILE") SERVER_REPO=$(jq -r '.extensions.server.repo' "$CONFIG_FILE") SERVER_BRANCH=$(jq -r '.extensions.server.branch' "$CONFIG_FILE") fi # Override branch if specified if [[ -n "$PHP_BRANCH" ]]; then PHP_SRC_BRANCH="$PHP_BRANCH" fi } # ═══════════════════════════════════════════════════════════════════════════════ # System checks # ═══════════════════════════════════════════════════════════════════════════════ check_system() { # Check we're on Linux if [[ "$(uname -s)" != "Linux" ]]; then error "This script is for Linux. Use build-macos.sh for macOS." fi # Check for apt if ! command -v apt-get &>/dev/null; then error "This script requires apt (Ubuntu/Debian). Your distro is not supported yet." fi # Check for sudo and pre-authenticate if [[ $EUID -ne 0 ]]; then if ! command -v sudo &>/dev/null; then error "sudo is required to install build dependencies." fi info "Sudo access is required to install build dependencies." sudo -v || error "Failed to obtain sudo credentials." fi } # ═══════════════════════════════════════════════════════════════════════════════ # Build steps # ═══════════════════════════════════════════════════════════════════════════════ install_dependencies() { step "Installing build dependencies" local pkgs=( autoconf automake bison re2c pkg-config dos2unix perl gcc g++ make cmake ninja-build git curl wget # Core (no libssl-dev — we build OpenSSL from source) libxml2-dev libsqlite3-dev libargon2-dev # Extensions libedit-dev libreadline-dev libonig-dev libsodium-dev libzip-dev zlib1g-dev libbz2-dev libgmp-dev libicu-dev libxslt1-dev # Image libpng-dev libjpeg-dev libwebp-dev libfreetype6-dev # Database libpq-dev libmysqlclient-dev # Extra libldap2-dev libsasl2-dev libtidy-dev libenchant-2-dev libffi-dev libsnmp-dev libgdbm-dev liblmdb-dev ) local sudo_cmd="" [[ $EUID -ne 0 ]] && sudo_cmd="sudo" run_with_spinner "Updating package lists" $sudo_cmd apt-get update -qq run_with_spinner "Installing ${#pkgs[@]} packages" $sudo_cmd apt-get install -y -qq --no-install-recommends "${pkgs[@]}" } build_openssl() { step "Building OpenSSL ${OPENSSL_VERSION}" local build_dir="$1" # Check if we already have a suitable version in /usr/local if pkg-config --exists openssl 2>/dev/null; then local sys_ver sys_ver=$(pkg-config --modversion openssl 2>/dev/null) local major minor IFS='.' read -r major minor _ <<< "$sys_ver" if (( major > 3 || (major == 3 && minor >= 5) )); then success "OpenSSL ${sys_ver} >= 3.5 found, skipping build" return 0 fi info "OpenSSL ${sys_ver} < 3.5 — building ${OPENSSL_VERSION} for HTTP/3 QUIC API" else info "Building OpenSSL ${OPENSSL_VERSION}..." fi local sudo_cmd="" [[ $EUID -ne 0 ]] && sudo_cmd="sudo" wget -q "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz" \ -O "${build_dir}/openssl.tar.gz" tar -xf "${build_dir}/openssl.tar.gz" -C "$build_dir" local ossl_dir="${build_dir}/openssl-${OPENSSL_VERSION}" run_with_spinner "Configuring OpenSSL" \ bash -c "cd '${ossl_dir}' && ./Configure --prefix=/usr/local --libdir=lib --openssldir=/etc/ssl enable-tls1_3 shared" run_with_spinner "Compiling OpenSSL" \ make -C "${ossl_dir}" -j"$BUILD_JOBS" run_with_spinner "Installing OpenSSL" \ $sudo_cmd make -C "${ossl_dir}" install_sw $sudo_cmd ldconfig success "OpenSSL ${OPENSSL_VERSION} installed" } build_nghttp2() { step "Building nghttp2 ${NGHTTP2_VERSION}" local build_dir="$1" local sudo_cmd="" [[ $EUID -ne 0 ]] && sudo_cmd="sudo" run_with_spinner "Downloading nghttp2" \ bash -c "wget -q 'https://github.com/nghttp2/nghttp2/releases/download/v${NGHTTP2_VERSION}/nghttp2-${NGHTTP2_VERSION}.tar.gz' -O '${build_dir}/nghttp2.tar.gz' && tar -xf '${build_dir}/nghttp2.tar.gz' -C '${build_dir}' && mv '${build_dir}/nghttp2-${NGHTTP2_VERSION}' '${build_dir}/nghttp2'" run_with_spinner "Configuring nghttp2" \ bash -c "cd '${build_dir}/nghttp2' && ./configure --prefix=/usr/local --enable-lib-only" run_with_spinner "Compiling nghttp2" \ make -C "${build_dir}/nghttp2" -j"$BUILD_JOBS" run_with_spinner "Installing nghttp2" \ $sudo_cmd make -C "${build_dir}/nghttp2" install $sudo_cmd ldconfig success "nghttp2 ${NGHTTP2_VERSION} installed" } build_nghttp3() { step "Building nghttp3 ${NGHTTP3_VERSION} + ngtcp2 ${NGTCP2_VERSION}" local build_dir="$1" local sudo_cmd="" [[ $EUID -ne 0 ]] && sudo_cmd="sudo" run_with_spinner "Downloading nghttp3" \ bash -c "wget -q 'https://github.com/ngtcp2/nghttp3/releases/download/v${NGHTTP3_VERSION}/nghttp3-${NGHTTP3_VERSION}.tar.gz' -O '${build_dir}/nghttp3.tar.gz' && tar -xf '${build_dir}/nghttp3.tar.gz' -C '${build_dir}' && mv '${build_dir}/nghttp3-${NGHTTP3_VERSION}' '${build_dir}/nghttp3'" run_with_spinner "Configuring nghttp3" \ bash -c "cd '${build_dir}/nghttp3' && ./configure --prefix=/usr/local --enable-lib-only" run_with_spinner "Compiling nghttp3" \ make -C "${build_dir}/nghttp3" -j"$BUILD_JOBS" run_with_spinner "Installing nghttp3" \ $sudo_cmd make -C "${build_dir}/nghttp3" install $sudo_cmd ldconfig run_with_spinner "Downloading ngtcp2" \ bash -c "wget -q 'https://github.com/ngtcp2/ngtcp2/releases/download/v${NGTCP2_VERSION}/ngtcp2-${NGTCP2_VERSION}.tar.gz' -O '${build_dir}/ngtcp2.tar.gz' && tar -xf '${build_dir}/ngtcp2.tar.gz' -C '${build_dir}' && mv '${build_dir}/ngtcp2-${NGTCP2_VERSION}' '${build_dir}/ngtcp2'" run_with_spinner "Configuring ngtcp2" \ bash -c "cd '${build_dir}/ngtcp2' && \ PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \ ./configure --prefix=/usr/local --with-openssl --enable-lib-only" run_with_spinner "Compiling ngtcp2" \ bash -c "PKG_CONFIG_PATH=/usr/local/lib/pkgconfig make -C '${build_dir}/ngtcp2' -j${BUILD_JOBS}" run_with_spinner "Installing ngtcp2" \ $sudo_cmd make -C "${build_dir}/ngtcp2" install $sudo_cmd ldconfig success "nghttp3 ${NGHTTP3_VERSION} and ngtcp2 ${NGTCP2_VERSION} installed" } build_libuv() { step "Building libuv ${LIBUV_VERSION}" local build_dir="$1" local libuv_dir="${build_dir}/libuv-${LIBUV_VERSION}" if pkg-config --exists libuv 2>/dev/null; then local sys_ver sys_ver=$(pkg-config --modversion libuv 2>/dev/null) info "System libuv found: ${sys_ver}" # Check if system version is recent enough (>= 1.44) local major minor major=$(echo "$sys_ver" | cut -d. -f1) minor=$(echo "$sys_ver" | cut -d. -f2) if (( major >= 1 && minor >= 44 )); then success "System libuv ${sys_ver} is sufficient, skipping build" return 0 fi warn "System libuv ${sys_ver} is too old, building ${LIBUV_VERSION}" fi info "Downloading libuv ${LIBUV_VERSION}..." wget -q "https://github.com/libuv/libuv/archive/v${LIBUV_VERSION}.tar.gz" -O "${build_dir}/libuv.tar.gz" tar -xf "${build_dir}/libuv.tar.gz" -C "$build_dir" mkdir -p "${libuv_dir}/build" run_with_spinner "Configuring libuv" \ cmake -S "${libuv_dir}" -B "${libuv_dir}/build" -G Ninja \ -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF run_with_spinner "Compiling libuv" \ ninja -C "${libuv_dir}/build" local sudo_cmd="" [[ $EUID -ne 0 ]] && sudo_cmd="sudo" run_with_spinner "Installing libuv" \ $sudo_cmd ninja -C "${libuv_dir}/build" install $sudo_cmd ldconfig success "libuv ${LIBUV_VERSION} installed" } build_libcurl() { step "Building libcurl ${CURL_VERSION}" local build_dir="$1" if [[ "$BUILD_LATEST_CURL" != "true" ]]; then info "Skipping libcurl build (BUILD_LATEST_CURL=false)" warn "Async file uploads will use synchronous fallback without libcurl >= 8.11.1" return 0 fi # Check system curl version — need >= 8.11.1 for full async support if pkg-config --exists libcurl 2>/dev/null; then local sys_ver sys_ver=$(pkg-config --modversion libcurl 2>/dev/null) local major minor patch IFS='.' read -r major minor patch <<< "$sys_ver" patch="${patch:-0}" # Need >= 8.11.1 for fully async file uploads (curl#15627) if (( major > 8 || (major == 8 && minor > 11) || (major == 8 && minor == 11 && patch >= 1) )); then success "System libcurl ${sys_ver} is sufficient (>= 8.11.1), skipping build" return 0 fi warn "System libcurl ${sys_ver} < 8.11.1 — async file uploads will be synchronous" info "Building libcurl ${CURL_VERSION} for full async support" fi info "Downloading libcurl ${CURL_VERSION}..." local curl_tag curl_tag="curl-$(echo "$CURL_VERSION" | tr '.' '_')" wget -q "https://github.com/curl/curl/releases/download/${curl_tag}/curl-${CURL_VERSION}.tar.gz" -O "${build_dir}/curl.tar.gz" tar -xf "${build_dir}/curl.tar.gz" -C "$build_dir" local curl_dir="${build_dir}/curl-${CURL_VERSION}" local sudo_cmd="" [[ $EUID -ne 0 ]] && sudo_cmd="sudo" run_with_spinner "Configuring libcurl" \ bash -c "cd '${curl_dir}' && \ PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \ ./configure --prefix=/usr/local \ --with-openssl --with-nghttp2=/usr/local \ --enable-shared --disable-static --without-libpsl --quiet" run_with_spinner "Compiling libcurl" \ bash -c "PKG_CONFIG_PATH=/usr/local/lib/pkgconfig make -C '${curl_dir}' -j${BUILD_JOBS} --quiet" run_with_spinner "Installing libcurl" \ $sudo_cmd make -C "${curl_dir}" install --quiet $sudo_cmd ldconfig success "libcurl ${CURL_VERSION} installed" } clone_sources() { step "Cloning source code" local src_dir="$1" info "Cloning php-src (${PHP_SRC_BRANCH})..." run_with_spinner "Cloning php-src" \ git clone --depth=1 --branch "$PHP_SRC_BRANCH" "https://github.com/${PHP_SRC_REPO}.git" "$src_dir" info "Cloning async extension (${ASYNC_BRANCH})..." run_with_spinner "Cloning ext/async" \ git clone --depth=1 --branch "$ASYNC_BRANCH" "https://github.com/${ASYNC_REPO}.git" "${src_dir}/ext/async" if [[ "$NO_XDEBUG" != "true" ]]; then info "Cloning Xdebug (${XDEBUG_BRANCH})..." run_with_spinner "Cloning Xdebug" \ git clone --depth=1 --branch "$XDEBUG_BRANCH" "https://github.com/${XDEBUG_REPO}.git" "${src_dir}/../xdebug" fi info "Cloning server extension (${SERVER_BRANCH})..." run_with_spinner "Cloning server" \ git clone --depth=1 --branch "$SERVER_BRANCH" "https://github.com/${SERVER_REPO}.git" "${src_dir}/../server" # Fix line endings if command -v dos2unix &>/dev/null; then dos2unix "${src_dir}/ext/async/config.m4" 2>/dev/null || true fi success "Sources cloned" } configure_php() { step "Configuring PHP build" local src_dir="$1" info "Running buildconf..." run_with_spinner "Running buildconf" \ bash -c "cd '${src_dir}' && ./buildconf --force" # Build configure flags local flags=( "--prefix=${INSTALL_DIR}" "--with-config-file-path=${INSTALL_DIR}/etc" "--with-config-file-scan-dir=${INSTALL_DIR}/etc/php.d" "--enable-zts" "--enable-async" "--enable-phpdbg" "--enable-bcmath" "--enable-calendar" "--enable-exif" "--enable-ftp" "--enable-intl" "--enable-mbstring" "--enable-pdo" "--enable-shmop" "--enable-soap" "--enable-sockets" "--enable-pcntl" "--enable-sysvmsg" "--enable-sysvsem" "--enable-sysvshm" "--enable-gd" "--enable-dba" "--enable-xmlreader" "--enable-embed" "--with-bz2" "--with-curl" "--with-gettext" "--with-gmp" "--with-mysqli=mysqlnd" "--with-openssl" "--with-pdo-mysql=mysqlnd" "--with-pdo-pgsql" "--with-pdo-sqlite" "--with-pgsql" "--with-sqlite3" "--with-xsl" "--with-zlib" "--with-jpeg" "--with-webp" "--with-freetype" "--with-zip" "--with-sodium" "--with-readline" "--with-tidy" "--with-ldap" "--with-ldap-sasl" "--with-enchant" "--with-ffi" "--with-snmp" "--with-gdbm" "--with-lmdb" "--with-libxml" "--without-pear" ) if [[ "$DEBUG_BUILD" == "true" ]]; then flags+=("--enable-debug") else flags+=("--disable-debug") fi info "Configure flags: ${#flags[@]} options" dimtext "${flags[*]}" run_with_spinner "Running configure" \ bash -c "cd '${src_dir}' && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig ./configure ${flags[*]}" success "Configuration complete" } build_php() { step "Compiling PHP" local src_dir="$1" info "Building with ${BUILD_JOBS} parallel jobs..." info "This may take 5-15 minutes depending on your hardware" run_with_spinner "Compiling PHP (${BUILD_JOBS} jobs)" \ make -C "$src_dir" -j"$BUILD_JOBS" success "PHP compiled successfully" } install_php() { step "Installing PHP" local src_dir="$1" mkdir -p "$INSTALL_DIR" run_with_spinner "Installing to ${INSTALL_DIR}" \ make -C "$src_dir" install success "PHP installed to ${INSTALL_DIR}" } build_xdebug() { step "Building Xdebug" local xdebug_dir="$1" local php_bin="${INSTALL_DIR}/bin" if [[ ! -d "$xdebug_dir" ]]; then warn "Xdebug source not found, skipping" return 0 fi run_with_spinner "Running phpize" \ bash -c "cd '${xdebug_dir}' && '${php_bin}/phpize'" run_with_spinner "Configuring Xdebug" \ bash -c "cd '${xdebug_dir}' && ./configure --with-php-config='${php_bin}/php-config'" run_with_spinner "Compiling Xdebug" \ make -C "${xdebug_dir}" -j"$BUILD_JOBS" run_with_spinner "Installing Xdebug" \ make -C "${xdebug_dir}" install success "Xdebug installed" } build_server() { step "Building true_async_server (HTTP/1+2+3)" local server_dir="$1" local php_bin="${INSTALL_DIR}/bin" if [[ ! -d "$server_dir" ]]; then warn "Server source not found, skipping" return 0 fi run_with_spinner "Running phpize" \ bash -c "cd '${server_dir}' && '${php_bin}/phpize'" local server_flags=( "--with-php-config='${php_bin}/php-config'" "--enable-http-server" "--enable-http2" "--with-openssl" "--with-nghttp2=/usr/local" ) if [[ "$BUILD_HTTP3" == "true" ]]; then server_flags+=( "--enable-http3" "--with-nghttp3=/usr/local" "--with-ngtcp2=/usr/local" ) info "HTTP/3 enabled" else server_flags+=("--disable-http3") info "HTTP/3 disabled" fi run_with_spinner "Configuring server extension" \ bash -c "cd '${server_dir}' && \ PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \ ./configure ${server_flags[*]}" run_with_spinner "Compiling server extension" \ make -C "${server_dir}" -j"$BUILD_JOBS" run_with_spinner "Installing server extension" \ make -C "${server_dir}" install success "true_async_server installed" } ensure_go() { local build_dir="$1" if command -v go &>/dev/null; then local go_ver go_ver=$(go version | sed 's/.*go\([0-9]*\.[0-9]*\).*/\1/') local major minor IFS='.' read -r major minor <<< "$go_ver" if (( major >= 1 && minor >= 26 )); then success "Go ${go_ver} is sufficient" return 0 fi warn "Go ${go_ver} is too old (need >= 1.26), installing Go ${GO_VERSION}" else info "Go not found, installing Go ${GO_VERSION}" fi local go_arch case "$(uname -m)" in x86_64) go_arch="amd64" ;; aarch64) go_arch="arm64" ;; *) error "Unsupported architecture for Go: $(uname -m)" ;; esac info "Downloading Go ${GO_VERSION} (${go_arch})..." wget -q "https://go.dev/dl/go${GO_VERSION}.linux-${go_arch}.tar.gz" -O "${build_dir}/go.tar.gz" tar -xf "${build_dir}/go.tar.gz" -C "$build_dir" export PATH="${build_dir}/go/bin:${PATH}" export GOPATH="${build_dir}/gopath" mkdir -p "$GOPATH" success "Go ${GO_VERSION} ready" } build_frankenphp() { step "Building FrankenPHP" if [[ "$BUILD_FRANKENPHP" != "true" ]]; then return 0 fi local build_dir="$1" local frankenphp_dir="${build_dir}/frankenphp" local php_config="${INSTALL_DIR}/bin/php-config" ensure_go "$build_dir" local sudo_cmd="" [[ $EUID -ne 0 ]] && sudo_cmd="sudo" run_with_spinner "Installing FrankenPHP build dependencies" \ $sudo_cmd apt-get install -y -qq --no-install-recommends libbrotli-dev info "Cloning FrankenPHP (${FRANKENPHP_BRANCH})..." run_with_spinner "Cloning FrankenPHP" \ git clone --depth=1 --branch "$FRANKENPHP_BRANCH" "https://github.com/${FRANKENPHP_REPO}.git" "$frankenphp_dir" run_with_spinner "go mod download (root)" \ bash -c "cd '${frankenphp_dir}' && go mod download" run_with_spinner "go mod download (caddy)" \ bash -c "cd '${frankenphp_dir}/caddy' && go mod download" run_with_spinner "Compiling FrankenPHP" \ bash -c "cd '${frankenphp_dir}/caddy/frankenphp' && \ PHP_PREFIX=\"\$('${php_config}' --prefix)\" && \ CGO_CFLAGS=\"\$('${php_config}' --includes)\" \ CGO_LDFLAGS=\"-L\${PHP_PREFIX}/lib -Wl,-rpath,\${PHP_PREFIX}/lib \$('${php_config}' --ldflags) \$('${php_config}' --libs)\" \ go build -tags 'trueasync,nowatcher' -o '${INSTALL_DIR}/bin/frankenphp' ." success "FrankenPHP installed to ${INSTALL_DIR}/bin/frankenphp" } setup_config() { step "Setting up PHP configuration" local conf_dir="${INSTALL_DIR}/etc/php.d" mkdir -p "$conf_dir" # opcache cat > "${conf_dir}/opcache.ini" << 'EOF' opcache.enable_cli=1 EOF success "Created opcache.ini" # server (always enabled) echo "extension=true_async_server" > "${conf_dir}/true_async_server.ini" success "Created true_async_server.ini" # xdebug if [[ "$NO_XDEBUG" != "true" ]]; then cat > "${conf_dir}/xdebug.ini" << 'EOF' zend_extension=xdebug.so xdebug.mode=off EOF success "Created xdebug.ini" fi } setup_path() { step "Setting up environment" local bin_dir="${INSTALL_DIR}/bin" # Install management script install_management_script "$bin_dir" if [[ "$SET_DEFAULT" == "true" ]]; then local shell_rc="" local current_shell current_shell=$(basename "${SHELL:-/bin/bash}") case "$current_shell" in bash) shell_rc="$HOME/.bashrc" ;; zsh) shell_rc="$HOME/.zshrc" ;; *) shell_rc="$HOME/.profile" ;; esac if [[ -n "$shell_rc" ]] && ! grep -q "php-trueasync" "$shell_rc" 2>/dev/null; then { echo "" echo "# TrueAsync PHP" echo "export PATH=\"${bin_dir}:\$PATH\"" } >> "$shell_rc" success "Added ${bin_dir} to PATH in ${shell_rc}" warn "Run 'source ${shell_rc}' or open a new terminal to use php" elif grep -q "php-trueasync" "$shell_rc" 2>/dev/null; then info "PATH already configured in ${shell_rc}" fi else info "PATH not modified (use --set-default to add to PATH)" info "You can run PHP directly: ${CYAN}${bin_dir}/php${NC}" fi } install_management_script() { local bin_dir="$1" local script="${bin_dir}/php-trueasync" cat > "$script" << 'MGMT_SCRIPT' #!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" VERSION_FILE=".trueasync-version" INSTALLER_URL="https://raw.githubusercontent.com/true-async/releases/master/installer/build-linux.sh" case "${1:-help}" in update|rebuild) echo "Rebuilding TrueAsync PHP..." INSTALL_DIR="$SCRIPT_DIR" exec bash <(curl -fsSL "$INSTALLER_URL") ;; version) if [[ -f "${SCRIPT_DIR}/${VERSION_FILE}" ]]; then cat "${SCRIPT_DIR}/${VERSION_FILE}" else "${SCRIPT_DIR}/bin/php" -v | head -1 fi ;; uninstall) echo "Uninstalling TrueAsync PHP from ${SCRIPT_DIR}..." for rc in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do if [[ -f "$rc" ]] && grep -q "php-trueasync" "$rc"; then sed -i.bak '/php-trueasync/d' "$rc" rm -f "${rc}.bak" echo "Cleaned PATH from $rc" fi done rm -rf "$SCRIPT_DIR" echo "TrueAsync PHP uninstalled." echo "Restart your terminal to apply PATH changes." ;; help|--help|-h) echo "TrueAsync PHP Manager" echo "" echo "Usage: php-trueasync " echo "" echo "Commands:" echo " rebuild Rebuild PHP from source (fetches latest code)" echo " version Show the installed version" echo " uninstall Remove TrueAsync PHP and clean up PATH" echo " help Show this help message" ;; *) echo "Unknown command: $1" echo "Run 'php-trueasync help' for usage." exit 1 ;; esac MGMT_SCRIPT chmod +x "$script" success "Management script installed: ${bin_dir}/php-trueasync" } verify_installation() { step "Verifying installation" local php_bin="${INSTALL_DIR}/bin/php" if [[ ! -x "$php_bin" ]]; then error "php binary not found at ${php_bin}" fi echo "" "$php_bin" -v echo "" info "Loaded modules:" local modules modules=$("$php_bin" -m | grep -E "^(async|xdebug|Core|date|pcre)" | tr '\n' ', ' | sed 's/,$//') dimtext "$modules ..." # Save version local version version=$("$php_bin" -r 'echo PHP_VERSION;') echo "$version" > "${INSTALL_DIR}/.trueasync-version" success "PHP ${version} with TrueAsync is ready!" } show_final_message() { local bin_dir="${INSTALL_DIR}/bin" echo "" echo -e " ${GREEN}${BOLD}Build complete!${NC}" echo -e " ─────────────────────────────────────" echo -e " PHP binary: ${CYAN}${bin_dir}/php${NC}" echo -e " Manager: ${CYAN}${bin_dir}/php-trueasync${NC}" if [[ "$BUILD_FRANKENPHP" == "true" ]]; then echo -e " FrankenPHP: ${CYAN}${bin_dir}/frankenphp${NC}" fi if [[ "$SET_DEFAULT" != "true" ]]; then echo "" echo -e " ${DIM}Tip: Run with --set-default to add to PATH${NC}" fi echo "" } # ═══════════════════════════════════════════════════════════════════════════════ # Main # ═══════════════════════════════════════════════════════════════════════════════ main() { parse_args "$@" show_banner check_system # Interactive wizard if [[ "$NO_INTERACTIVE" != "true" ]] && [[ -t 0 ]]; then run_wizard else # Non-interactive: skip if already installed if [[ -x "${INSTALL_DIR}/bin/php" ]]; then local existing_ver existing_ver=$("${INSTALL_DIR}/bin/php" -v 2>/dev/null | head -1 || echo "unknown") info "PHP already installed in ${INSTALL_DIR}:" echo -e " ${DIM}${existing_ver}${NC}" info "Skipping build. Remove the directory or use a different INSTALL_DIR to rebuild." exit 0 fi fi # Default HTTP3 to false in non-interactive mode if not set [[ -z "$BUILD_HTTP3" ]] && BUILD_HTTP3="false" # Determine total steps: base + openssl + nghttp2 + libcurl + server = +4 always # + nghttp3/ngtcp2 if HTTP3; + xdebug if enabled; + frankenphp if enabled STEP_TOTAL=14 [[ "$BUILD_HTTP3" == "true" ]] && STEP_TOTAL=$((STEP_TOTAL + 1)) [[ "$NO_XDEBUG" != "true" ]] && STEP_TOTAL=$((STEP_TOTAL + 1)) [[ "$BUILD_FRANKENPHP" == "true" ]] && STEP_TOTAL=$((STEP_TOTAL + 1)) # Read config read_config # Create temp build directory local build_dir build_dir=$(mktemp -d) local src_dir="${build_dir}/php-src" trap 'chmod -R u+w "$build_dir" 2>/dev/null; rm -rf "$build_dir"' EXIT # Execute build steps install_dependencies build_openssl "$build_dir" build_nghttp2 "$build_dir" [[ "$BUILD_HTTP3" == "true" ]] && build_nghttp3 "$build_dir" build_libuv "$build_dir" build_libcurl "$build_dir" clone_sources "$src_dir" configure_php "$src_dir" build_php "$src_dir" install_php "$src_dir" if [[ "$NO_XDEBUG" != "true" ]]; then build_xdebug "${build_dir}/xdebug" fi build_server "${build_dir}/server" build_frankenphp "$build_dir" setup_config setup_path verify_installation show_final_message } main "$@"