Skip to content
Merged
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ jobs:
- name: Bats (offline)
run: bats tests/linux/

# Experimental (B12 spike): does phpvm.sh behave on macOS/BSD userland?
# Not a merge gate yet - failures inform the macOS backlog item.
macos:
name: macOS bats (experimental)
runs-on: macos-latest
continue-on-error: true
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Install bats
run: brew install bats-core

- name: Bats (offline)
run: bats tests/linux/

version-consistency:
name: Version consistency
runs-on: ubuntu-latest
Expand Down
97 changes: 97 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,26 @@ with elapsed time over the `configure` / `make` / `make install` steps on Linux.
Both are drawn on stderr and are suppressed automatically when output is not a
terminal, so piping and CI logs stay clean.

### CA bundle (Windows)

Windows PHP builds ship without a CA bundle, so out of the box every HTTPS
request from PHP fails with `cURL error 60`. On install, phpvm downloads the
[Mozilla CA bundle](https://curl.se/docs/caextract.html) once to
`~/.phpvm/cacert.pem` and points the new version's `curl.cainfo` and
`openssl.cafile` at it. The bundle is shared, so switching PHP versions never
loses the fix.

```powershell
phpvm cacert # show bundle status (path + age)
phpvm cacert update # refresh the bundle from curl.se
phpvm install 8.3 --no-cacert # opt out if you manage your own bundle
phpvm fix-ini # re-apply to an existing install
```

If the download fails (offline install), phpvm warns and continues — run
`phpvm cacert update` later. Linux is unaffected: source builds use the
distro's system certificate store.

### Auto-Switch with `.phpvmrc` (Windows)

Drop a `.phpvmrc` file in your project root containing the PHP version you want:
Expand Down Expand Up @@ -272,6 +292,64 @@ phpvm/

---

## Troubleshooting

### `cURL error 60: SSL certificate problem` (Windows)

Windows PHP builds ship no CA bundle, so HTTPS from PHP (Guzzle, Laravel HTTP
client, API calls) fails TLS verification even though `curl.exe` works fine
(it uses the Windows cert store). phpvm ≥ 1.10.0 configures a shared bundle
automatically on install. For versions installed earlier, run:

```powershell
phpvm fix-ini # wires curl.cainfo / openssl.cafile to ~/.phpvm/cacert.pem
```

Do **not** work around this with `verify => false` in application code — that
disables TLS verification and tends to leak into production.

### `VCRUNTIME140.dll was not found` / php.exe won't start (Windows)

PHP needs the matching Visual C++ Redistributable (see the matrix below —
vs16/vs17 builds need the 2015–2022 x64 redist). Download:
<https://aka.ms/vs/17/release/vc_redist.x64.exe>

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

Another PHP on PATH (XAMPP, Laragon, Herd) is shadowing phpvm. 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.

### Extensions won't load / `Unable to load dynamic library`

`extension_dir` in php.ini may point somewhere else (typically after copying
an ini from another install). Run `phpvm fix-ini` to re-pin it to the active
version's `ext\` folder, then verify with `phpvm ext list`.

### `running scripts is disabled on this system` (Windows install)

PowerShell's ExecutionPolicy blocks the installer. Allow local scripts for
your user, then re-run:

```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```

### `Configure failed` / `Build failed` (Linux)

A dev library is missing. Run `phpvm deps` for the exact install command for
your distro, and check the tail of `~/.phpvm/build.log` for the first error.

### `.phpvmrc` doesn't auto-switch

The shell hook isn't active. Windows: `phpvm hook install`, then open a new
terminal. Linux: make sure your `~/.bashrc` / `~/.zshrc` sources
`~/.phpvm/phpvm.sh`. Note that auto-switch never installs missing versions —
it only switches between installed ones.

---

## Compatibility

| Platform | Shell | Status |
Expand All @@ -282,6 +360,25 @@ phpvm/
| Debian 11+ | bash / zsh | ✅ |
| Fedora / RHEL | bash / zsh | ✅ |
| Arch Linux | bash / zsh | ✅ |
| macOS 13+ | bash / zsh | 🧪 experimental — CI-tested only, `brew` deps; FPM defaults assume Linux |

### PHP × compiler toolchain (Windows builds)

Which Visual Studio toolchain each PHP line is built with on windows.php.net,
and therefore which VC++ Redistributable it needs at runtime. phpvm resolves
this automatically; the table is here for debugging download or DLL issues.

| PHP | Toolchain | VC++ Redistributable |
|---|---|---|
| 5.x | vc11 | Visual C++ 2012 |
| 7.0 – 7.1 | vc14 | Visual C++ 2015 |
| 7.2 – 7.4 | vc15 | Visual C++ 2015–2019 |
| 8.0 – 8.3 | vs16 | Visual C++ 2015–2022 |
| 8.4+ | vs17 | Visual C++ 2015–2022 |

Both TS (Thread Safe) and NTS builds are supported; phpvm prefers the TS zip
and falls back to NTS. `phpvm ext install` detects the active build's
TS/NTS + toolchain and downloads matching extension DLLs.

---

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.9.1"
PHPVM_VERSION="1.10.0"
PHPVM_DIR="${PHPVM_DIR:-$HOME/.phpvm}"
PHPVM_REPO="https://raw.githubusercontent.com/devhardiyanto/phpvm/main"

Expand Down
39 changes: 24 additions & 15 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.9.1"
PHPVM_VERSION="1.10.0"
PHPVM_DIR="${PHPVM_DIR:-$HOME/.phpvm}"
PHPVM_VERSIONS="$PHPVM_DIR/versions"
PHPVM_CURRENT="$PHPVM_DIR/current"
Expand Down Expand Up @@ -40,8 +40,9 @@ _phpvm_check_update() {
# Only check once per day
if [[ -f "$PHPVM_LAST_CHECK" ]]; then
local last_ts now elapsed
last_ts=$(date -r "$PHPVM_LAST_CHECK" +%s 2>/dev/null || \
stat -c %Y "$PHPVM_LAST_CHECK" 2>/dev/null || echo 0)
# GNU stat (-c) on Linux, BSD stat (-f) on macOS.
last_ts=$(stat -c %Y "$PHPVM_LAST_CHECK" 2>/dev/null || \
stat -f %m "$PHPVM_LAST_CHECK" 2>/dev/null || echo 0)
now=$(date +%s)
elapsed=$(( now - last_ts ))
[[ $elapsed -lt $PHPVM_CHECK_INTERVAL ]] && return
Expand Down Expand Up @@ -87,7 +88,7 @@ _phpvm_init() {
_phpvm_use_path() {
local bin="$PHPVM_CURRENT/bin"
# Remove any existing phpvm path entries
PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$PHPVM_DIR" | paste -sd ':')
PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$PHPVM_DIR" | paste -sd ':' -)
export PATH="$PHPVM_BIN:$bin:$PATH"
}

Expand Down Expand Up @@ -166,7 +167,7 @@ _phpvm_auto() {
if ! rc=$(_phpvm_find_rc); then
if [[ -n "${PHPVM_AUTO_ACTIVE:-}" ]]; then
local old="$PHPVM_VERSIONS/$PHPVM_AUTO_ACTIVE/bin"
PATH=$(echo "$PATH" | tr ':' '\n' | grep -vxF "$old" | paste -sd ':')
PATH=$(echo "$PATH" | tr ':' '\n' | grep -vxF "$old" | paste -sd ':' -)
export PATH
unset PHPVM_AUTO_ACTIVE
[[ $silent -eq 0 ]] && _dim "Cleared auto PHP (no .phpvmrc upstream)."
Expand All @@ -191,7 +192,7 @@ _phpvm_auto() {

if [[ -n "${PHPVM_AUTO_ACTIVE:-}" ]]; then
local old="$PHPVM_VERSIONS/$PHPVM_AUTO_ACTIVE/bin"
PATH=$(echo "$PATH" | tr ':' '\n' | grep -vxF "$old" | paste -sd ':')
PATH=$(echo "$PATH" | tr ':' '\n' | grep -vxF "$old" | paste -sd ':' -)
fi

local new="$PHPVM_VERSIONS/$resolved/bin"
Expand Down Expand Up @@ -284,6 +285,13 @@ _phpvm_print_dep_install() {
_dim " oniguruma-devel libzip-devel zlib-devel readline-devel \\"
_dim " libsodium-devel"
;;
brew)
_dim " brew install \\"
_dim " autoconf bison re2c pkg-config \\"
_dim " openssl@3 libxml2 sqlite curl oniguruma libzip zlib readline \\"
_dim " libpng jpeg webp freetype gmp libsodium gettext"
_dim " (Xcode Command Line Tools required: xcode-select --install)"
;;
*)
_dim " Please install: gcc make autoconf bison re2c pkg-config"
_dim " and dev libraries: libxml2, sqlite3, openssl, curl, oniguruma, libzip"
Expand Down Expand Up @@ -509,7 +517,8 @@ phpvm_install() {
(
cd "$src_dir" || { _err "Could not enter source directory: $src_dir"; exit 1; }

./buildconf --force &>>"$PHPVM_LOG" 2>&1 || true # needed only for git checkouts
# >> + 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

_phpvm_run_logged "Configuring" ./configure "${configure_opts[@]}" \
|| { _err "Configure failed. See log: $PHPVM_LOG"; exit 1; }
Expand Down Expand Up @@ -573,7 +582,7 @@ _phpvm_older_patch_hint() {
[[ -z "$older" ]] && return 0

newest=$(echo "$older" | tail -1)
_dim "Older patch of ${ver%.*} still installed: $(echo "$older" | paste -sd ', ')"
_dim "Older patch of ${ver%.*} still installed: $(echo "$older" | paste -sd ', ' -)"
_dim "Remove it with: phpvm uninstall $newest"
}

Expand Down Expand Up @@ -1171,11 +1180,6 @@ phpvm_help() {
COMPOSER
phpvm composer Install Composer for active PHP version

LARAVEL QUICK SETUP
phpvm ext laravel Enable all Laravel extensions (full)
phpvm ext laravel minimal Required extensions only
phpvm ext laravel full Required + recommended + Redis

AUTO-SWITCH (.phpvmrc)
phpvm auto Switch to the version named in .phpvmrc
phpvm hook enable Enable auto-switching on cd (bash/zsh)
Expand All @@ -1186,6 +1190,11 @@ phpvm_help() {
phpvm upgrade Upgrade phpvm to latest version
phpvm version Show current phpvm version

LARAVEL QUICK SETUP
phpvm ext laravel Enable all Laravel extensions (full)
phpvm ext laravel minimal Required extensions only
phpvm ext laravel full Required + recommended + Redis

EXTENSION MANAGEMENT
phpvm ext list Show loaded extensions
phpvm ext enable <name> Enable extension (conf.d drop-in)
Expand All @@ -1201,8 +1210,8 @@ phpvm_help() {
phpvm ext install xdebug
phpvm ext enable opcache

Install dir: $PHPVM_DIR
Build log: $PHPVM_LOG
Home: $PHPVM_DIR
Build log: $PHPVM_LOG

EOF
}
Expand Down
106 changes: 106 additions & 0 deletions tests/windows/CACert.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
Describe 'Set-IniCACert' {
BeforeAll {
. $PSScriptRoot/Common.ps1
$script:Bundle = 'C:\Users\test\.phpvm\cacert.pem'
}

It 'Uncomments and points both directives at the bundle' {
$ini = @"
[curl]
;curl.cainfo =

[openssl]
;openssl.cafile=
"@
$out = Set-IniCACert $ini $Bundle
$out | Should -Match ([regex]::Escape("curl.cainfo = `"$Bundle`""))
$out | Should -Match ([regex]::Escape("openssl.cafile = `"$Bundle`""))
$out | Should -Not -Match '(?m)^;+\s*curl\.cainfo'
}

It 'Overwrites directives that already have a value' {
$ini = "curl.cainfo = `"D:\old\ca.pem`"`r`nopenssl.cafile = `"D:\old\ca.pem`""
$out = Set-IniCACert $ini $Bundle
$out | Should -Not -Match ([regex]::Escape('D:\old\ca.pem'))
$out | Should -Match ([regex]::Escape("curl.cainfo = `"$Bundle`""))
}

It 'Appends both directives when absent' {
$out = Set-IniCACert "memory_limit = 128M" $Bundle
$out | Should -Match ([regex]::Escape("curl.cainfo = `"$Bundle`""))
$out | Should -Match ([regex]::Escape("openssl.cafile = `"$Bundle`""))
}

It 'Is idempotent' {
$once = Set-IniCACert ";curl.cainfo =`r`n;openssl.cafile =" $Bundle
$twice = Set-IniCACert $once $Bundle
$twice | Should -Be $once
}

It 'Does not touch unrelated lines' {
$ini = "extension_dir = `"C:\php\ext`"`r`n;curl.cainfo ="
$out = Set-IniCACert $ini $Bundle
$out | Should -Match ([regex]::Escape("extension_dir = `"C:\php\ext`""))
}
}

Describe 'Update-IniCACert' {
BeforeAll {
. $PSScriptRoot/Common.ps1
}

It 'Patches a php.ini file on disk' {
$ini = Join-Path $TestDrive 'php.ini'
";curl.cainfo =`r`n;openssl.cafile =" | Set-Content $ini -NoNewline
Update-IniCACert $ini 'C:\bundle\cacert.pem' | Should -BeTrue
Get-Content $ini -Raw | Should -Match ([regex]::Escape('curl.cainfo = "C:\bundle\cacert.pem"'))
}

It 'Returns $false when php.ini is missing' {
Update-IniCACert (Join-Path $TestDrive 'nope.ini') 'C:\bundle\cacert.pem' | Should -BeFalse
}

It 'Returns $false when bundle path is empty' {
$ini = Join-Path $TestDrive 'php2.ini'
'x = 1' | Set-Content $ini
Update-IniCACert $ini '' | Should -BeFalse
}
}

Describe 'Get-CABundle' {
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 'Returns the existing bundle without downloading' {
'-----BEGIN CERTIFICATE-----' | Set-Content $PHPVM_CACERT
Mock -CommandName Invoke-Download -MockWith { throw 'should not be called' }
Get-CABundle | Should -Be $PHPVM_CACERT
Remove-Item $PHPVM_CACERT -Force
}

It 'Downloads and keeps a valid PEM bundle' {
Mock -CommandName Invoke-Download -MockWith {
param($url, $dest)
"-----BEGIN CERTIFICATE-----`nMIIB...`n-----END CERTIFICATE-----" | Set-Content $dest
}
Get-CABundle | Should -Be $PHPVM_CACERT
Test-Path $PHPVM_CACERT | Should -BeTrue
Remove-Item $PHPVM_CACERT -Force
}

It 'Rejects a payload that is not a PEM bundle and returns $null' {
Mock -CommandName Invoke-Download -MockWith {
param($url, $dest)
'<html>proxy login</html>' | Set-Content $dest
}
Get-CABundle 3> $null | Should -BeNullOrEmpty
Test-Path $PHPVM_CACERT | Should -BeFalse
}
}
6 changes: 6 additions & 0 deletions tests/windows/PureFunctions.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ Describe 'Get-VSVersion' {
It 'Defaults to vs17 for future majors' {
Get-VSVersion '9.0.0' | Should -Be 'vs17'
}

It 'Does not throw on non-version input (StrictMode guard)' {
Get-VSVersion '' | Should -Be 'vs17'
Get-VSVersion 'composer' | Should -Be 'vs17'
Get-VSVersion '8' | Should -Be 'vs17'
}
}

Describe 'Get-WebString' {
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.9.1
1.10.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.9.1"
$PHPVM_VERSION = "1.10.0"
$PHPVM_DIR = if ($env:PHPVM_DIR) { $env:PHPVM_DIR } else { "$env:USERPROFILE\.phpvm" }
$PHPVM_BIN = "$PHPVM_DIR\bin"

Expand Down
Loading
Loading