Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion linux/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
99 changes: 87 additions & 12 deletions linux/phpvm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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=()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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"
}

Expand Down Expand Up @@ -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
Expand Down
125 changes: 125 additions & 0 deletions tests/linux/build_preflight.bats
Original file line number Diff line number Diff line change
@@ -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" ]
}
9 changes: 9 additions & 0 deletions tests/linux/commands.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
Loading
Loading