From c42dcaff2dcab27439901cd533831f4b5213299d Mon Sep 17 00:00:00 2001 From: Xin Date: Thu, 1 Oct 2026 23:15:35 +0100 Subject: [PATCH] feat: consume prebuilt V8 archives in wheel CI, retire GCP runners Wheel builds now run entirely on free GitHub-hosted runners: - x86_64 wheels on ubuntu-latest, aarch64 wheels natively on ubuntu-24.04-arm (no more QEMU), both inside manylinux_2_28 containers via maturin-action. - The prebuilt librusty_v8 static library + src binding are downloaded from the librusty_v8-v{version} release (version derived from Cargo.lock) and consumed via RUSTY_V8_ARCHIVE / RUSTY_V8_SRC_BINDING_PATH, so wheel builds no longer compile V8. A missing archive release fails loudly with instructions to run the 'Build V8 archive' workflow. - aarch64 wheel tests run natively on arm runners instead of QEMU. - GCP spot-VM provisioning (provision/cleanup jobs, startup.sh) and the from-source wheel build script are removed. - Add verify-v8-archive workflow (manual dispatch): whole-archive -z,defs link test under real glibc 2.28 proving archive linkability (first-party Rust FFI symbols stubbed, anything else fails). --- .github/scripts/build-manylinux-wheels.sh | 51 ----- .github/scripts/startup.sh | 53 ----- .github/workflows/CI.yml | 233 ++++++---------------- .github/workflows/verify-v8-archive.yml | 73 +++++++ 4 files changed, 139 insertions(+), 271 deletions(-) delete mode 100755 .github/scripts/build-manylinux-wheels.sh delete mode 100644 .github/scripts/startup.sh create mode 100644 .github/workflows/verify-v8-archive.yml diff --git a/.github/scripts/build-manylinux-wheels.sh b/.github/scripts/build-manylinux-wheels.sh deleted file mode 100755 index a38b307..0000000 --- a/.github/scripts/build-manylinux-wheels.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash -set -e - -# Build script for manylinux Python wheels -# Usage: ./build-wheels.sh [TARGET_ARCH] [MANYLINUX_VERSION] [OUTPUT_DIR] -# TARGET_ARCH: x86_64-unknown-linux-gnu (default) or aarch64-unknown-linux-gnu -# MANYLINUX_VERSION: 2_28 (default) -# OUTPUT_DIR: dist (default) - -TARGET_ARCH="${1:-x86_64-unknown-linux-gnu}" -MANYLINUX_VERSION="${2:-2_28}" -OUTPUT_DIR="${3:-dist}" - -echo "=== Building manylinux Python wheels for ${TARGET_ARCH} ===" -echo "Manylinux version: ${MANYLINUX_VERSION}" -echo "Output directory: ${OUTPUT_DIR}" - -# Verify environment -echo "Rust version:" -rustc --version -echo "Cargo version:" -cargo --version -echo "Maturin version:" -maturin --version - -# Patch Cargo.toml to use V8 from git (fixes missing files in crates.io package) -echo "" -echo "Patching Cargo.toml to use V8 from git..." -if ! grep -q "\[patch.crates-io\]" Cargo.toml; then - echo "" >> Cargo.toml - echo "# Patched by build script: use V8 from git instead of crates.io" >> Cargo.toml - echo "[patch.crates-io]" >> Cargo.toml - echo 'v8 = { git = "https://github.com/denoland/rusty_v8", tag = "v142.1.0" }' >> Cargo.toml - echo "✓ Applied V8 git patch to Cargo.toml" -else - echo "✓ V8 patch already exists in Cargo.toml" -fi - -# Build wheels -echo "" -echo "Starting maturin build..." -maturin build \ - --target "${TARGET_ARCH}" \ - --manylinux "${MANYLINUX_VERSION}" \ - --release \ - --out "${OUTPUT_DIR}" \ - -vv - -echo "=== Build completed successfully ===" -echo "Wheels generated:" -ls -lh "${OUTPUT_DIR}" diff --git a/.github/scripts/startup.sh b/.github/scripts/startup.sh deleted file mode 100644 index 3b66258..0000000 --- a/.github/scripts/startup.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash -set -e - -echo "=== Starting GitHub Actions Runner Setup ===" - -# Install dependencies -echo "Installing dependencies..." -apt-get update -apt-get install -y curl jq - -# Install Docker -echo "Installing Docker..." -curl -fsSL https://get.docker.com/ -o get-docker.sh -sh get-docker.sh -systemctl start docker -systemctl enable docker -docker --version - -# Create a user for the runner -echo "Creating runner user..." -useradd -m -s /bin/bash runner -usermod -aG docker runner - -# Download and install GitHub Actions runner -echo "Downloading GitHub Actions runner..." -cd /home/runner -RUNNER_VERSION="2.329.0" -curl -o actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz -tar xzf ./actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz -chown -R runner:runner /home/runner - -# Get runner token from metadata -echo "Fetching configuration from GCP metadata..." -RUNNER_TOKEN=$(curl -s -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/attributes/runner-token) -REPO_URL=$(curl -s -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/attributes/repo-url) -RUNNER_NAME=$(curl -s -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/attributes/runner-name) - -echo "Repository URL: ${REPO_URL}" -echo "Runner Name: ${RUNNER_NAME}" -echo "Token length: ${#RUNNER_TOKEN} characters" - -# Validate we got the token -if [ -z "$RUNNER_TOKEN" ] || [ "$RUNNER_TOKEN" = "null" ]; then - echo "ERROR: Failed to get runner token from metadata" - exit 1 -fi - -# Configure and start the runner -echo "Configuring GitHub Actions runner..." -su - runner -c "cd /home/runner && ./config.sh --url ${REPO_URL} --token ${RUNNER_TOKEN} --name ${RUNNER_NAME} --labels self-hosted,gcp,spot --unattended" - -echo "Starting runner..." -su - runner -c "cd /home/runner && ./run.sh" diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 694f7c9..ef30d6b 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -8,7 +8,7 @@ on: permissions: contents: write # Required for creating releases - id-token: write # Required for Workload Identity Federation and PyPI trusted publishing + id-token: write # Required for PyPI trusted publishing jobs: @@ -53,144 +53,69 @@ jobs: name: wheels-sdist path: dist - # Provision GCP Spot VM with self-hosted runner - provision: - runs-on: ubuntu-latest - outputs: - runner-name: ${{ steps.create-vm.outputs.runner_name }} + # Build manylinux wheels on free GitHub runners, consuming the prebuilt V8 + # static library published by the "Build V8 archive" workflow. V8 is only + # compiled from source when the v8 crate version in Cargo.lock changes; + # wheel builds just link against the archive. + linux: + runs-on: ${{ matrix.platform.runner }} + strategy: + fail-fast: false + matrix: + platform: + - runner: ubuntu-latest + target: x86_64-unknown-linux-gnu + arch: x86_64 + - runner: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + arch: aarch64 steps: - uses: actions/checkout@v5 - - id: auth - uses: google-github-actions/auth@v3 - with: - workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} - service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - - uses: google-github-actions/setup-gcloud@v3 - - - name: Get GitHub Runner Token - id: get-token + - name: Resolve prebuilt V8 archive + id: v8 + env: + GH_TOKEN: ${{ github.token }} run: | - echo "Requesting runner registration token for ${{ github.repository }}" - RESPONSE=$(curl -s -X POST \ - -H "Authorization: token ${{ secrets.GH_PAT }}" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/${{ github.repository }}/actions/runners/registration-token) - - TOKEN=$(echo "$RESPONSE" | jq -r .token) - - if [ "$TOKEN" = "null" ] || [ -z "$TOKEN" ]; then - echo "Failed to get registration token" - echo "Error message: $(echo "$RESPONSE" | jq -r .message)" + VERSION=$(sed -n '/^name = "v8"$/{n;s/^version = "\(.*\)"/\1/p;}' Cargo.lock) + if [ -z "$VERSION" ]; then + echo "::error::Failed to determine v8 crate version from Cargo.lock" exit 1 fi + TAG="librusty_v8-v${VERSION}" + echo "Using prebuilt V8 archive release: $TAG" + mkdir -p .v8 + if ! gh release download "$TAG" --repo "${{ github.repository }}" \ + --pattern "src_binding_release_${{ matrix.platform.target }}.rs" --dir .v8; then + echo "::error::Missing release $TAG (asset src_binding_release_${{ matrix.platform.target }}.rs). Run the 'Build V8 archive' workflow after bumping v8 in Cargo.lock." + exit 1 + fi + echo "archive_url=https://github.com/${{ github.repository }}/releases/download/${TAG}/librusty_v8_release_${{ matrix.platform.target }}.a.gz" >> "$GITHUB_OUTPUT" - echo "::add-mask::$TOKEN" - echo "token=$TOKEN" >> $GITHUB_OUTPUT - echo "Successfully obtained registration token" - - - name: Create Spot VM - id: create-vm - run: | - RUNNER_NAME="gcp-runner-${{ github.run_id }}-${{ github.run_attempt }}" - echo "runner_name=$RUNNER_NAME" >> $GITHUB_OUTPUT - - gcloud compute instances create $RUNNER_NAME \ - --zone=${{ vars.GCP_ZONE || 'us-central1-a' }} \ - --machine-type=${{ vars.GCP_MACHINE_TYPE || 'n2-highcpu-32' }} \ - --provisioning-model=SPOT \ - --instance-termination-action=DELETE \ - --image-family=ubuntu-2204-lts \ - --image-project=ubuntu-os-cloud \ - --boot-disk-size=200GB \ - --metadata=runner-token=${{ steps.get-token.outputs.token }},repo-url=https://github.com/${{ github.repository }},runner-name=$RUNNER_NAME \ - --metadata-from-file=startup-script=.github/scripts/startup.sh - - - name: Wait for Runner Registration - run: | - echo "Waiting for runner to register..." - for i in {1..60}; do - if curl -s -H "Authorization: token ${{ secrets.GH_PAT }}" \ - https://api.github.com/repos/${{ github.repository }}/actions/runners \ - | jq -e '.runners[] | select(.name == "${{ steps.create-vm.outputs.runner_name }}")' > /dev/null; then - echo "Runner registered successfully!" - exit 0 - fi - echo "Attempt $i/60: Runner not yet registered, waiting 10s..." - sleep 10 - done - echo "Runner failed to register within 10 minutes" - exit 1 - - # Build manylinux wheels (x86_64 and aarch64) on self-hosted GCP runner - build-wheels-manylinux: - needs: provision - runs-on: [self-hosted, gcp, spot] - steps: - - uses: actions/checkout@v5 - - - name: Verify runner environment - run: | - echo "Building manylinux wheels" - echo "Runner name: ${{ runner.name }}" - echo "Runner OS: ${{ runner.os }}" - echo "Branch: ${{ github.ref }}" - echo "Commit: ${{ github.sha }}" - uname -a - df -h - free -h - docker --version - - # Build x86_64 - - name: Build x86_64 Docker image - run: | - docker build \ - -t jsrun-manylinux-builder:x86_64 \ - -f .github/docker/Dockerfile.x86_64 \ - .github/docker/ - - - name: Build x86_64 wheels in Docker - run: | - mkdir -p dist - docker run --rm \ - -v "$(pwd):/workdir" \ - -w /workdir \ - jsrun-manylinux-builder:x86_64 \ - bash .github/scripts/build-manylinux-wheels.sh x86_64-unknown-linux-gnu - - # Build aarch64 - - name: Build aarch64 Docker image - run: | - docker build \ - -t jsrun-manylinux-builder:aarch64 \ - -f .github/docker/Dockerfile.aarch64 \ - .github/docker/ - - - name: Build aarch64 wheels in Docker - run: | - mkdir -p dist - docker run --rm \ - -v "$(pwd):/workdir" \ - -w /workdir \ - jsrun-manylinux-builder:aarch64 \ - bash .github/scripts/build-manylinux-wheels.sh aarch64-unknown-linux-gnu - - # Upload all wheels - - name: List all built wheels - run: | - echo "All built wheels:" - ls -lh dist/ + - name: Build wheels + uses: PyO3/maturin-action@v1 + env: + RUSTY_V8_ARCHIVE: ${{ steps.v8.outputs.archive_url }} + RUSTY_V8_SRC_BINDING_PATH: ${{ github.workspace }}/.v8/src_binding_release_${{ matrix.platform.target }}.rs + with: + target: ${{ matrix.platform.target }} + manylinux: 2_28 + args: --release --out dist + # maturin-action forwards RUST*-prefixed env vars into the build + # container; the explicit -e flags are belt and braces. + docker-options: -e RUSTY_V8_ARCHIVE -e RUSTY_V8_SRC_BINDING_PATH + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - name: Upload wheels uses: actions/upload-artifact@v4 with: - name: wheels-linux-manylinux - path: dist/*.whl + name: wheels-linux-${{ matrix.platform.arch }} + path: dist - # Test built manylinux wheels on GitHub ubuntu runners + # Test built manylinux wheels against all supported Python versions. + # A single cp310-abi3 wheel per architecture covers 3.10+. test-wheels-x86_64: - needs: build-wheels-manylinux + needs: linux runs-on: ubuntu-latest strategy: matrix: @@ -201,10 +126,10 @@ jobs: - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - - name: Download manylinux wheels + - name: Download wheels uses: actions/download-artifact@v6 with: - name: wheels-linux-manylinux + name: wheels-linux-x86_64 path: dist - name: Install and test wheel run: | @@ -216,60 +141,34 @@ jobs: pytest tests/ -v test-wheels-aarch64: - needs: build-wheels-manylinux - runs-on: ubuntu-latest + needs: linux + runs-on: ubuntu-24.04-arm strategy: matrix: python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] fail-fast: false steps: - uses: actions/checkout@v5 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - uses: actions/setup-python@v6 with: - platforms: arm64 - - name: Download manylinux wheels + python-version: ${{ matrix.python-version }} + - name: Download wheels uses: actions/download-artifact@v6 with: - name: wheels-linux-manylinux + name: wheels-linux-aarch64 path: dist - - name: Test wheel in aarch64 container + - name: Install and test wheel run: | + python -m pip install --upgrade pip WHEEL=$(ls dist/*cp310-abi3*aarch64.whl | head -n 1) - echo "Testing wheel: $WHEEL" - docker run --rm --platform linux/arm64 \ - -v "$(pwd):/work" \ - -w /work \ - python:${{ matrix.python-version }}-slim \ - bash -c " - pip install --upgrade pip && \ - pip install $WHEEL && \ - pip install pytest pytest-asyncio && \ - pytest tests/ -v - " - - # Cleanup: Delete the GCP VM - cleanup: - needs: [provision, build-wheels-manylinux] - if: always() - runs-on: ubuntu-latest - steps: - - uses: google-github-actions/auth@v3 - with: - workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} - service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} - - - uses: google-github-actions/setup-gcloud@v3 - - - name: Delete VM - run: | - gcloud compute instances delete ${{ needs.provision.outputs.runner-name }} \ - --zone=${{ vars.GCP_ZONE || 'us-central1-a' }} \ - --quiet || echo "VM may have already been terminated" + echo "Installing wheel: $WHEEL" + pip install "$WHEEL" + pip install pytest pytest-asyncio + pytest tests/ -v # Create GitHub Release with all artifacts release: - needs: [macos, sdist, build-wheels-manylinux, test-wheels-x86_64, test-wheels-aarch64] + needs: [macos, sdist, linux, test-wheels-x86_64, test-wheels-aarch64] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') steps: diff --git a/.github/workflows/verify-v8-archive.yml b/.github/workflows/verify-v8-archive.yml new file mode 100644 index 0000000..3e627d0 --- /dev/null +++ b/.github/workflows/verify-v8-archive.yml @@ -0,0 +1,73 @@ +name: Verify V8 archive + +# Proves the published librusty_v8 archive is actually linkable under +# manylinux_2_28 (glibc 2.28): a whole-archive link into a shared object +# with -Wl,-z,defs forces every object in the archive to be linked and +# every undefined symbol to resolve against glibc 2.28 - the same demand +# the wheel link makes. + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify-x86_64: + runs-on: ubuntu-latest + container: quay.io/pypa/manylinux_2_28_x86_64 + timeout-minutes: 20 + steps: + - name: Show toolchain and glibc version + run: | + ldd --version | head -1 + gcc --version | head -1 + + - name: Download archive from release + run: | + curl -sSfL -o librusty_v8.a.gz \ + https://github.com/${{ github.repository }}/releases/download/librusty_v8-v150.4.0/librusty_v8_release_x86_64-unknown-linux-gnu.a.gz + gunzip librusty_v8.a.gz + ls -lh librusty_v8.a + + - name: Whole-archive link test (-z,defs on glibc 2.28) + run: | + # Some symbols are intentionally undefined in the C++ archive: they + # are rusty_v8's and V8-Temporal's C++->Rust FFI hooks, provided by + # the Rust side (v8 and temporal_capi crates) at the final cargo + # link. Denoland's own prebuilt has the identical undefined set. + # Strategy: attempt the strict link, collect missing symbols, assert + # ALL of them are first-party FFI names (anything else - e.g. a + # glibc symbol - fails the job), then stub and re-link strictly. + link() { + gcc -shared -o /tmp/librusty_v8_test.so \ + -Wl,--whole-archive librusty_v8.a -Wl,--no-whole-archive \ + "$@" \ + -Wl,-z,defs \ + -lpthread -ldl -lm -lrt -lgcc_s + } + if link 2>link_err.txt; then + echo "Linked with no stubs needed" + else + awk -F'`' '/undefined reference to/{split($2,a,"\x27"); print a[1]}' link_err.txt \ + | sort -u > missing_syms.txt + echo "Missing symbols: $(wc -l < missing_syms.txt)" + NON_FFI=$(grep -vE '^(temporal_rs_|rusty_v8_|v8__|v8_inspector__|cppgc__|crdtp__)' missing_syms.txt || true) + if [ -n "${NON_FFI}" ]; then + echo "ERROR: unresolved symbols that are NOT first-party Rust FFI (likely glibc incompatibility):" + echo "${NON_FFI}" + exit 1 + fi + awk '{print "void "$1"(void) {}"}' missing_syms.txt > stubs.c + gcc -c stubs.c -o stubs.o + link stubs.o + fi + ls -lh /tmp/librusty_v8_test.so + echo "Whole-archive link succeeded under glibc 2.28" + + - name: Inspect resulting shared object + run: | + echo "=== glibc version requirements of the linked .so ===" + objdump -T /tmp/librusty_v8_test.so | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -5 + echo "=== NEEDED libraries ===" + objdump -p /tmp/librusty_v8_test.so | grep NEEDED