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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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