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 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 }}" 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 ------------------------ 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"] + } +}