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
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ phpvm install 8.3.0 # install a specific PHP version
phpvm install 8.3 # install latest 8.3.x patch (auto-resolves)
phpvm install 8 # install latest 8.x patch (e.g. 8.5.x)
phpvm install 7.4 # works for older lines (7.x, 5.x)
phpvm install 8.4.16 --no-use # install but keep the current version active
phpvm use 8.3.0 # switch active version
phpvm list # list installed versions
phpvm current # show active version
Expand All @@ -77,7 +78,13 @@ phpvm ini # open php.ini in editor
```

A successful `phpvm install` automatically activates the freshly installed
version, so you can skip a separate `phpvm use` for the common case.
version, so you can skip a separate `phpvm use` for the common case. Pass
`--no-use` to install a version in the background without switching to it.

Long installs report live progress: a download bar on Windows, and a spinner
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.

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

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

Expand Down
84 changes: 74 additions & 10 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.8.3"
PHPVM_VERSION="1.9.0"
PHPVM_DIR="${PHPVM_DIR:-$HOME/.phpvm}"
PHPVM_VERSIONS="$PHPVM_DIR/versions"
PHPVM_CURRENT="$PHPVM_DIR/current"
Expand Down Expand Up @@ -297,6 +297,52 @@ _phpvm_cpus() {
nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2
}

# Run a long build step with its output appended to $PHPVM_LOG, showing a live
# spinner + elapsed time so a multi-minute `make` never looks hung.
# _phpvm_run_logged "Building with 8 cores" make -j8
# The spinner is drawn on stderr and only when stderr is a terminal, so CI and
# the bats suite see exactly the output they saw before. Returns the real exit
# code of the command.
_phpvm_run_logged() {
local label="$1"; shift

if [[ ! -t 2 ]]; then
_step "$label ..."
"$@" >> "$PHPVM_LOG" 2>&1
return $?
fi

"$@" >> "$PHPVM_LOG" 2>&1 &
local pid=$!

# Ctrl+C must take the build down with us, not orphan it.
trap 'kill "$pid" 2>/dev/null; printf "\r\033[K" >&2; trap - INT; return 130' INT

# Plain ASCII frames + explicit index: ${var:i++:1} is a bashism that bites
# in zsh, and Unicode braille spinners mangle on older terminals.
local frames="|/-\\"
local i=0 start=$SECONDS elapsed frame
while kill -0 "$pid" 2>/dev/null; do
elapsed=$((SECONDS - start))
frame=${frames:$i:1}
i=$(( (i + 1) % 4 ))
printf '\r\033[36m %s %s ... %dm%02ds\033[0m' \
"$frame" "$label" $((elapsed / 60)) $((elapsed % 60)) >&2
sleep 0.5
done

wait "$pid"
local rc=$?
trap - INT
printf '\r\033[K' >&2

elapsed=$((SECONDS - start))
if [[ $rc -eq 0 ]]; then
_step "$label ... done in $((elapsed / 60))m$((elapsed % 60))s"
fi
return $rc
}

# Resolve a partial version to the highest published patch on php.net.
# "8" -> latest 8.x (e.g. 8.5.7)
# "8.3" -> latest 8.3.x (e.g. 8.3.31)
Expand Down Expand Up @@ -332,8 +378,16 @@ _phpvm_resolve_remote() {
# phpvm install <version>
# ==============================================================================
phpvm_install() {
local ver="$1"
[[ -z "$ver" ]] && { _err "Usage: phpvm install <version> (e.g. phpvm install 8.3.0)"; return 1; }
local ver="" no_use=0
while [[ $# -gt 0 ]]; do
case "$1" in
--no-use) no_use=1 ;;
-*) _err "Unknown option: $1. Usage: phpvm install <version> [--no-use]"; return 1 ;;
*) [[ -z "$ver" ]] && ver="$1" ;;
esac
shift
done
[[ -z "$ver" ]] && { _err "Usage: phpvm install <version> [--no-use] (e.g. phpvm install 8.3.0)"; return 1; }

# Partial version: "8" -> latest 8.x, "8.3" -> latest 8.3.x.
if [[ "$ver" =~ ^[0-9]+$ || "$ver" =~ ^[0-9]+\.[0-9]+$ ]]; then
Expand All @@ -349,6 +403,14 @@ phpvm_install() {
fi
fi

# Reject non-versions up front; otherwise they only fail later as a confusing
# "Download failed" from php.net.
if [[ ! "$ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
_err "Invalid version '$ver'. Usage: phpvm install <version> (e.g. phpvm install 8.3.0)"
[[ "$ver" == "composer" ]] && _dim "Did you mean: phpvm composer"
return 1
fi

local target="$PHPVM_VERSIONS/$ver"

if [[ -d "$target" ]]; then
Expand Down Expand Up @@ -393,7 +455,6 @@ phpvm_install() {
tar -xzf "$cache_file" -C "$PHPVM_CACHE"

# Configure
_step "Configuring (this may take a minute) ..."
mkdir -p "$target"

local configure_opts=(
Expand Down Expand Up @@ -438,7 +499,7 @@ phpvm_install() {
return 1
}
./buildconf --force &>>"$PHPVM_LOG" 2>&1 || true # needed only for git checkouts
./configure "${configure_opts[@]}" >> "$PHPVM_LOG" 2>&1 || {
_phpvm_run_logged "Configuring" ./configure "${configure_opts[@]}" || {
_err "Configure failed. See log: $PHPVM_LOG"
rm -rf "$target"
return 1
Expand All @@ -447,16 +508,14 @@ phpvm_install() {
# Build
local cpus
cpus=$(_phpvm_cpus)
_step "Building with $cpus cores (grab a coffee, this takes a few minutes) ..."
make -j"$cpus" >> "$PHPVM_LOG" 2>&1 || {
_phpvm_run_logged "Building with $cpus cores" make -j"$cpus" || {
_err "Build failed. See log: $PHPVM_LOG"
rm -rf "$target"
return 1
}

# Install
_step "Installing ..."
make install >> "$PHPVM_LOG" 2>&1 || {
_phpvm_run_logged "Installing" make install || {
_err "Install failed. See log: $PHPVM_LOG"
rm -rf "$target"
return 1
Expand All @@ -476,7 +535,11 @@ phpvm_install() {

_ok "PHP $ver installed successfully."

# Activate the freshly built version right away (point 4 — auto-use).
# Activate the freshly built version right away, unless opted out.
if [[ $no_use -eq 1 ]]; then
_dim "Not switching (--no-use). Run: phpvm use $ver"
return 0
fi
phpvm_use "$ver"
}

Expand Down Expand Up @@ -1061,6 +1124,7 @@ phpvm_help() {

VERSION MANAGEMENT
phpvm install <version> Build & install a PHP version
--no-use install without switching to it
phpvm use <version> Switch the active PHP version
phpvm list List installed versions
phpvm current Show active version info
Expand Down
76 changes: 76 additions & 0 deletions tests/linux/commands.bats
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,79 @@ EOF
[ "$status" -eq 0 ]
[ "$output" = "8.3.5" ]
}

# ---------- phpvm_install version guard ----------

@test "install: rejects a non-version argument before touching the network" {
run phpvm_install composer
[ "$status" -ne 0 ]
[[ "$output" == *"Invalid version 'composer'"* ]]
[[ "$output" == *"Did you mean: phpvm composer"* ]]
}

@test "install: rejects a malformed version" {
run phpvm_install 8.3.x
[ "$status" -ne 0 ]
[[ "$output" == *"Invalid version"* ]]
}

@test "install: full x.y.z passes the guard (already-installed path)" {
_fake_php_install 8.3.0
run phpvm_install 8.3.0
[ "$status" -eq 0 ]
[[ "$output" == *"already installed"* ]]
}

# ---------- _phpvm_run_logged ----------

@test "run_logged: returns the command's exit code" {
export PHPVM_LOG="$BATS_TEST_TMPDIR/build.log"
run _phpvm_run_logged "Failing" false
[ "$status" -ne 0 ]
run _phpvm_run_logged "Passing" true
[ "$status" -eq 0 ]
}

@test "run_logged: appends command output to the build log, not stdout" {
export PHPVM_LOG="$BATS_TEST_TMPDIR/build.log"
: > "$PHPVM_LOG"
run _phpvm_run_logged "Echoing" echo "secret-build-noise"
[ "$status" -eq 0 ]
[[ "$output" != *"secret-build-noise"* ]]
grep -q "secret-build-noise" "$PHPVM_LOG"
}

@test "run_logged: prints the label when stderr is not a tty (no spinner)" {
export PHPVM_LOG="$BATS_TEST_TMPDIR/build.log"
run _phpvm_run_logged "Configuring" true
[ "$status" -eq 0 ]
[[ "$output" == *"Configuring"* ]]
# The spinner frames must never reach a non-tty stream.
[[ "$output" != *$'\r'* ]]
}

# ---------- phpvm install --no-use ----------

@test "install --no-use: flag is stripped, version still parsed (flag last)" {
run phpvm_install 8.3.x --no-use
[ "$status" -ne 0 ]
[[ "$output" == *"Invalid version '8.3.x'"* ]]
}

@test "install --no-use: flag is stripped, version still parsed (flag first)" {
run phpvm_install --no-use 8.3.x
[ "$status" -ne 0 ]
[[ "$output" == *"Invalid version '8.3.x'"* ]]
}

@test "install --no-use: rejects an unknown option" {
run phpvm_install 8.3.0 --bogus
[ "$status" -ne 0 ]
[[ "$output" == *"Unknown option: --bogus"* ]]
}

@test "install: bare --no-use without a version still errors on usage" {
run phpvm_install --no-use
[ "$status" -ne 0 ]
[[ "$output" == *"Usage: phpvm install"* ]]
}
77 changes: 77 additions & 0 deletions tests/windows/Install.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
Describe 'Invoke-Install version guard' {
BeforeAll {
. $PSScriptRoot/Common.ps1
}

It 'Rejects a non-version argument instead of throwing' {
Mock -CommandName Write-Err -MockWith {}
Mock -CommandName Write-Dim -MockWith {}
Mock -CommandName Resolve-PHPURL -MockWith { throw 'must not be reached' }

{ Invoke-Install 'composer' } | Should -Not -Throw

Should -Invoke Write-Err -Times 1 -ParameterFilter {
$m -like "Invalid version 'composer'*"
}
Should -Invoke Resolve-PHPURL -Times 0
}

It 'Hints at the composer command on that specific typo' {
Mock -CommandName Write-Err -MockWith {}
Mock -CommandName Write-Dim -MockWith {}
Mock -CommandName Resolve-PHPURL -MockWith { throw 'must not be reached' }

Invoke-Install 'composer'

Should -Invoke Write-Dim -Times 1 -ParameterFilter {
$m -like '*phpvm composer*'
}
}

It 'Rejects a malformed version' {
Mock -CommandName Write-Err -MockWith {}
Mock -CommandName Resolve-PHPURL -MockWith { throw 'must not be reached' }

{ Invoke-Install '8.3.x' } | Should -Not -Throw

Should -Invoke Write-Err -Times 1 -ParameterFilter {
$m -like 'Invalid version*'
}
Should -Invoke Resolve-PHPURL -Times 0
}
}

Describe 'Invoke-Install --no-use parsing' {
BeforeAll {
. $PSScriptRoot/Common.ps1
}

BeforeEach {
Mock -CommandName Write-Err -MockWith {}
Mock -CommandName Write-Dim -MockWith {}
Mock -CommandName Resolve-PHPURL -MockWith { throw 'must not be reached' }
}

# The flag must be stripped in either position, matching the Linux arg loop.
# A deliberately malformed version proves which token was taken as the version.
It 'Strips the flag when it trails the version' {
Invoke-Install '8.3.x' '--no-use'
Should -Invoke Write-Err -Times 1 -ParameterFilter { $m -like "Invalid version '8.3.x'*" }
}

It 'Strips the flag when it leads the version' {
Invoke-Install '--no-use' '8.3.x'
Should -Invoke Write-Err -Times 1 -ParameterFilter { $m -like "Invalid version '8.3.x'*" }
}

It 'Errors on usage when --no-use is passed with no version' {
Invoke-Install '--no-use' ''
Should -Invoke Write-Err -Times 1 -ParameterFilter { $m -like 'Usage: phpvm install*' }
}

It 'Rejects an unknown option' {
Invoke-Install '8.3.0' '--bogus'
Should -Invoke Write-Err -Times 1 -ParameterFilter { $m -like 'Unknown option: --bogus*' }
Should -Invoke Resolve-PHPURL -Times 0
}
}
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.8.3
1.9.0
2 changes: 1 addition & 1 deletion windows/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

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

Expand Down
Loading