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
19 changes: 7 additions & 12 deletions .github/docker/Dockerfile.x86_64
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
# x86_64 manylinux builder for jsrun Python wheels

# Donor stage for a glibc 2.28 sysroot (AlmaLinux 8 based). The rust-cross
# base image used to ship its own crosstool-ng toolchain + 2.28 sysroot at
# /usr/x86_64-unknown-linux-gnu, but a mid-2026 rebuild of the mutable tag
# left that directory empty, so V8 would otherwise compile against the
# Ubuntu host glibc (2.35) and reference symbols newer than manylinux_2_28
# allows (e.g. pthread_cond_clockwait, glibc 2.30).
# Donor stage for a glibc 2.28 sysroot (AlmaLinux 8). The rust-cross base
# image's own toolchain/sysroot went missing in a mid-2026 rebuild of its
# mutable tag; without a 2.28 sysroot, V8 compiles against the Ubuntu host
# glibc and references symbols too new for manylinux_2_28 wheels.
FROM quay.io/pypa/manylinux_2_28_x86_64 AS sysroot

FROM ghcr.io/rust-cross/manylinux_2_28-cross:x86_64
Expand Down Expand Up @@ -38,14 +36,11 @@ ENV CXX=clang-19
ENV LIBCLANG_PATH=/usr/lib/llvm-19/lib
ENV PATH=/usr/lib/llvm-19/bin:${PATH}

# glibc 2.28 sysroot donated from the official manylinux image, so V8 is
# compiled against 2.28 headers. V8 gates newer glibc APIs behind
# compile-time glibc version checks, so old headers keep the archive
# manylinux_2_28 compatible.
# V8 gates newer glibc APIs behind compile-time version checks, so building
# against 2.28 headers keeps the archive manylinux_2_28 compatible. The gcc
# dir provides crt objects/libgcc needed when linking host tool executables.
COPY --from=sysroot /usr/include /opt/manylinux_2_28_sysroot/usr/include
COPY --from=sysroot /usr/lib64 /opt/manylinux_2_28_sysroot/usr/lib64
# GCC installation dir: provides crtbeginS.o/crtendS.o, libgcc.a and the
# libgcc_s.so devel symlink needed when linking host tool executables.
COPY --from=sysroot /usr/lib/gcc /opt/manylinux_2_28_sysroot/usr/lib/gcc
RUN ln -s usr/lib64 /opt/manylinux_2_28_sysroot/lib64
ENV TARGET_SYSROOT=/opt/manylinux_2_28_sysroot
Expand Down
42 changes: 11 additions & 31 deletions .github/scripts/build-v8-archive.sh
Original file line number Diff line number Diff line change
@@ -1,36 +1,23 @@
#!/bin/bash
set -euo pipefail

# Build a prebuilt rusty_v8 static library archive for reuse by wheel builds.
#
# Runs inside the manylinux builder containers (.github/docker/Dockerfile.*)
# with the repository mounted at the working directory. Produces the same
# artifact layout as denoland's rusty_v8 releases:
# librusty_v8_release_{target}.a.gz
# src_binding_release_{target}.rs
# Build a prebuilt rusty_v8 static library archive for reuse by wheel builds
# (same artifact layout as denoland's rusty_v8 releases). Runs inside the
# manylinux builder containers with the repository mounted at the workdir.
#
# Usage: ./build-v8-archive.sh TARGET_TRIPLE [OUTPUT_DIR]
# TARGET_TRIPLE: x86_64-unknown-linux-gnu or aarch64-unknown-linux-gnu
# OUTPUT_DIR: dist-v8 (default)

TARGET_ARCH="${1:?Usage: build-v8-archive.sh TARGET_TRIPLE [OUTPUT_DIR]}"
OUTPUT_DIR="${2:-dist-v8}"

V8_VERSION=$(sed -n '/^name = "v8"$/{n;s/^version = "\(.*\)"/\1/p;}' Cargo.lock)
if [ -z "${V8_VERSION}" ]; then
echo "Failed to determine v8 crate version from Cargo.lock" >&2
exit 1
fi
V8_VERSION=$("$(dirname "$0")/v8-version.sh")

echo "=== Building rusty_v8 v${V8_VERSION} from source for ${TARGET_ARCH} ==="
rustc --version
cargo --version

