Skip to content
Open
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
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,31 @@ jobs:
- name: Bats (offline)
run: bats tests/linux/

# B12 slice: does a real build-from-source succeed on macOS ARM?
# Informational — a configure/build failure prints the log tail and still
# passes, so this surfaces the remaining ARM gaps without a permanent red.
- name: Install build dependencies (brew)
run: |
brew install autoconf bison re2c pkg-config openssl@3 libxml2 \
sqlite curl oniguruma libzip zlib readline libpng jpeg webp \
freetype gmp libsodium gettext

- name: Smoke install + use (build from source, informational)
shell: bash
run: |
# Keg-only bison must shadow the ancient system bison for the build.
export PATH="$(brew --prefix bison)/bin:$PATH"
export PHPVM_NO_INIT=1
source linux/phpvm.sh
unset PHPVM_NO_INIT
if phpvm install 8.3 --no-use && phpvm use 8.3 && php --version; then
echo "macOS build-from-source smoke: OK"
else
echo "::warning::macOS build-from-source smoke failed (known B12 ARM gap)"
echo "----- build.log (tail) -----"
tail -60 "$HOME/.phpvm/build.log" 2>/dev/null || echo "(no build.log)"
fi

version-consistency:
name: Version consistency
runs-on: ubuntu-latest
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,20 @@ phpvm cacert update # refresh the bundle from curl.se
phpvm install 8.3 --no-cacert # opt out if you manage your own bundle
```

### Diagnostics

`phpvm doctor` runs a read-only health check and points at the command that
fixes each finding — it never changes anything.

```bash
phpvm doctor
```

Checks: active version, whether `php` on PATH resolves to phpvm (catches
XAMPP/Laragon/WAMP shadowing), `extension_dir` vs the active build, and —
per OS — the CA bundle + VC++ runtime (Windows) or openssl + build toolchain
(Linux). Start here when something behaves unexpectedly.

### Auto-Switch with `.phpvmrc`

Drop a `.phpvmrc` file in your project root containing the PHP version you want:
Expand Down Expand Up @@ -260,7 +274,8 @@ vs16/vs17 builds need the 2015–2022 x64 redist). Download:

### `php -v` shows the wrong version (Windows)

Another PHP on PATH (XAMPP, Laragon, Herd) is shadowing phpvm. Check with
Another PHP on PATH (XAMPP, Laragon, Herd) is shadowing phpvm. Run
`phpvm doctor` to detect it, or check with
`phpvm which` — if the path isn't `~\.phpvm\current\php.exe`, move
`%USERPROFILE%\.phpvm\current` above the other entry in your User PATH, or
remove the other entry.
Expand Down
2 changes: 1 addition & 1 deletion linux/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

set -e

PHPVM_VERSION="1.11.0"
PHPVM_VERSION="1.12.0"
PHPVM_DIR="${PHPVM_DIR:-$HOME/.phpvm}"
PHPVM_REPO="https://raw.githubusercontent.com/devhardiyanto/phpvm/main"

Expand Down
114 changes: 110 additions & 4 deletions linux/phpvm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# phpvm use 8.3.0
# ==============================================================================

PHPVM_VERSION="1.11.0"
PHPVM_VERSION="1.12.0"
PHPVM_DIR="${PHPVM_DIR:-$HOME/.phpvm}"
PHPVM_VERSIONS="$PHPVM_DIR/versions"
PHPVM_CURRENT="$PHPVM_DIR/current"
Expand Down Expand Up @@ -472,13 +472,25 @@ phpvm_install() {
# Configure
mkdir -p "$target"

# www-data doesn't exist on macOS; its stock web user is _www. Only affects
# the default user baked into php-fpm.conf, not the CLI build.
local fpm_user="www-data"
[[ "$(uname -s)" == "Darwin" ]] && fpm_user="_www"

# gettext is keg-only on Homebrew: libintl.h isn't on the default include
# path, so --with-gettext needs the explicit prefix. Linux has it in glibc.
local gettext_opt="--with-gettext"
if [[ "$(uname -s)" == "Darwin" ]] && command -v brew &>/dev/null; then
gettext_opt="--with-gettext=$(brew --prefix gettext)"
fi

local configure_opts=(
"--prefix=$target"
"--with-config-file-path=$target/etc"
"--with-config-file-scan-dir=$target/etc/conf.d"
"--enable-fpm"
"--with-fpm-user=www-data"
"--with-fpm-group=www-data"
"--with-fpm-user=$fpm_user"
"--with-fpm-group=$fpm_user"
"--enable-mbstring"
"--enable-intl"
"--enable-opcache"
Expand All @@ -500,7 +512,7 @@ phpvm_install() {
"--with-jpeg"
"--with-webp"
"--with-freetype"
"--with-gettext"
"$gettext_opt"
"--with-gmp"
"--with-pgsql"
"--with-pdo-pgsql"
Expand All @@ -517,6 +529,18 @@ phpvm_install() {
(
cd "$src_dir" || { _err "Could not enter source directory: $src_dir"; exit 1; }

# macOS: Homebrew ships openssl@3/curl/zlib/... as keg-only, so pkg-config
# can't find them on the default path. Prepend their pkgconfig dirs so
# --with-openssl et al. resolve. No-op on Linux.
if [[ "$(uname -s)" == "Darwin" ]] && command -v brew &>/dev/null; then
brew_prefix="$(brew --prefix)"
for keg in openssl@3 curl zlib libzip icu4c oniguruma readline libxml2 sqlite; do
[[ -d "$brew_prefix/opt/$keg/lib/pkgconfig" ]] && \
PKG_CONFIG_PATH="$brew_prefix/opt/$keg/lib/pkgconfig:${PKG_CONFIG_PATH:-}"
done
export PKG_CONFIG_PATH
fi

# >> + 2>&1 (not &>>): macOS still ships bash 3.2, which can't parse &>>.
./buildconf --force >>"$PHPVM_LOG" 2>&1 || true # needed only for git checkouts

Expand Down Expand Up @@ -691,6 +715,86 @@ phpvm_which() {
fi
}

# ==============================================================================
# phpvm doctor — read-only health check (never mutates state)
# ==============================================================================
phpvm_doctor() {
echo ""
echo -e " \033[36mphpvm doctor — environment health check\033[0m"
echo -e " \033[36m─────────────────────────────────────────────────────────\033[0m"

local ok=0 warn=0
_dok() { printf ' \033[32m[ok] %s\033[0m\n' "$*"; ok=$((ok+1)); }
_dwarn(){ printf ' \033[33m[warn] %s\033[0m\n' "$*"; warn=$((warn+1)); }

# 1. Active version + symlink health.
local cur
cur=$(_phpvm_current_version)
if [[ -n "$cur" ]]; then
_dok "Active PHP version: $cur"
else
_dwarn "No active PHP version. Run: phpvm use <version>"
fi

# 2. PATH: whichever php resolves first is what runs.
if command -v php &>/dev/null; then
local php_path
php_path=$(command -v php)
case "$php_path" in
"$PHPVM_CURRENT"/*|"$PHPVM_BIN"/*|"$PHPVM_VERSIONS"/*)
_dok "'php' resolves to phpvm: $php_path" ;;
*)
_dwarn "'php' resolves to a non-phpvm install: $php_path"
_dim "Ensure $PHPVM_DIR is sourced in your shell rc, then open a new shell." ;;
esac
else
_dwarn "No 'php' on PATH. Run: phpvm use <version> (and source phpvm.sh in your rc)."
fi

# 3. extension_dir readable for the active build.
if [[ -n "$cur" ]]; then
local ext_dir
ext_dir=$(php -r "echo ini_get('extension_dir');" 2>/dev/null)
if [[ -n "$ext_dir" && -d "$ext_dir" ]]; then
_dok "extension_dir present: $ext_dir"
else
_dwarn "extension_dir not found or unreadable for active version."
_dim "Fix with: phpvm fix-ini"
fi
fi

# 4. OpenSSL in PHP (HTTPS for composer / ext downloads).
if [[ -n "$cur" ]] && php -m 2>/dev/null | grep -qi '^openssl$'; then
_dok "openssl extension loaded (HTTPS OK)."
elif [[ -n "$cur" ]]; then
_dwarn "openssl not loaded — composer/HTTPS may fail."
_dim "Enable with: phpvm ext enable openssl"
fi

# 5. Build toolchain (needed for future installs — build from source).
local missing=()
local t
for t in gcc make autoconf bison re2c pkg-config; do
command -v "$t" &>/dev/null || missing+=("$t")
done
if [[ ${#missing[@]} -eq 0 ]]; then
_dok "Build toolchain present (install/build ready)."
else
_dwarn "Missing build tools: ${missing[*]}"
_dim "See: phpvm deps"
fi

echo ""
if [[ $warn -eq 0 ]]; then
_ok "All checks passed ($ok ok)."
else
_warn "$warn warning(s), $ok ok. See fixes above."
fi
echo ""

unset -f _dok _dwarn 2>/dev/null
}

# ==============================================================================
# phpvm ini
# ==============================================================================
Expand Down Expand Up @@ -1243,6 +1347,7 @@ phpvm_help() {
phpvm ini Open active php.ini in \$EDITOR
phpvm fix-ini Sync extension_dir in active php.ini
phpvm deps Print dependency install command
phpvm doctor Diagnose PATH, extension_dir, openssl, toolchain

COMPOSER / WP-CLI
phpvm composer Install Composer for active PHP version
Expand Down Expand Up @@ -1453,6 +1558,7 @@ phpvm() {
current) phpvm_current ;;
uninstall|remove) phpvm_uninstall "$@" ;;
which) phpvm_which ;;
doctor) phpvm_doctor ;;
ini) phpvm_ini ;;
deps) phpvm_deps ;;
ext) phpvm_ext "$@" ;;
Expand Down
23 changes: 23 additions & 0 deletions tests/linux/commands.bats
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,26 @@ EOF
[[ "$output" == *"Older patch of 8.5 still installed: 8.5.6"* ]]
[[ "$output" == *"phpvm uninstall 8.5.6"* ]]
}

# ---------- phpvm_doctor ----------

@test "doctor: warns when no active version" {
eval "_phpvm_current_version() { echo ''; }"
run phpvm_doctor
[ "$status" -eq 0 ]
[[ "$output" == *"environment health check"* ]]
[[ "$output" == *"No active PHP version"* ]]
[[ "$output" == *"warning(s)"* ]]
}

@test "doctor: reports active version, ext_dir and openssl" {
_fake_php_install 8.3.0
export FAKE_PHP_EXT_DIR="$PHPVM_VERSIONS/8.3.0/lib/php/extensions"
export FAKE_PHP_EXTS="Core openssl"
export PATH="$PHPVM_VERSIONS/8.3.0/bin:$PATH"
run phpvm_doctor
[ "$status" -eq 0 ]
[[ "$output" == *"Active PHP version: 8.3.0"* ]]
[[ "$output" == *"extension_dir present"* ]]
[[ "$output" == *"openssl extension loaded"* ]]
}
31 changes: 31 additions & 0 deletions tests/windows/Doctor.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Describe 'Invoke-Doctor' {
BeforeAll {
$env:PHPVM_DIR = Join-Path $TestDrive '.phpvm'
New-Item -ItemType Directory -Path $env:PHPVM_DIR -Force | Out-Null
. $PSScriptRoot/Common.ps1
}

AfterAll {
Remove-Item Env:PHPVM_DIR -ErrorAction SilentlyContinue
}

It 'Warns and summarizes when no active PHP version is set' {
Mock -CommandName Get-CurrentVersion -MockWith { $null }

$out = Invoke-Doctor 6>&1 | Out-String

$out | Should -Match 'environment health check'
$out | Should -Match 'No active PHP version'
$out | Should -Match 'warning\(s\)'
}

It 'Reports the active version and never throws' {
Mock -CommandName Get-CurrentVersion -MockWith { '8.3.0' }
# IniPath null -> exercises the "no php.ini" branch without hitting php.exe.
Mock -CommandName Get-PHPBuildInfo -MockWith { @{ Exe = 'php'; IniPath = $null; ExtDir = 'C:\x\ext' } }

$out = Invoke-Doctor 6>&1 | Out-String

$out | Should -Match 'Active PHP version: 8\.3\.0'
}
}
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.11.0
1.12.0
2 changes: 1 addition & 1 deletion windows/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

$PHPVM_VERSION = "1.11.0"
$PHPVM_VERSION = "1.12.0"
$PHPVM_DIR = if ($env:PHPVM_DIR) { $env:PHPVM_DIR } else { "$env:USERPROFILE\.phpvm" }
$PHPVM_BIN = "$PHPVM_DIR\bin"

Expand Down
Loading
Loading