diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d238bd..a951931 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,22 @@ jobs: - name: Bats (offline) run: bats tests/linux/ + # bats runs under bash, so nothing else covers zsh - even though phpvm.sh is + # documented as bash/zsh and installs a chpwd hook for zsh users. + zsh: + name: zsh compatibility + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install zsh + run: | + sudo apt-get update + sudo apt-get install -y zsh + + - name: zsh smoke + run: zsh tests/linux/zsh-smoke.zsh + # Experimental (B12 spike): does phpvm.sh behave on macOS/BSD userland? # Not a merge gate yet - failures inform the macOS backlog item. macos: diff --git a/README.md b/README.md index b84e477..63d9c1b 100644 --- a/README.md +++ b/README.md @@ -402,8 +402,14 @@ Invoke-Pester -Configuration (New-PesterConfiguration -Hashtable (Import-PowerSh ```bash bats tests/linux/ +zsh tests/linux/zsh-smoke.zsh # bats runs under bash; this covers zsh ``` +`phpvm.sh` targets both bash and zsh, and the two differ in ways that stay +invisible until a zsh user hits them — zsh arrays are 1-based, zsh does not +word-split unquoted parameters, and `${var:i}` is parsed as a history modifier +rather than a substring offset. Run the zsh smoke before touching `phpvm.sh`. + --- ## License diff --git a/linux/install.sh b/linux/install.sh index c4c20ae..23fc745 100644 --- a/linux/install.sh +++ b/linux/install.sh @@ -6,7 +6,7 @@ set -e -PHPVM_VERSION="1.13.0" +PHPVM_VERSION="1.13.1" PHPVM_DIR="${PHPVM_DIR:-$HOME/.phpvm}" PHPVM_REPO="https://raw.githubusercontent.com/devhardiyanto/phpvm/main" diff --git a/linux/phpvm.sh b/linux/phpvm.sh index fc6b0b5..b1c3631 100644 --- a/linux/phpvm.sh +++ b/linux/phpvm.sh @@ -10,7 +10,7 @@ # phpvm use 8.3.0 # ============================================================================== -PHPVM_VERSION="1.13.0" +PHPVM_VERSION="1.13.1" PHPVM_DIR="${PHPVM_DIR:-$HOME/.phpvm}" PHPVM_VERSIONS="$PHPVM_DIR/versions" PHPVM_CURRENT="$PHPVM_DIR/current" @@ -234,6 +234,51 @@ _phpvm_detect_os() { fi } +# Version of the OpenSSL that ./configure will actually resolve. pkg-config is +# what configure consults, so ask it first; the `openssl` CLI is a fallback and +# can disagree with the installed headers. Echoes "3.0.2"; non-zero if unknown. +_phpvm_openssl_version() { + local v="" + command -v pkg-config &>/dev/null && v=$(pkg-config --modversion openssl 2>/dev/null) + [[ -z "$v" ]] && command -v openssl &>/dev/null && v=$(openssl version 2>/dev/null | awk '{print $2}') + # Strip a letter suffix: OpenSSL 1.1.1w -> 1.1.1 + v="${v%%[a-zA-Z]*}" + [[ -n "$v" ]] || return 1 + echo "$v" +} + +# PHP's ext/openssl references RSA_SSLV23_PADDING, which OpenSSL removed in +# 3.0. php-src only stopped using it in 8.1, so anything older dies partway +# through `make` with a wall of C errors. That is knowable up front, so refuse +# before spending ten minutes compiling. +# https://github.com/php/php-src/issues/9503 +# Escape hatch: PHPVM_SKIP_OPENSSL_CHECK=1 for setups where pkg-config resolves +# an OpenSSL 1.1 that this probe can't see. +_phpvm_check_openssl_compat() { + local ver="$1" + [[ -n "${PHPVM_SKIP_OPENSSL_CHECK:-}" ]] && return 0 + + local major="${ver%%.*}" minor + minor=$(echo "$ver" | cut -d. -f2) + # PHP 8.1+ builds fine against OpenSSL 3. + (( major > 8 )) && return 0 + (( major == 8 && minor >= 1 )) && return 0 + + local ssl + ssl=$(_phpvm_openssl_version) || return 0 # can't tell -> don't block + [[ "${ssl%%.*}" -lt 3 ]] && return 0 + + _err "PHP $ver cannot be built against OpenSSL $ssl." + _dim "OpenSSL 3.0 removed RSA_SSLV23_PADDING; PHP's openssl extension only" + _dim "stopped using it in 8.1, so the build would fail in ext/openssl." + echo "" + _dim "Options:" + _dim " - Install a supported line: phpvm install 8.1 (or newer)" + _dim " - Keep OpenSSL 1.1 as what pkg-config resolves, then re-run with:" + _dim " PHPVM_SKIP_OPENSSL_CHECK=1 phpvm install $ver" + return 1 +} + # ── Check build dependencies ────────────────────────────────────────────────── _phpvm_check_deps() { local missing=() @@ -358,6 +403,22 @@ _phpvm_run_logged() { return $rc } +# A failed build leaves thousands of lines of compiler noise in $PHPVM_LOG, and +# the line that explains it is buried near the middle — "See log" alone makes +# the user page through it. Pull out the first hard error instead. +_phpvm_show_build_error() { + [[ -f "$PHPVM_LOG" ]] || return 0 + + local first + # configure's own failure is the more useful line when both are present. + first=$(grep -m1 -E '^configure: error:' "$PHPVM_LOG" 2>/dev/null) + [[ -z "$first" ]] && first=$(grep -m1 -E '(^|[[:space:]])(fatal )?error:' "$PHPVM_LOG" 2>/dev/null) + [[ -z "$first" ]] && return 0 + + _err "First error in the log:" + _dim " ${first}" +} + # Resolve a partial version to the highest published patch on php.net. # "8" -> latest 8.x (e.g. 8.5.7) # "8.3" -> latest 8.3.x (e.g. 8.3.31) @@ -435,6 +496,7 @@ phpvm_install() { # Check deps before attempting download _phpvm_check_deps || return 1 + _phpvm_check_openssl_compat "$ver" || return 1 # Download source local tarball="php-$ver.tar.gz" @@ -572,6 +634,7 @@ phpvm_install() { _phpvm_run_logged "Installing" make install \ || { _err "Install failed. See log: $PHPVM_LOG"; exit 1; } ) || { + _phpvm_show_build_error rm -rf "$target" return 1 } @@ -625,7 +688,9 @@ _phpvm_older_patch_hint() { [[ -z "$older" ]] && return 0 newest=$(echo "$older" | tail -1) - _dim "Older patch of ${ver%.*} still installed: $(echo "$older" | paste -sd ', ' -)" + # paste -d takes a *list* of delimiters and cycles through it, so ', ' would + # join as "a,b c". Join on a comma, then space it out. + _dim "Older patch of ${ver%.*} still installed: $(echo "$older" | paste -sd, - | sed 's/,/, /g')" _dim "Remove it with: phpvm uninstall $newest" } @@ -1524,33 +1589,43 @@ _phpvm_levenshtein() { (( la == 0 )) && { echo "$lb"; return; } (( lb == 0 )) && { echo "$la"; return; } local i j cost prev cur del ins sub min + # Indices are shifted by one so the array starts at 1: zsh arrays are + # 1-based and reject row[0] outright ("assignment to invalid subscript + # range"), while bash simply leaves index 0 unused. local -a row - for (( j = 0; j <= lb; j++ )); do row[j]=$j; done + for (( j = 0; j <= lb; j++ )); do row[j+1]=$j; done for (( i = 1; i <= la; i++ )); do - prev=${row[0]} - row[0]=$i + prev=${row[1]} + row[1]=$i for (( j = 1; j <= lb; j++ )); do - cur=${row[j]} - if [[ "${a:i-1:1}" == "${b:j-1:1}" ]]; then cost=0; else cost=1; fi - del=$(( row[j] + 1 )); ins=$(( row[j-1] + 1 )); sub=$(( prev + cost )) + cur=${row[j+1]} + # ${a:i-1:1} makes zsh read ":i" as a history modifier + # ("unrecognized modifier"). Spell the offset arithmetic out. + if [[ "${a:$((i-1)):1}" == "${b:$((j-1)):1}" ]]; then cost=0; else cost=1; fi + del=$(( row[j+1] + 1 )); ins=$(( row[j] + 1 )); sub=$(( prev + cost )) min=$del (( ins < min )) && min=$ins (( sub < min )) && min=$sub - row[j]=$min + row[j+1]=$min prev=$cur done done - echo "${row[lb]}" + echo "${row[lb+1]}" } # Canonical command list (includes aliases) for suggestions. -_PHPVM_COMMANDS="install use list ls current uninstall remove which doctor ini deps ext composer wp-cli fix-ini auto hook upgrade update version help" +# An array, not a space-separated string: zsh does not word-split unquoted +# parameters, so `for c in $_PHPVM_COMMANDS` handed the whole list over as a +# single candidate and every typo was answered with the entire list. +_PHPVM_COMMANDS=(install use list ls current uninstall remove which doctor ini + deps ext composer wp-cli fix-ini auto hook upgrade update + version help) # Unknown command: suggest the nearest match instead of dumping the full help. _phpvm_unknown() { local cmd="$1" local best="" bestd=99 c d - for c in $_PHPVM_COMMANDS; do + for c in "${_PHPVM_COMMANDS[@]}"; do d=$(_phpvm_levenshtein "$cmd" "$c") (( d < bestd )) && { bestd=$d; best=$c; } done diff --git a/tests/linux/build_preflight.bats b/tests/linux/build_preflight.bats new file mode 100644 index 0000000..1be9df8 --- /dev/null +++ b/tests/linux/build_preflight.bats @@ -0,0 +1,125 @@ +#!/usr/bin/env bats +# Build preflight: the OpenSSL 3 vs PHP < 8.1 guard and the build-log error +# surfacing. Older-patch hint formatting lives in commands.bats. +# Run from repo root: bats tests/linux/ + +setup() { + export PHPVM_DIR="$BATS_TEST_TMPDIR/phpvm" + export PHPVM_VERSIONS="$PHPVM_DIR/versions" + export PHPVM_CURRENT="$PHPVM_DIR/current" + export PHPVM_LOG="$BATS_TEST_TMPDIR/build.log" + export PHPVM_NO_INIT=1 + export PHPVM_NO_UPDATE_CHECK=1 + unset PHPVM_SKIP_OPENSSL_CHECK + mkdir -p "$PHPVM_VERSIONS" + + # shellcheck disable=SC1091 + . "$BATS_TEST_DIRNAME/../../linux/phpvm.sh" +} + +# Pin the detected OpenSSL version so the result doesn't depend on the host. +_stub_openssl() { + eval "_phpvm_openssl_version() { echo '$1'; }" +} + +# ── OpenSSL compatibility guard ─────────────────────────────────────────────── + +@test "openssl 3 blocks PHP 7.3" { + _stub_openssl 3.0.2 + run _phpvm_check_openssl_compat 7.3.33 + [ "$status" -eq 1 ] + [[ "$output" == *"cannot be built against OpenSSL 3.0.2"* ]] +} + +@test "openssl 3 blocks PHP 7.4 and 8.0" { + _stub_openssl 3.2.1 + run _phpvm_check_openssl_compat 7.4.33 + [ "$status" -eq 1 ] + run _phpvm_check_openssl_compat 8.0.30 + [ "$status" -eq 1 ] +} + +@test "openssl 3 allows PHP 8.1 and newer" { + _stub_openssl 3.0.2 + run _phpvm_check_openssl_compat 8.1.0 + [ "$status" -eq 0 ] + run _phpvm_check_openssl_compat 8.3.10 + [ "$status" -eq 0 ] + run _phpvm_check_openssl_compat 9.0.0 + [ "$status" -eq 0 ] +} + +@test "openssl 1.1 allows old PHP" { + _stub_openssl 1.1.1 + run _phpvm_check_openssl_compat 7.3.33 + [ "$status" -eq 0 ] +} + +@test "undetectable openssl does not block the build" { + eval "_phpvm_openssl_version() { return 1; }" + run _phpvm_check_openssl_compat 7.3.33 + [ "$status" -eq 0 ] +} + +@test "PHPVM_SKIP_OPENSSL_CHECK overrides the guard" { + _stub_openssl 3.0.2 + PHPVM_SKIP_OPENSSL_CHECK=1 run _phpvm_check_openssl_compat 7.3.33 + [ "$status" -eq 0 ] +} + +@test "the block message names the remedy" { + _stub_openssl 3.0.2 + run _phpvm_check_openssl_compat 7.3.33 + [[ "$output" == *"phpvm install 8.1"* ]] + [[ "$output" == *"PHPVM_SKIP_OPENSSL_CHECK=1"* ]] +} + +@test "openssl version probe strips a letter suffix" { + # Only meaningful when the host actually has one of the probes. + if ! command -v pkg-config &>/dev/null && ! command -v openssl &>/dev/null; then + skip "no pkg-config or openssl on this host" + fi + run _phpvm_openssl_version + if [ "$status" -eq 0 ]; then + [[ "$output" =~ ^[0-9]+(\.[0-9]+)*$ ]] + fi +} + +# ── Build log error surfacing ──────────────────────────────────────────────── + +@test "surfaces the first compiler error from the log" { + cat > "$PHPVM_LOG" <<'LOG' +checking for gcc... yes +gcc -c foo.c -o foo.o +ext/openssl/openssl.c:1491:58: error: 'RSA_SSLV23_PADDING' undeclared +make: *** [Makefile:736: ext/openssl/openssl.lo] Error 1 +LOG + run _phpvm_show_build_error + [[ "$output" == *"First error in the log"* ]] + [[ "$output" == *"RSA_SSLV23_PADDING"* ]] +} + +@test "prefers a configure error over later compiler noise" { + cat > "$PHPVM_LOG" <<'LOG' +checking for libxml... no +configure: error: Package requirements (libxml-2.0) were not met +something.c:1:1: error: later noise +LOG + run _phpvm_show_build_error + [[ "$output" == *"libxml-2.0"* ]] + [[ "$output" != *"later noise"* ]] +} + +@test "stays quiet when the log holds no error" { + printf 'all good\nbuild complete\n' > "$PHPVM_LOG" + run _phpvm_show_build_error + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "stays quiet when there is no log at all" { + rm -f "$PHPVM_LOG" + run _phpvm_show_build_error + [ "$status" -eq 0 ] + [ -z "$output" ] +} diff --git a/tests/linux/commands.bats b/tests/linux/commands.bats index 48980ef..1f43f70 100644 --- a/tests/linux/commands.bats +++ b/tests/linux/commands.bats @@ -428,6 +428,15 @@ EOF [[ "$output" == *"phpvm uninstall 8.5.6"* ]] } +@test "older_patch_hint: separates several patches with a comma and a space" { + # paste -d cycles its delimiter list, so ', ' used to join as "a,b c". + mkdir -p "$PHPVM_VERSIONS"/{8.5.1,8.5.2,8.5.6,8.5.8} + run _phpvm_older_patch_hint 8.5.8 + [ "$status" -eq 0 ] + [[ "$output" == *"8.5.1, 8.5.2, 8.5.6"* ]] + [[ "$output" != *"8.5.2 8.5.6"* ]] +} + # ---------- phpvm_doctor ---------- @test "doctor: warns when no active version" { diff --git a/tests/linux/zsh-smoke.zsh b/tests/linux/zsh-smoke.zsh new file mode 100644 index 0000000..ec50031 --- /dev/null +++ b/tests/linux/zsh-smoke.zsh @@ -0,0 +1,81 @@ +#!/usr/bin/env zsh +# zsh compatibility smoke for linux/phpvm.sh. +# +# The bats suite runs under bash, so nothing else in this repo exercises zsh - +# yet phpvm.sh is documented as bash/zsh and registers a chpwd hook for zsh +# users. Anything that quietly depends on bash semantics (0-indexed arrays, +# word-splitting on unquoted parameters) only shows up here. +# +# Run from repo root: zsh tests/linux/zsh-smoke.zsh + +emulate -R zsh # no bash/ksh compatibility crutches - test zsh as users get it + +typeset -i fail=0 + +check() { # check + if [[ "$3" == *"$2"* ]]; then + print "ok - $1" + else + print "FAIL - $1" + print " expected to contain: $2" + print " actual: $3" + fail=1 + fi +} + +tmp=$(mktemp -d) +export PHPVM_DIR="$tmp/phpvm" +export PHPVM_NO_UPDATE_CHECK=1 +export PHPVM_NO_INIT=1 +if ! source ./linux/phpvm.sh; then + print "FAIL - phpvm.sh could not be sourced under zsh" + exit 1 +fi +unset PHPVM_NO_INIT + +print -r -- "-- levenshtein (array indexing) --" +out=$(_phpvm_levenshtein kitten sitting 2>&1) +check "levenshtein kitten/sitting = 3" "3" "$out" +out=$(_phpvm_levenshtein '' abc 2>&1) +check "levenshtein ''/abc = 3" "3" "$out" +out=$(_phpvm_levenshtein same same 2>&1) +check "levenshtein same/same = 0" "0" "$out" + +print -r -- "-- did-you-mean (command-list splitting) --" +out=$(phpvm instal 2>&1) +check "suggests install for 'instal'" "Did you mean 'install'" "$out" +out=$(phpvm doctro 2>&1) +check "suggests doctor for 'doctro'" "Did you mean 'doctor'" "$out" +out=$(phpvm zzzzzzzzzz 2>&1) +check "no suggestion for nonsense" "is not a phpvm command" "$out" + +print -r -- "-- basic read-only commands --" +out=$(phpvm version 2>&1) +check "version prints the version" "phpvm" "$out" +out=$(phpvm list 2>&1) +check "list runs without error" "No PHP versions installed" "$out" +out=$(phpvm help 2>&1) +check "help runs" "PHP Version Manager" "$out" +out=$(phpvm current 2>&1) +check "current runs" "No PHP version" "$out" + +print -r -- "-- .phpvmrc parsing --" +mkdir -p "$tmp/proj" +print "8.3" > "$tmp/proj/.phpvmrc" +out=$(cd "$tmp/proj" && _phpvm_read_rc "$tmp/proj/.phpvmrc" 2>&1) +check "reads a .phpvmrc version" "8.3" "$out" + +print -r -- "-- older-patch hint --" +mkdir -p "$PHPVM_DIR/versions/8.5.1" "$PHPVM_DIR/versions/8.5.2" "$PHPVM_DIR/versions/8.5.8" +out=$(_phpvm_older_patch_hint 8.5.8 2>&1) +check "joins older patches with ', '" "8.5.1, 8.5.2" "$out" + +rm -rf "$tmp" + +print "" +if (( fail )); then + print "zsh smoke: FAILED" + exit 1 +fi +print "zsh smoke: all checks passed" +exit 0 diff --git a/version.txt b/version.txt index f88cf52..da38e07 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.13.0 \ No newline at end of file +1.13.1 \ No newline at end of file diff --git a/windows/install.ps1 b/windows/install.ps1 index ea7166e..53c843f 100644 --- a/windows/install.ps1 +++ b/windows/install.ps1 @@ -8,7 +8,7 @@ Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" -$PHPVM_VERSION = "1.13.0" +$PHPVM_VERSION = "1.13.1" $PHPVM_DIR = if ($env:PHPVM_DIR) { $env:PHPVM_DIR } else { "$env:USERPROFILE\.phpvm" } $PHPVM_BIN = "$PHPVM_DIR\bin" diff --git a/windows/phpvm.ps1 b/windows/phpvm.ps1 index c389391..8d2c79b 100644 --- a/windows/phpvm.ps1 +++ b/windows/phpvm.ps1 @@ -23,7 +23,7 @@ Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" # -- Constants ----------------------------------------------------------------- -$PHPVM_VERSION = "1.13.0" +$PHPVM_VERSION = "1.13.1" $PHPVM_DIR = if ($env:PHPVM_DIR) { $env:PHPVM_DIR } else { "$env:USERPROFILE\.phpvm" } $VERSIONS_DIR = "$PHPVM_DIR\versions" $CURRENT_LINK = "$PHPVM_DIR\current" diff --git a/windows/src/00-header.ps1 b/windows/src/00-header.ps1 index 52f0bfd..f288432 100644 --- a/windows/src/00-header.ps1 +++ b/windows/src/00-header.ps1 @@ -15,7 +15,7 @@ Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" # -- Constants ----------------------------------------------------------------- -$PHPVM_VERSION = "1.13.0" +$PHPVM_VERSION = "1.13.1" $PHPVM_DIR = if ($env:PHPVM_DIR) { $env:PHPVM_DIR } else { "$env:USERPROFILE\.phpvm" } $VERSIONS_DIR = "$PHPVM_DIR\versions" $CURRENT_LINK = "$PHPVM_DIR\current"