# The rust-cross base images configure CARGO_TARGET_*_LINKER env vars, but
# the mutable image tags drift and the referenced cross-gcc may no longer
# exist (observed with x86_64-unknown-linux-gnu-gcc). Only host build
# scripts are linked here (-p v8 produces an rlib), so fall back to the
# clang installed by our Dockerfiles when a configured linker is missing.
# The mutable base-image tags drift; fall back to our clang when a configured
# linker is missing (only host build scripts are linked; -p v8 yields an rlib).
for var in $(env | sed -n 's/^\(CARGO_TARGET_[A-Z0-9_]*_LINKER\)=.*/\1/p'); do
linker="${!var}"
if ! command -v "${linker}" >/dev/null 2>&1; then
Expand All @@ -39,13 +26,10 @@ for var in $(env | sed -n 's/^\(CARGO_TARGET_[A-Z0-9_]*_LINKER\)=.*/\1/p'); do
fi
done

# The crates.io package is missing files required for from-source builds,
# so patch v8 to the matching git tag.
# crates.io package lacks files needed for from-source builds; use the git tag.
if ! grep -q "\[patch.crates-io\]" Cargo.toml; then
cat >> Cargo.toml <<EOF

# Patched by build script: use V8 from git (crates.io package lacks files
# needed for from-source builds)
[patch.crates-io]
v8 = { git = "https://github.com/denoland/rusty_v8", tag = "v${V8_VERSION}" }
EOF
Expand All @@ -67,8 +51,7 @@ for artifact in "${STATIC_LIB}" "${SRC_BINDING}"; do
fi
done

