diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9e997c..f182ed9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index c84bf27..1a5543c 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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: + + +### `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 | @@ -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. --- diff --git a/linux/install.sh b/linux/install.sh index 49005fc..a7bdab4 100644 --- a/linux/install.sh +++ b/linux/install.sh @@ -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" diff --git a/linux/phpvm.sh b/linux/phpvm.sh index 15cfd44..43405df 100644 --- a/linux/phpvm.sh +++ b/linux/phpvm.sh @@ -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" @@ -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 @@ -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" } @@ -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)." @@ -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" @@ -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" @@ -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; } @@ -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" } @@ -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) @@ -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 Enable extension (conf.d drop-in) @@ -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 } diff --git a/tests/windows/CACert.Tests.ps1 b/tests/windows/CACert.Tests.ps1 new file mode 100644 index 0000000..34efabb --- /dev/null +++ b/tests/windows/CACert.Tests.ps1 @@ -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) + 'proxy login' | Set-Content $dest + } + Get-CABundle 3> $null | Should -BeNullOrEmpty + Test-Path $PHPVM_CACERT | Should -BeFalse + } +} diff --git a/tests/windows/PureFunctions.Tests.ps1 b/tests/windows/PureFunctions.Tests.ps1 index 2e1cd0f..70906b2 100644 --- a/tests/windows/PureFunctions.Tests.ps1 +++ b/tests/windows/PureFunctions.Tests.ps1 @@ -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' { diff --git a/version.txt b/version.txt index 9ab8337..81c871d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.9.1 +1.10.0 diff --git a/windows/install.ps1 b/windows/install.ps1 index 3958136..e77d0a2 100644 --- a/windows/install.ps1 +++ b/windows/install.ps1 @@ -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" diff --git a/windows/phpvm.ps1 b/windows/phpvm.ps1 index 9ebb9cc..1ff38b6 100644 --- a/windows/phpvm.ps1 +++ b/windows/phpvm.ps1 @@ -15,11 +15,13 @@ Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" # -- Constants ----------------------------------------------------------------- -$PHPVM_VERSION = "1.9.1" +$PHPVM_VERSION = "1.10.0" $PHPVM_DIR = if ($env:PHPVM_DIR) { $env:PHPVM_DIR } else { "$env:USERPROFILE\.phpvm" } $VERSIONS_DIR = "$PHPVM_DIR\versions" $CURRENT_LINK = "$PHPVM_DIR\current" $PHPVM_BIN = "$PHPVM_DIR\bin" +$PHPVM_CACERT = "$PHPVM_DIR\cacert.pem" +$PHPVM_CACERT_URL = "https://curl.se/ca/cacert.pem" # -- Update checker (once per day, via version.txt) --------------------------- $PHPVM_UPDATE_URL = "https://raw.githubusercontent.com/devhardiyanto/phpvm/main/version.txt" @@ -121,10 +123,13 @@ function Get-PHPBuildInfo ([string]$phpExe = "") { if ($version -match '(\d+\.\d+\.\d+)') { $version = $Matches[1] } $short = $version -replace '^(\d+\.\d+)\..*', '$1' - $tsLine = ($raw | Select-String "Thread Safety" | Select-Object -First 1).ToString() - $isTS = $tsLine -match "enabled" + # Both lines can be absent when php -i fails or emits garbage; .ToString() + # on the empty pipeline would throw a raw MethodInvocationException. + $tsLine = $raw | Select-String "Thread Safety" | Select-Object -First 1 + $isTS = $tsLine -and ($tsLine.ToString() -match "enabled") - $compLine = ($raw | Select-String "Compiler" | Select-Object -First 1).ToString() + $compLine = $raw | Select-String "Compiler" | Select-Object -First 1 + $compLine = if ($compLine) { $compLine.ToString() } else { "" } $vs = switch -Regex ($compLine) { "MSVC17|VS17" { "vs17"; break } "MSVC16|VS16" { "vs16"; break } @@ -156,6 +161,8 @@ function Get-PHPBuildInfo ([string]$phpExe = "") { # Per windows.php.net: 5.x -> vc11, 7.0-7.1 -> vc14, 7.2-7.4 -> vc15, # 8.0-8.3 -> vs16, 8.4+ -> vs17. function Get-VSVersion ([string]$ver) { + # Anything that isn't x.y... would blow up the [int] casts below. + if ($ver -notmatch '^\d+\.\d+') { return "vs17" } $major = [int]($ver -split '\.')[0] $minor = [int]($ver -split '\.')[1] if ($major -eq 5) { return "vc11" } @@ -224,7 +231,11 @@ function Get-CurrentVersion { if (-not (Test-Path $CURRENT_LINK)) { return $null } $item = Get-Item $CURRENT_LINK -Force if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { - return Split-Path $item.Target -Leaf + # On PS 5.1 Target can be missing or empty for some reparse points; + # Split-Path $null would throw under StrictMode. + $target = if ($item.PSObject.Properties['Target']) { @($item.Target)[0] } else { $null } + if (-not $target) { return $null } + return Split-Path $target -Leaf } return $null } @@ -337,6 +348,59 @@ function Unblock-PHPVMPath ([string]$path) { } } +# -- CA bundle (curl.cainfo / openssl.cafile) ---------------------------------- +# Windows PHP builds ship no CA bundle, so every HTTPS request from PHP fails +# with cURL error 60 until one is configured. One shared bundle in $PHPVM_DIR +# serves all installed versions. + +# Ensure $PHPVM_CACERT exists; download the Mozilla bundle if missing (or -Force). +# Best-effort: returns the bundle path, or $null when absent and undownloadable. +# Never throws - an offline install must still succeed. +function Get-CABundle ([switch]$Force) { + if ((Test-Path $PHPVM_CACERT) -and -not $Force) { return $PHPVM_CACERT } + + Write-Step "Downloading CA bundle (curl.se/ca/cacert.pem) ..." + $tmp = "$env:TEMP\phpvm-cacert.pem" + try { + Invoke-Download $PHPVM_CACERT_URL $tmp + $head = Get-Content $tmp -TotalCount 200 -ErrorAction Stop + if (-not ($head -match "BEGIN CERTIFICATE")) { throw "not a PEM bundle" } + Move-Item $tmp $PHPVM_CACERT -Force + Write-Ok "CA bundle saved: $PHPVM_CACERT" + } catch { + Remove-Item $tmp -Force -ErrorAction SilentlyContinue + Write-Warn "Could not fetch CA bundle: $_" + if (Test-Path $PHPVM_CACERT) { return $PHPVM_CACERT } + Write-Dim "HTTPS from PHP may fail with cURL error 60. Retry later: phpvm cacert update" + return $null + } + return $PHPVM_CACERT +} + +# Point curl.cainfo and openssl.cafile at $bundlePath in raw php.ini content. +# Uncomments/overwrites existing directives; appends a block when absent. +function Set-IniCACert ([string]$content, [string]$bundlePath) { + foreach ($key in @("curl.cainfo", "openssl.cafile")) { + $pattern = "(?m)^;*\s*$([regex]::Escape($key))\s*=.*$" + $line = "$key = `"$bundlePath`"" + if ($content -match $pattern) { + $content = [regex]::Replace($content, $pattern, $line.Replace('$', '$$')) + } else { + $content = $content.TrimEnd() + "`r`n$line`r`n" + } + } + return $content +} + +# Apply the shared bundle to one php.ini file. No-op if either is missing. +function Update-IniCACert ([string]$iniPath, [string]$bundlePath) { + if (-not $bundlePath -or -not (Test-Path $iniPath)) { return $false } + $before = Get-Content $iniPath -Raw + $after = Set-IniCACert $before $bundlePath + if ($after -ne $before) { $after | Set-Content $iniPath -NoNewline } + return $true +} + # Look up the expected SHA-256 for a PHP zip on windows.php.net. # Returns lowercase hex digest, or $null if no checksum is published. function Get-PHPZipHash ([string]$zipUrl) { @@ -382,14 +446,16 @@ function Test-URLExists ([string]$url) { # ============================================================================== function Invoke-Install ([string]$ver, [string]$flag) { - # Accept --no-use in either position, matching the Linux arg loop. + # Accept flags in either position, matching the Linux arg loop. $noUse = $false + $noCacert = $false $positional = @() foreach ($a in @($ver, $flag)) { - if (-not $a) { continue } - if ($a -eq "--no-use") { $noUse = $true } - elseif ($a -like "-*") { Write-Err "Unknown option: $a. Usage: phpvm install [--no-use]"; return } - else { $positional += $a } + if (-not $a) { continue } + if ($a -eq "--no-use") { $noUse = $true } + elseif ($a -eq "--no-cacert") { $noCacert = $true } + elseif ($a -like "-*") { Write-Err "Unknown option: $a. Usage: phpvm install [--no-use] [--no-cacert]"; return } + else { $positional += $a } } $ver = if ($positional.Count -gt 0) { $positional[0] } else { "" } @@ -485,6 +551,15 @@ function Invoke-Install ([string]$ver, [string]$flag) { $content | Set-Content $ini -NoNewline } + # Windows PHP has no CA bundle -> HTTPS from PHP fails (cURL error 60). + # Point this version at the shared bundle, unless opted out. + if (-not $noCacert) { + $bundle = Get-CABundle + if ($bundle -and (Update-IniCACert $ini $bundle)) { + Write-Ok "CA bundle configured (curl.cainfo / openssl.cafile)." + } + } + Write-Ok "PHP $ver installed successfully." # Activate the freshly installed version right away, unless opted out. @@ -788,17 +863,17 @@ function Show-PHPVMHookStatus { } function Invoke-Hook ([string]$sub) { + # enable/disable match the Linux verbs; install/uninstall kept as aliases. switch ($sub.ToLower()) { - 'install' { Install-PHPVMHook } - 'uninstall' { Uninstall-PHPVMHook } - 'remove' { Uninstall-PHPVMHook } + { $_ -in 'enable', 'install' } { Install-PHPVMHook } + { $_ -in 'disable', 'uninstall', 'remove' } { Uninstall-PHPVMHook } 'status' { Show-PHPVMHookStatus } default { Write-Host "" Write-Host " phpvm hook - manage the PowerShell auto-switch hook" -ForegroundColor Cyan - Write-Host " phpvm hook install Add the prompt hook to `$PROFILE" - Write-Host " phpvm hook uninstall Remove the hook" - Write-Host " phpvm hook status Check whether the hook is installed" + Write-Host " phpvm hook enable Enable .phpvmrc auto-switching (prompt hook in `$PROFILE)" + Write-Host " phpvm hook disable Disable the hook" + Write-Host " phpvm hook status Check whether the hook is enabled" Write-Host "" } } @@ -1346,22 +1421,25 @@ function Show-Help { VERSION MANAGEMENT phpvm install Download & install a PHP version - --no-use install without switching to it + --no-use install without switching to it + --no-cacert skip CA bundle configuration phpvm use Switch the active PHP version phpvm list List installed versions phpvm current Show active version info phpvm uninstall Remove a PHP version phpvm which Path to active php.exe phpvm ini Open active php.ini in Notepad + phpvm fix-ini Sync extension_dir & CA bundle in active php.ini + phpvm cacert [status|update] Manage the shared CA bundle (HTTPS/TLS) COMPOSER phpvm composer Install Composer for active PHP version AUTO-SWITCH (.phpvmrc) phpvm auto Switch to the version named in .phpvmrc - phpvm hook install Install the PowerShell prompt hook - phpvm hook uninstall Remove the prompt hook - phpvm hook status Check whether the hook is installed + phpvm hook enable Enable auto-switching (PowerShell prompt hook) + phpvm hook disable Disable the hook + phpvm hook status Check whether the hook is enabled SELF UPDATE phpvm upgrade Upgrade phpvm to latest version @@ -1421,9 +1499,44 @@ function Invoke-FixIni { Write-Ok "Added extension_dir -> $extPath" } + # Repair the CA bundle wiring too - fixes cURL error 60 on installs that + # predate the shared bundle. + $bundle = Get-CABundle + if ($bundle -and (Update-IniCACert $ini $bundle)) { + Write-Ok "CA bundle configured (curl.cainfo / openssl.cafile)." + } + Write-Dim "Verify: phpvm ext list" } +# phpvm cacert [status|update] - manage the shared CA bundle. +function Invoke-Cacert ([string]$sub) { + switch ($sub.ToLower()) { + "update" { + $bundle = Get-CABundle -Force + if (-not $bundle) { return } + $cur = Get-CurrentVersion + if ($cur) { + if (Update-IniCACert "$VERSIONS_DIR\$cur\php.ini" $bundle) { + Write-Ok "Active php.ini points at the refreshed bundle." + } + } + } + { $_ -in "", "status" } { + if (Test-Path $PHPVM_CACERT) { + $age = [int]((Get-Date) - (Get-Item $PHPVM_CACERT).LastWriteTime).TotalDays + Write-Ok "CA bundle: $PHPVM_CACERT (updated $age day(s) ago)" + Write-Dim "Refresh with: phpvm cacert update" + } else { + Write-Warn "No CA bundle yet. Run: phpvm cacert update" + } + } + default { + Write-Err "Usage: phpvm cacert [status|update]" + } + } +} + function Invoke-Upgrade { $scriptUrl = "https://raw.githubusercontent.com/devhardiyanto/phpvm/main/windows/phpvm.ps1" @@ -1487,7 +1600,7 @@ function Get-Levenshtein ([string]$a, [string]$b) { # Unknown command: suggest the nearest match instead of dumping the full help. function Invoke-Unknown ([string]$cmd) { $cmds = @("install","use","list","ls","current","uninstall","remove", - "which","ini","fix-ini","ext","composer","auto","hook", + "which","ini","fix-ini","cacert","ext","composer","auto","hook", "upgrade","update","version","help") $best = ""; $bestd = 99 foreach ($c in $cmds) { @@ -1517,6 +1630,7 @@ if (-not $env:PHPVM_NO_ENTRY) { "which" { Invoke-Which } "ini" { Invoke-Ini } "fix-ini" { Invoke-FixIni } + "cacert" { Invoke-Cacert $SubOrVer } "ext" { Invoke-Ext $SubOrVer $Arg2 $Arg3 } "auto" { Invoke-Auto } "hook" { Invoke-Hook $SubOrVer }