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
38 changes: 37 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,45 @@ jobs:
- name: Run Tests
run: cargo test

e2e-fpm:
name: e2e tests - linux
needs: tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable

- name: Cache Rust dependencies
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2

- name: Install runtime deps
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends netcat-openbsd expect libfcgi-bin

- name: Build pvm (release)
run: cargo build --release

- name: Run E2E suite
env:
PVM_BIN: ${{ github.workspace }}/target/release/pvm
PVM_VERSION_MAJOR_MINOR: '8.5'
run: bash tests/e2e/run.sh

- name: Upload fpm logs on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: fpm-logs
path: |
/tmp/fpm.stdout
/tmp/fpm.stderr
if-no-files-found: ignore

release:
name: Semantic Release
needs: tests
needs: [tests, e2e-fpm]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
Expand Down
167 changes: 167 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,170 @@ pvm init

### Auto-Switching
If you run `pvm init` or manually create a `.php-version` file in a project directory containing `8.3`, PVM will automatically switch to your best local `8.3.x` patch when you `cd` into that folder. The `cd` hook is installed via `pvm env` (Bash, Zsh, or Fish — auto-detected from `$SHELL`).

## Packages

Each PHP version can ship up to three binaries; you pick which during `pvm install` via the MultiSelect prompt. All land under `$PVM_DIR/versions/<full-semver>/bin/`. Upstream reference for all three SAPIs: [static-php.dev — SAPI Reference](https://static-php.dev/en/guide/sapi-reference.html).

| Package | Binary | What it is |
|---------|--------|------------|
| `cli` (default) | `php` | Standard command-line PHP — runs scripts, REPL via `php -a`, drives Composer. See [SAPI Reference: CLI](https://static-php.dev/en/guide/sapi-reference.html#cli). |
| `fpm` | `php-fpm` | FastCGI Process Manager for serving PHP behind nginx/Caddy/Apache. Setup details in the next section + [SAPI Reference: FPM](https://static-php.dev/en/guide/sapi-reference.html#fpm). |
| `micro` | `micro.sfx` | [phpmicro](https://github.com/easysoft/phpmicro) self-contained executable stub — concat with a `.php` or `.phar` to ship a single-file PHP app. Combining requires the upstream [`spc`](https://static-php.dev/en/guide/getting-started.html) toolchain (`spc micro:combine app.phar --output=app`); pvm only delivers the stub. See [SAPI Reference: Micro](https://static-php.dev/en/guide/sapi-reference.html#micro). |

After `pvm use <version>`, every selected binary is on `$PATH` (CLI as `php`, FPM as `php-fpm`); `micro.sfx` stays at its absolute path since it's a build artifact, not something you invoke directly.

## Running PHP-FPM

> Upstream reference: [static-php.dev — SAPI Reference: FPM](https://static-php.dev/en/guide/sapi-reference.html#fpm) documents the binary's CLI flags (`-y`, `-c`, `-t`), a minimal `php-fpm.conf`, and an nginx FastCGI block. The guide below extends that with service wiring (systemd / launchd) and pvm-specific paths.

PVM downloads a static `php-fpm` binary alongside `php` when you tick the `fpm` package during `pvm install`. The static-php-cli tarball ships only the binary — no `php-fpm.conf`, no pool files, no init script — so you wire those up yourself. The binary lives next to the CLI at:

```text
$PVM_DIR/versions/<full-semver>/bin/php-fpm
```

`$PVM_DIR` defaults to `~/.local/share/pvm`. After running `pvm use 8.4` it is also on `$PATH` as plain `php-fpm`.

### 1. Install the fpm package

```bash
pvm install 8.4
# When the MultiSelect prompt appears, tick "fpm" (and "cli" if you want both).
pvm use 8.4
php-fpm -v # confirm it resolves to the pvm-managed binary
which php-fpm # → ~/.local/share/pvm/versions/8.4.x/bin/php-fpm
```

### 2. Create a minimal config

Put these under `~/.config/php-fpm/` (any path works — the binary takes `-y` and `-c`):

`~/.config/php-fpm/php-fpm.conf`:

```ini
[global]
pid = /tmp/php-fpm.pid
error_log = /tmp/php-fpm.log
daemonize = no

include = /home/YOU/.config/php-fpm/pool.d/*.conf
```

`~/.config/php-fpm/pool.d/www.conf`:

```ini
[www]
user = YOU
group = YOU
listen = 127.0.0.1:9000
; or a unix socket:
; listen = /tmp/php-fpm-www.sock
; listen.owner = YOU
; listen.group = YOU
; listen.mode = 0660

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

catch_workers_output = yes
clear_env = no
```

Replace `YOU` with your username (`whoami`).

### 3. Run it in the foreground

```bash
# Validate config first
php-fpm -y ~/.config/php-fpm/php-fpm.conf -t

# Foreground run, logs to stdout
php-fpm -y ~/.config/php-fpm/php-fpm.conf -F

# With a custom php.ini (the static binary has no compiled-in ini path)
php-fpm -c ~/.config/php-fpm/php.ini -y ~/.config/php-fpm/php-fpm.conf -F
```

Flag summary (matches upstream [SAPI Reference: FPM](https://static-php.dev/en/guide/sapi-reference.html#fpm)):

- `-y <file>` — `php-fpm.conf` path (required, no default for static builds)
- `-c <file>` — `php.ini` path (optional; without it, fpm runs with hard-coded defaults)
- `-t` — validate config and exit
- `-F` — stay in foreground (don't fork to daemon)
- `-v` — print version
- `-m` — list compiled-in extensions

### 4. Run it as a service

**systemd (Linux, user unit)** — `~/.config/systemd/user/php-fpm.service`:

```ini
[Unit]
Description=PHP-FPM (managed by pvm)
After=network.target

[Service]
Type=simple
ExecStart=%h/.local/share/pvm/versions/8.4.18/bin/php-fpm -y %h/.config/php-fpm/php-fpm.conf -F
Restart=on-failure

[Install]
WantedBy=default.target
```

```bash
systemctl --user daemon-reload
systemctl --user enable --now php-fpm
journalctl --user -u php-fpm -f
```

Pin the full semver in `ExecStart` (e.g. `8.4.18`) — symlinking to `versions/8.4` is not maintained by pvm, so a future `pvm install 8.4` that resolves to `8.4.19` will not move the service.

**launchd (macOS)** — `~/Library/LaunchAgents/dev.pvm.php-fpm.plist`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>dev.pvm.php-fpm</string>
<key>ProgramArguments</key>
<array>
<string>/Users/YOU/.local/share/pvm/versions/8.4.18/bin/php-fpm</string>
<string>-y</string>
<string>/Users/YOU/.config/php-fpm/php-fpm.conf</string>
<string>-F</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/tmp/php-fpm.out.log</string>
<key>StandardErrorPath</key><string>/tmp/php-fpm.err.log</string>
</dict>
</plist>
```

```bash
launchctl load ~/Library/LaunchAgents/dev.pvm.php-fpm.plist
```

### 5. Hook up nginx

```nginx
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
```

### Notes

- The static binary is self-contained — no system libphp / no extension `.so` files. Run `php-fpm -m` to list the extensions baked into your build.
- Switching the active CLI via `pvm use 8.3` does **not** restart your fpm service; the service runs whichever absolute path you wired into the unit/plist. Bump the path and reload when you upgrade.
- For multiple parallel versions (e.g. 8.3 + 8.4), run two services on different ports/sockets — `pvm` does not multiplex fpm for you.
28 changes: 28 additions & 0 deletions tests/e2e/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Sandbox image for the pvm e2e suite.
# Build: docker build -t pvm-e2e tests/e2e
# Run: docker run --rm \
# -v "$(pwd)/tests/e2e:/home/tester/e2e:ro" \
# -v "$(pwd)/target/release/pvm:/home/tester/pvm:ro" \
# -e PVM_BIN=/home/tester/pvm \
# pvm-e2e bash /home/tester/e2e/run.sh
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive

# Runtime deps mirror what release.yml installs:
# curl, ca-certificates — for install.sh fallback + upstream tarball download
# bash — driver/case scripts
# netcat-openbsd — listener readiness check
# procps — pgrep for worker counting
# expect — drives interactive dialoguer prompts
# libfcgi-bin — supplies cgi-fcgi for FastCGI roundtrip cases
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates bash netcat-openbsd procps \
expect libfcgi-bin \
&& rm -rf /var/lib/apt/lists/*

RUN useradd -m -s /bin/bash tester
USER tester
WORKDIR /home/tester
ENV SHELL=/bin/bash
CMD ["/bin/bash"]
99 changes: 99 additions & 0 deletions tests/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# pvm e2e tests (Linux)

Locks the README "Running PHP-FPM" walkthrough and the core `pvm` flows in lockstep with the upstream static-php-cli FPM tarballs. Runs on every PR via `release.yml` job **e2e tests - linux**.

## Layout

```text
tests/e2e/
├── Dockerfile sandbox image (ubuntu:24.04 + expect + libfcgi-bin)
├── run.sh driver — version resolution + per-case execution
├── _lib.sh shared helpers (run_under_expect, fcgi_call, sandbox guard)
├── README.md this file
└── cases/
├── 01_install.sh real `pvm install` with interactive MultiSelect
├── 02_ls.sh `pvm ls` discovers the version + package tags
├── 03_use_wrapper.sh `pvm use` via shell wrapper switches PATH
├── 04_current.sh `pvm current`
├── 05_php_version_hook.sh `.php-version` cd-hook
├── 06_use_missing.sh missing-version install prompt + decline (#24)
├── 07_patch_update.sh patch-update detection
├── 08_fpm_config.sh README php-fpm.conf + pool.d/{www,sock}.conf, `-t`
├── 09_fpm_run.sh `php-fpm -F` listens on TCP + unix socket
├── 10_fcgi_tcp.sh FastCGI roundtrip over TCP
├── 11_fcgi_sock.sh FastCGI roundtrip over unix socket
├── 12_php_ini_effective.sh `-c php.ini` is effective inside the worker
├── 13_pid_log.sh pid file + error log written
├── 14_fpm_shutdown.sh SIGQUIT clean shutdown
└── 15_uninstall.sh `pvm uninstall` removes the version dir
```

The driver runs each `cases/NN_*.sh` as a fresh bash subprocess so state from one case cannot mask bugs in the next. (The previous monolith silently passed because pvm's 24h `.update_check_guard` suppressed the patch-update prompt for any case after the first one to use it.)

## Why the safety check

`run.sh` mutates `$HOME/.local/share/pvm`, `/tmp/php-fpm.*`, and `~/.config/php-fpm`. Running on a dev machine would clobber whatever real pvm install lives there. So the driver refuses to run unless one of these is true:

- `/.dockerenv` exists (you're inside a container)
- `GITHUB_ACTIONS=true` (you're on a hosted runner)
- `/proc/1/cgroup` shows a container runtime (docker, containerd, kubepods)
- `PVM_E2E_FORCE=1` is set (manual override at your own risk)

## Running locally

Build the sandbox image once (rebuild only when `Dockerfile` changes):

```bash
docker build -t pvm-e2e tests/e2e
```

Build pvm from source:

```bash
cargo build --release
```

Run the full suite:

```bash
docker run --rm \
-v "$(pwd)/tests/e2e:/home/tester/e2e:ro" \
-v "$(pwd)/target/release/pvm:/home/tester/pvm:ro" \
-e PVM_BIN=/home/tester/pvm \
pvm-e2e bash /home/tester/e2e/run.sh
```

## Useful overrides

| Env var | Default | Effect |
|---------|---------|--------|
| `PVM_BIN` | _unset_ | Path to a pre-built pvm. Unset → driver runs `install.sh` and pulls the latest GitHub release. |
| `PVM_VERSION_MAJOR_MINOR` | `8.5` | Major.minor line to test. Both `LATEST` and `PREVIOUS` patches must exist upstream. |
| `PVM_E2E_ONLY` | _unset_ | Space-separated case files to run, e.g. `"01_install.sh 07_patch_update.sh"`. Useful for reproducing one failure. |
| `FPM_TCP_ADDR` | `127.0.0.1:9000` | Override the TCP listener if `:9000` is busy. |
| `PVM_E2E_FORCE` | _unset_ | Set to `1` to bypass the sandbox guard. Don't. |

### Run a single case

```bash
docker run --rm \
-v "$(pwd)/tests/e2e:/home/tester/e2e:ro" \
-v "$(pwd)/target/release/pvm:/home/tester/pvm:ro" \
-e PVM_BIN=/home/tester/pvm \
-e PVM_E2E_ONLY="07_patch_update.sh" \
pvm-e2e bash /home/tester/e2e/run.sh
```

Note: cases 09–13 depend on case 08 having written the FPM config and on a running FPM process — running them in isolation will fail unless you also include the cases that set up that state.

### Test PHP 8.4 instead of 8.5

```bash
docker run --rm ... \
-e PVM_VERSION_MAJOR_MINOR=8.4 \
pvm-e2e bash /home/tester/e2e/run.sh
```

## In CI

The job lives in `.github/workflows/release.yml` as `e2e tests - linux`, chained `tests` → `e2e tests - linux` → `release`. It builds pvm from source, installs `expect` + `libfcgi-bin` + `netcat-openbsd`, and invokes `tests/e2e/run.sh` directly on the runner — no Docker layer needed because the runner *is* the sandbox (`GITHUB_ACTIONS=true`).
Loading