# Diagnostics: show the GN args that were actually resolved and whether the
# sysroot made it into the compile commands.
# Diagnostics: resolved gn args and whether the sysroot reached the compiler.
GN_OUT_DIR="${BUILD_DIR}/gn_out"
if [ -f "${GN_OUT_DIR}/args.gn" ]; then
echo "=== resolved gn args (${GN_OUT_DIR}/args.gn) ==="
Expand All @@ -77,10 +60,9 @@ fi
echo "=== --sysroot flags in ninja compile commands ==="
grep -rho -- "--sysroot=[^ \"]*" "${GN_OUT_DIR}"/*.ninja 2>/dev/null | sort | uniq -c || echo "(none found)"

# Guard against glibc drift: the archive must stay linkable under
# manylinux_2_28 (glibc 2.28). Fail loudly if the static lib references
# symbols introduced in later glibc versions (the denylist covers known
# offenders from glibc 2.29-2.38; extend it if auditwheel ever complains).
# The archive must stay linkable under manylinux_2_28 (glibc 2.28): fail on
# strong references to newer glibc symbols instead of publishing a broken
# artifact. Weak references are harmless (linker leaves them null).
GLIBC_POST_228_SYMBOLS='^(pthread_cond_clockwait|pthread_mutex_clocklock|pthread_rwlock_clockrdlock|pthread_rwlock_clockwrlock|sem_clockwait|pthread_clockjoin_np|gettid|getdents64|__libc_single_threaded|arc4random|arc4random_buf|arc4random_uniform|close_range|__isoc23_.*)$'
UNDEFINED_SYMBOLS=""
for nm_bin in llvm-nm "${TARGET_ARCH}-nm" nm; do
Expand All @@ -96,8 +78,6 @@ if [ -z "${UNDEFINED_SYMBOLS}" ]; then
echo "ERROR: no nm tool in the image could read ${STATIC_LIB}; refusing to publish unaudited archive" >&2
exit 1
fi
# Only strong undefined references ("U") are fatal: weak ones ("w"/"v") are
# left null by the linker on older glibc and handled by runtime fallbacks.
WEAK_MATCHES=$(echo "${UNDEFINED_SYMBOLS}" | awk '$1 == "w" || $1 == "v" {print $2}' | sort -u | grep -E "${GLIBC_POST_228_SYMBOLS}" || true)
if [ -n "${WEAK_MATCHES}" ]; then
echo "Note: weak references to post-2.28 symbols (harmless): ${WEAK_MATCHES}"
Expand Down
9 changes: 9 additions & 0 deletions .github/scripts/v8-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash
# Print the v8 crate version pinned in Cargo.lock.
set -euo pipefail
VERSION=$(sed -n '/^name = "v8"$/{n;s/^version = "\(.*\)"/\1/p;}' "$(dirname "$0")/../../Cargo.lock")
if [ -z "${VERSION}" ]; then
echo "Failed to determine v8 crate version from Cargo.lock" >&2
exit 1
fi
echo "${VERSION}"
96 changes: 75 additions & 21 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
- uses: astral-sh/setup-uv@v7
- uses: dtolnay/rust-toolchain@stable
# Rust version comes from rust-toolchain.toml via the preinstalled rustup.
- name: Test
run: make all
- name: Build wheels
Expand All @@ -33,30 +33,28 @@ jobs:
args: --release --out dist
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
- name: Upload wheels
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: wheels-macos-${{ matrix.platform.target }}
path: dist

sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
- name: Upload sdist
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: wheels-sdist
path: dist

# 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.
# Build manylinux wheels against the prebuilt V8 archive published by the
# "Build V8 archive" workflow (V8 only compiles when Cargo.lock bumps it).
linux:
runs-on: ${{ matrix.platform.runner }}
strategy:
Expand All @@ -77,11 +75,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
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
VERSION=$(.github/scripts/v8-version.sh)
TAG="librusty_v8-v${VERSION}"
echo "Using prebuilt V8 archive release: $TAG"
mkdir -p .v8
Expand All @@ -101,17 +95,47 @@ jobs:
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
uses: actions/upload-artifact@v7
with:
name: wheels-linux-${{ matrix.platform.arch }}
path: dist

# Build musllinux wheels. denoland ships musl prebuilts (no glibc-style
# symbol versioning), which rusty_v8's build script downloads automatically.
linux-musl:
runs-on: ${{ matrix.platform.runner }}
strategy:
fail-fast: false
matrix:
platform:
- runner: ubuntu-latest
target: x86_64-unknown-linux-musl
arch: x86_64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
arch: aarch64
steps:
- uses: actions/checkout@v5
- name: Build wheels
uses: PyO3/maturin-action@v1
env:
# Extension modules need dynamic musl; Rust's musl targets default to static crt.
RUSTFLAGS: -C target-feature=-crt-static
with:
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2
args: --release --out dist
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: wheels-musllinux-${{ matrix.platform.arch }}
path: dist

# Test built manylinux wheels against all supported Python versions.
# A single cp310-abi3 wheel per architecture covers 3.10+.
test-wheels-x86_64:
Expand All @@ -127,7 +151,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Download wheels
uses: actions/download-artifact@v6
uses: actions/download-artifact@v8
with:
name: wheels-linux-x86_64
path: dist
Expand All @@ -153,7 +177,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Download wheels
uses: actions/download-artifact@v6
uses: actions/download-artifact@v8
with:
name: wheels-linux-aarch64
path: dist
Expand All @@ -166,16 +190,46 @@ jobs:
pip install pytest pytest-asyncio
pytest tests/ -v

# Test musllinux wheels inside Alpine containers (run via docker rather
# than `container:` because node-based actions cannot execute on musl).
test-wheels-musl:
needs: linux-musl
runs-on: ${{ matrix.platform.runner }}
strategy:
fail-fast: false
matrix:
platform:
- runner: ubuntu-latest
arch: x86_64
- runner: ubuntu-24.04-arm
arch: aarch64
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v5
- name: Download wheels
uses: actions/download-artifact@v8
with:
name: wheels-musllinux-${{ matrix.platform.arch }}
path: dist
- name: Test wheel in Alpine container
run: |
WHEEL=$(ls dist/*cp310-abi3*musllinux*${{ matrix.platform.arch }}.whl | head -n 1)
echo "Testing wheel: $WHEEL"
docker run --rm \
-v "$(pwd):/work" -w /work \
python:${{ matrix.python-version }}-alpine \
sh -c "pip install --upgrade pip && pip install $WHEEL pytest pytest-asyncio && pytest tests/ -q"

# Create GitHub Release with all artifacts
release:
needs: [macos, sdist, linux, test-wheels-x86_64, test-wheels-aarch64]
needs: [macos, sdist, linux, linux-musl, test-wheels-x86_64, test-wheels-aarch64, test-wheels-musl]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v5

- name: Download all artifacts
uses: actions/download-artifact@v6
uses: actions/download-artifact@v8
with:
path: artifacts

Expand Down Expand Up @@ -205,7 +259,7 @@ jobs:
id-token: write # Required for PyPI trusted publishing
steps:
- name: Download all artifacts
uses: actions/download-artifact@v6
uses: actions/download-artifact@v8
with:
path: artifacts

Expand Down
39 changes: 15 additions & 24 deletions .github/workflows/build-v8-archive.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
name: Build V8 archive

# Builds rusty_v8 from source inside the manylinux_2_28 containers and
# publishes the static library + src binding as GitHub release assets.
# The wheel CI consumes these via RUSTY_V8_ARCHIVE/RUSTY_V8_SRC_BINDING_PATH,
# so V8 only needs to be compiled when the v8 crate version in Cargo.lock
# changes (run this workflow manually after such a bump).
#
# Why from source: denoland's prebuilt archives require glibc >= 2.30
# (e.g. pthread_cond_clockwait), but manylinux_2_28 wheels must link
# against glibc 2.28.
# Compiles rusty_v8 from source inside the manylinux_2_28 containers and
# publishes the static library + src binding as release assets, which wheel
# CI consumes via RUSTY_V8_ARCHIVE. Run manually after the v8 version in
# Cargo.lock changes. (From source because denoland's gnu prebuilts require
# glibc > 2.28; see .github/docker/Dockerfile.x86_64.)

on:
workflow_dispatch:
Expand All @@ -32,10 +28,9 @@ jobs:
strategy:
fail-fast: false
matrix:
arch: ${{ fromJSON(
inputs.arch == 'x86_64' && '[{"name":"x86_64","target":"x86_64-unknown-linux-gnu","dockerfile":"Dockerfile.x86_64"}]'
|| inputs.arch == 'aarch64' && '[{"name":"aarch64","target":"aarch64-unknown-linux-gnu","dockerfile":"Dockerfile.aarch64"}]'
|| '[{"name":"x86_64","target":"x86_64-unknown-linux-gnu","dockerfile":"Dockerfile.x86_64"},{"name":"aarch64","target":"aarch64-unknown-linux-gnu","dockerfile":"Dockerfile.aarch64"}]') }}
arch: ${{ fromJSON(inputs.arch == 'all' && '["x86_64","aarch64"]' || format('["{0}"]', inputs.arch)) }}
env:
TARGET: ${{ matrix.arch }}-unknown-linux-gnu
steps:
- uses: actions/checkout@v5

Expand All @@ -49,19 +44,15 @@ jobs:
- name: Read v8 version from Cargo.lock
id: v8
run: |
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
VERSION=$(.github/scripts/v8-version.sh)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Building archive for rusty_v8 v$VERSION"

- name: Build builder image
run: |
docker build \
-t jsrun-v8-builder:${{ matrix.arch.name }} \
-f .github/docker/${{ matrix.arch.dockerfile }} \
-t jsrun-v8-builder:${{ matrix.arch }} \
-f .github/docker/Dockerfile.${{ matrix.arch }} \
.github/docker/

- name: Build V8 from source
Expand All @@ -70,16 +61,16 @@ jobs:
docker run --rm \
-v "$(pwd):/workdir" \
-w /workdir \
jsrun-v8-builder:${{ matrix.arch.name }} \
bash .github/scripts/build-v8-archive.sh ${{ matrix.arch.target }} dist-v8
jsrun-v8-builder:${{ matrix.arch }} \
bash .github/scripts/build-v8-archive.sh "$TARGET" dist-v8

- name: Upload artifacts to release
env:
GH_TOKEN: ${{ github.token }}
run: |
# Prerelease, and deliberately not matching the v* pattern that
# triggers release CI.
TAG="librusty_v8-v${{ steps.v8.outputs.version }}"
# Prerelease so archive tags never show up as the "latest" jsrun release.
# The tag deliberately does not match the v* pattern that triggers CI.
gh release view "$TAG" >/dev/null 2>&1 || \
gh release create "$TAG" \
--prerelease \
Expand Down
Loading
Loading