From 72cdceb953ffa83635583e5767ad1d6db1b8d5f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Ja=C5=A1a?= Date: Thu, 4 Jun 2026 14:50:53 +0200 Subject: [PATCH 1/4] Add composer.json for PIE / Composer installability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declares the extension as type=php-ext so pie.phar (PHP Installer for Extensions) can install it directly, and so the package is discoverable on Packagist as phpv8/v8js. Key fields: - extension-name: v8js — the loaded extension name. (Set explicitly rather than auto-derived from the Composer package name, in case the vendor/package shape changes in the future.) - configure-options: [{name: with-v8js, needs-value: true}] — mirrors the single --with-v8js[=DIR] macro in config.m4. - download-url-method: [pre-packaged-binary, composer-default] — when prebuilt .so assets are attached to a release (see the new release.yml workflow), PIE downloads those; otherwise it falls back to a Composer- default git-archive source build. - os-families-exclude: [windows] — Windows support would require prebuilt DLLs via php/php-windows-builder; not part of this change. - require.php: ^8.1 — matches what this branch supports after PR #545. PHP 8.0 is EOL (Nov 2023). No source files are touched; this is a packaging-only addition. --- composer.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 composer.json diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..270dfc85 --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name": "phpv8/v8js", + "description": "V8 JavaScript Engine for PHP — Composer/PIE-installable", + "type": "php-ext", + "license": "MIT", + "keywords": ["v8", "javascript", "php-ext", "pie", "extension"], + "require": { + "php": "^8.1" + }, + "php-ext": { + "extension-name": "v8js", + "support-zts": true, + "support-nts": true, + "configure-options": [ + { + "name": "with-v8js", + "description": "Path to the V8 install prefix (e.g. /usr, /usr/local, /opt/homebrew). Auto-searches /usr/local and /usr if no value is given.", + "needs-value": true + } + ], + "download-url-method": ["pre-packaged-binary", "composer-default"], + "os-families-exclude": ["windows"] + } +} From c2167e8c9db56c3aa4a592af50228f991f88da39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Ja=C5=A1a?= Date: Thu, 4 Jun 2026 14:51:52 +0200 Subject: [PATCH 2/4] Add release workflow: publish prebuilt PIE binaries on tag push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new GitHub Actions workflow (separate file so it coexists with the existing build-test.yml) that on every tag push: 1. Drafts a GitHub release for the tag (idempotent — gh release view || gh release create — so re-runs on the same tag don't fail). 2. Matrix-builds a .so per platform tuple, each cell running INSIDE the matching official PHP Docker image so the produced binary links against the libnode SOVERSION that ships with that image: - php:X.Y-cli/zts -> Debian trixie, libnode.so.115 (glibc) - php:X.Y-cli/zts-alpine -> Alpine 3.22ish, libnode.so.137 (musl) Building on the bare ubuntu-24.04 runner would link against libnode.so.109 (Node 18 from Ubuntu noble) — missing on the more common php:X.Y-* images and causing 'cannot open shared object file' at PHP startup. 3. Packages the .so per PIE's asset-naming convention and uploads it to the draft release directly via curl + GitHub REST API. Matrix (5 PHP × 2 ZTS × 3 platforms, less excludes = 30 Linux cells): ubuntu-24.04 + debian x86_64 glibc (10 cells, all PHP, NTS+TS) ubuntu-24.04 + alpine x86_64 musl (10 cells) ubuntu-24.04-arm + debian arm64 glibc (10 cells) + 5 macOS cells (currently fail at make due to V8 14 incompatibility — phpv8/v8js#546 — wrapped in continue-on-error) Notes on what this workflow does NOT use, and why: - shivammathur/setup-php is NOT used for Linux: PHP is already provided by the php:X.Y-* container, including the matching ZTS mode and exactly the libc that the rest of the system uses. - php/pie-ext-binary-builder is NOT used: that action has two known bugs that produced unusable binaries in earlier iterations of this workflow: (a) Its libc detection reads only stdout from `ldd --version`, but Alpine's musl ldd writes the 'musl' marker to stderr. Result: Alpine cells silently labeled their assets 'glibc', which then collided in the release with real Debian uploads (asset already_exists errors), and the published 'glibc' assets were a mix of actual-glibc and musl-mislabeled-as- glibc binaries — runtime-broken for whichever variant lost the race. (b) Its Octokit-based upload auto-retries on transient 5xx. If the first request succeeded but the response was dropped, the retry hits 422 (already_exists) and the cell is reported as failed even though the asset was uploaded. The inline shell here merges stderr in libc detection (`ldd --version 2>&1` captured first to avoid pipefail propagating musl ldd's non-zero exit), and uses an idempotent delete-then-POST upload pattern so re-runs don't 422. - arm64+alpine is intentionally excluded. GitHub Actions doesn't currently support running JS actions (e.g., actions/checkout) inside arm64+musl containers — the node20 runtime it injects fails to execute. Tracked at actions/runner. Until that's fixed, arm64+alpine users fall back to source builds via composer-default (with --with-v8js=/usr). End users running 'pie install phpv8/v8js' then get a prebuilt binary matching their (php-version, arch, libc, ZTS) tuple instead of compiling from source. PIE falls back to source build automatically if no matching prebuilt exists for the user's platform — so this workflow's coverage gaps are graceful, not hard failures. --- .github/workflows/release.yml | 207 ++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..378f12e6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,207 @@ +name: Build and release PIE binaries + +on: + push: + tags: ['*'] + +permissions: + contents: read + +jobs: + create-draft-release: + runs-on: ubuntu-latest + permissions: + # contents:write is required to create the draft release. + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-tags: 'true' + ref: ${{ github.ref }} + - name: Create draft release from tag + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release view "${{ github.ref_name }}" >/dev/null 2>&1 \ + || gh release create "${{ github.ref_name }}" --title "${{ github.ref_name }}" --draft --generate-notes + + add-pie-binaries-linux: + needs: [create-draft-release] + runs-on: ${{ matrix.runner }} + # Build inside the official PHP Docker image so the .so links against the + # SOVERSION that ships with that base image: + # php:X.Y-cli/zts -> Debian trixie, libnode.so.115 + # php:X.Y-cli/zts-alpine -> Alpine 3.22ish, libnode.so.137 + # Building on the bare ubuntu-24.04 runner would link against + # libnode.so.109 (Node 18) instead — missing on the more popular php:X.Y + # images and causing dynamic-link failures at PHP startup. Building in + # the matching image aligns the link target with the most likely host. + # The Alpine image is needed separately because Alpine uses musl libc, + # which is ABI-incompatible with glibc; PIE encodes this in the asset + # name as `glibc` vs `musl` and matches based on the user's libc. + container: + image: php:${{ matrix.php-version }}-${{ matrix.zts-mode == 'ts' && 'zts' || 'cli' }}${{ matrix.distro == 'alpine' && '-alpine' || '' }} + strategy: + fail-fast: false + matrix: + # ubuntu-24.04 = x86_64 native runner + # ubuntu-24.04-arm = arm64 native runner (no QEMU) + # The container on each runner inherits the host arch, so each cell + # produces a single-arch binary. + runner: [ubuntu-24.04, ubuntu-24.04-arm] + php-version: ['8.1', '8.2', '8.3', '8.4', '8.5'] + # `php:X.Y-cli`/`-cli-alpine` are NTS; `-zts`/`-zts-alpine` are ZTS. + zts-mode: [nts, ts] + # debian = Debian trixie (libnode.so.115, glibc) + # alpine = Alpine 3.22 (libnode.so.137, musl) + distro: [debian, alpine] + exclude: + # GitHub Actions doesn't currently support running JS actions + # (e.g., actions/checkout) inside arm64+musl containers — the node + # runtime it injects into the container fails to execute. All 10 + # arm64+alpine cells dead at `actions/checkout` step. Skip until + # this is fixed upstream in actions/runner. + - runner: ubuntu-24.04-arm + distro: alpine + permissions: + # contents:write is required to upload to the draft release. + contents: write + steps: + - uses: actions/checkout@v4 + + # The slim php:X.Y-* images don't include build tooling or libnode-dev. + # pie-ext-binary-builder needs jq + zip + a C++ toolchain (it shells out + # to `phpize / ./configure / make / zip`). libv8/libnode headers come + # from libnode-dev (Debian) or nodejs-dev (Alpine); config.m4 searches + # both libv8.so and libnode.so, finding libnode at /usr/include/node. + - name: Install build tooling and libnode (Debian) + if: matrix.distro == 'debian' + run: | + apt-get update + apt-get install -y --no-install-recommends \ + build-essential autoconf libtool pkg-config \ + libnode-dev jq zip ca-certificates curl git + + - name: Install build tooling and nodejs (Alpine) + if: matrix.distro == 'alpine' + run: | + apk add --no-cache \ + build-base autoconf libtool m4 pkgconfig \ + nodejs-dev jq zip ca-certificates curl git + + # Inline replacement for php/pie-ext-binary-builder@0.0.2 with two fixes: + # + # 1. Correct libc detection on Alpine. The upstream action reads only + # `stdout` from `ldd --version`, but Alpine's musl ldd writes the + # "musl" marker to stderr. Alpine cells were silently being labeled + # `glibc`, producing assets named like Debian's and colliding in + # the release with "asset already exists" errors. Merging stderr + # via `2>&1` fixes the detection. + # + # 2. Idempotent upload. The upstream action uses Octokit which + # auto-retries on transient 5xx. If the first request succeeded + # but the response was dropped, the retry hits 422 (already_exists) + # and the cell is reported as failed even though the asset was + # uploaded. Here we explicitly delete an asset with the same name + # before uploading, so a re-run is safe. + - name: Build, package, upload .so + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + + # Platform detection + EXT_NAME=$(jq -r '."php-ext"."extension-name" // (.name | split("/")[1])' composer.json) + PHP_VER=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;') + case "$(uname -m)" in + x86_64) ARCH=x86_64 ;; + aarch64) ARCH=arm64 ;; + *) ARCH=$(uname -m) ;; + esac + # NB: Alpine's musl ldd exits 1 (prints usage) — `set -o pipefail` would + # propagate that and the `if` would see false, falling through to glibc. + # Capture stdout+stderr with `|| true` first, then grep the string. + LDD_OUT=$(ldd --version 2>&1 || true) + if printf "%s" "$LDD_OUT" | grep -qi musl; then LIBC=musl; else LIBC=glibc; fi + ZTS_SUFFIX=$(php -r 'echo PHP_ZTS ? "-zts" : "";') + DEBUG_SUFFIX=$(php -r 'echo PHP_DEBUG ? "-debug" : "";') + ASSET="php_${EXT_NAME}-${TAG}_php${PHP_VER}-${ARCH}-linux-${LIBC}${DEBUG_SUFFIX}${ZTS_SUFFIX}.zip" + echo "Target asset: $ASSET" + + # Build + phpize + ./configure --with-v8js=/usr + make -j"$(nproc)" + + # Package + (cd modules && zip -j "/tmp/$ASSET" "${EXT_NAME}.so") + + # Resolve release ID (drafts visible because we're authenticated) + RELEASE_ID=$(curl -fsSL \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/releases" \ + | jq -r --arg tag "$TAG" '.[] | select(.tag_name==$tag) | .id' | head -1) + if [ -z "$RELEASE_ID" ]; then echo "ERROR: no release for tag $TAG" >&2; exit 1; fi + echo "Release ID: $RELEASE_ID" + + # Idempotent upload: delete existing same-named asset, then POST + EXISTING=$(curl -fsSL \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/$REPO/releases/$RELEASE_ID/assets?per_page=100" \ + | jq -r --arg name "$ASSET" '.[] | select(.name==$name) | .id') + if [ -n "$EXISTING" ]; then + echo "Deleting existing asset $EXISTING" + curl -fsSL -X DELETE \ + -H "Authorization: Bearer $GH_TOKEN" \ + "https://api.github.com/repos/$REPO/releases/assets/$EXISTING" + fi + curl -fsSL -X POST \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Content-Type: application/zip" \ + --data-binary "@/tmp/$ASSET" \ + "https://uploads.github.com/repos/$REPO/releases/$RELEASE_ID/assets?name=$ASSET" \ + >/dev/null + echo "Uploaded $ASSET" + + add-pie-binaries-macos: + needs: [create-draft-release] + runs-on: macos-14 + strategy: + fail-fast: false + matrix: + php-version: ['8.1', '8.2', '8.3', '8.4', '8.5'] + # macOS jobs are NTS only — Homebrew PHP doesn't ship ZTS. + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + env: + phpts: nts + + - name: Install libv8 (macOS) + # NOTE: Homebrew's current `v8` formula is 14.x, which phpv8/v8js (php8 + # branch) cannot build against yet (V8 14.6 removed Local::Holder(), + # changed SetAlignedPointerInInternalField, and replaced String::Write + # with WriteV2). Tracked: https://github.com/phpv8/v8js/issues/546. + # Until that's resolved, macOS cells will fail at `make` and this + # workflow won't publish macOS binaries. Linux cells are unaffected. + run: | + brew install v8 + echo "V8_PREFIX=$(brew --prefix v8)" >> "$GITHUB_ENV" + + - name: Build and release + continue-on-error: true + uses: php/pie-ext-binary-builder@0.0.2 + with: + release-tag: ${{ github.ref_name }} + github-token: ${{ secrets.GITHUB_TOKEN }} + configure-flags: "--with-v8js=${{ env.V8_PREFIX }}" From 300b07b9c30b1e0cba83a21ef8dbe1a1777eed32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Ja=C5=A1a?= Date: Thu, 4 Jun 2026 14:52:09 +0200 Subject: [PATCH 3/4] Add .gitattributes to keep dev-only paths out of the release archive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Composer (or PIE in composer-default mode) downloads this package, it uses GitHub's git archive of the tag. Without export-ignore rules, the archive includes the per-platform README.{Linux,MacOS,Win32}.md build instructions and the PECL-era package.xml — none of which are useful inside an installed extension. They also bloat the download. Specifically: - /.github — CI is for the repo, not the package - /.gitattributes — this file itself - /.gitignore — VCS-only - /.dockerignore — VCS-only - /README.Linux.md — already covered by Compiling latest version docs - /README.MacOS.md — same - /README.Win32.md — same (Windows is excluded by composer.json anyway) - /package.xml — PECL-era manifest; superseded by composer.json Also forces LF line endings on shell/autoconf/workflow files so that a Windows contributor's git checkout with autocrlf=true doesn't break the CI workflow or config.m4. The .cc/.h source files (v8js_*.cc, v8js_*.h, php_v8js*.h, config.m4, config.w32, Makefile.frag) and tests/ are NOT export-ignored — they're the actual extension and stay in the archive. --- .gitattributes | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1e2056f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,19 @@ +# Strip development-only and PECL-era paths from the archive that Composer +# downloads (and that PIE uses for `composer-default` source builds). +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.dockerignore export-ignore +/README.Linux.md export-ignore +/README.MacOS.md export-ignore +/README.Win32.md export-ignore +/package.xml export-ignore + +# Force LF on shell / autoconf / workflow inputs so Windows checkouts don't +# break the build. +*.sh text eol=lf +*.m4 text eol=lf +*.w32 text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.json text eol=lf From c6078613eaacf8c35aa5b9ed9c46a39b16e69099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Ja=C5=A1a?= Date: Thu, 4 Jun 2026 14:52:39 +0200 Subject: [PATCH 4/4] README: document PIE installation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new 'Installation via PIE' section between the existing 'Pre-built binaries' (Docker) and 'Compiling latest version' sections. Non-destructive insertion — surrounding sections are unchanged. Three things this section emphasises that are easy to get wrong: 1. The Debian/Ubuntu install needs libnode-dev (not the removed-from-Debian libv8-dev), and the Alpine install needs nodejs-dev (not just nodejs; the runtime shared library libnode.so.X ships in -dev on Alpine). 2. The fast 'prebuilt' install path is plain `pie install phpv8/v8js` with NO --with-v8js flag. PIE refuses prebuilt downloads when any configure option is passed (it can't honour the flag from a prebuilt), so passing --with-v8js silently disables the prebuilt and triggers a source build. 3. --with-v8js is documented as the source-build escape hatch, for hosts without a matching prebuilt or with a custom V8 install. For PHP 8.1+ users on a supported platform this is the lowest-friction path to a working v8js install: no ./configure, no make, no PECL. --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/README.md b/README.md index 83036303..8186b0ff 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,49 @@ image](https://registry.hub.docker.com/r/stesie/v8js/). It has v8, v8js and php so you can give it a try with PHP in "interactive mode". There is no Apache, etc. running however. +Installation via PIE +-------------------- + +For PHP 8.1+, V8Js can be installed via [PIE (PHP Installer for Extensions)](https://github.com/php/pie) +in one command. PIE will download a matching prebuilt `.so` for your platform if one is published +on the release, or fall back to compiling from source. + +```bash +# Debian / Ubuntu (php:X.Y-cli/fpm/apache, Debian trixie, Ubuntu 25.04+): +sudo apt-get install -y libnode-dev unzip +pie install phpv8/v8js + +# Alpine (php:X.Y-cli-alpine, etc.): +apk add --no-cache nodejs-dev unzip +pie install phpv8/v8js +``` + +Do **not** pass `--with-v8js=...` on the prebuilt path — PIE refuses prebuilt binaries when configure +options are set (it can't know whether the prebuilt was built with the flag you wanted) and falls +back to a source build instead. + +For hosts where no matching prebuilt is published (e.g. macOS Apple Silicon — see issue +[#546](https://github.com/phpv8/v8js/issues/546)) or where you want to build against a custom V8, +pass the path explicitly to force a source build: + +```bash +pie install phpv8/v8js --with-v8js=/usr # Debian/Ubuntu source build +pie install phpv8/v8js --with-v8js=$(brew --prefix v8) # macOS Homebrew (currently V8 14 — see #546) +pie install phpv8/v8js --with-v8js=/opt/v8 # custom V8 build +``` + +Prebuilt binaries are attached to each tagged release for: + +| Platform | PHP versions | NTS | TS | +|------------------------------------------------|--------------------------|------|------| +| linux-glibc-x86_64 (Debian trixie / php:X.Y-cli) | 8.1, 8.2, 8.3, 8.4, 8.5 | ✅ | ✅ | +| linux-glibc-arm64 (Debian trixie / php:X.Y-cli) | 8.1, 8.2, 8.3, 8.4, 8.5 | ✅ | ✅ | +| linux-musl-x86_64 (Alpine / php:X.Y-cli-alpine) | 8.1, 8.2, 8.3, 8.4, 8.5 | ✅ | ✅ | + +The PIE manifest lives in [`composer.json`](composer.json); the release workflow that produces the +prebuilt assets lives in [`.github/workflows/release.yml`](.github/workflows/release.yml). + + Compiling latest version ------------------------