From 1b1cb02d47faac2a1f432c371db6044cd302e078 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 19 Sep 2026 21:08:54 +0000 Subject: [PATCH 1/6] stpyv8: Add version 13.1.201.22 Builds V8 13.1.201.22 from source with V8's own riscv64-linux-gnu- gcc_toolchain and wraps it in the Boost.Python extension stpyv8 ships, mirroring upstream's linux.yml build-v8/build jobs narrowed to riscv64. --- .github/workflows/build-stpyv8.yml | 184 ++++++++++++++++++ docs/packages/stpyv8.yaml | 5 + ...e-V8-checkout-work-on-a-riscv64-host.patch | 151 ++++++++++++++ ...ld-V8-with-the-riscv64-gcc-toolchain.patch | 54 +++++ 4 files changed, 394 insertions(+) create mode 100644 .github/workflows/build-stpyv8.yml create mode 100644 docs/packages/stpyv8.yaml create mode 100644 patches/stpyv8/13.1.201.22/0001-setup-make-the-V8-checkout-work-on-a-riscv64-host.patch create mode 100644 patches/stpyv8/13.1.201.22/0002-settings-build-V8-with-the-riscv64-gcc-toolchain.patch diff --git a/.github/workflows/build-stpyv8.yml b/.github/workflows/build-stpyv8.yml new file mode 100644 index 00000000000..1beade1d2a9 --- /dev/null +++ b/.github/workflows/build-stpyv8.yml @@ -0,0 +1,184 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on: https://github.com/cloudflare/stpyv8/blob/v13.1.201.22/.github/workflows/linux.yml +name: Build stpyv8 wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/stpyv8.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-stpyv8.yml' + - 'docs/packages/stpyv8.yaml' + - 'patches/stpyv8/**' + push: + branches: [main] + paths: + - '.github/workflows/build-stpyv8.yml' + - 'docs/packages/stpyv8.yaml' + - 'patches/stpyv8/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: stpyv8 + version: ${{ inputs.version }} + + build_v8: + name: Build v8 for stpyv8 ${{ matrix.version }} riscv64 + needs: [setup] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + env: + STPYV8_VERSION: ${{ matrix.version }} + V8_DEPS_LINUX: '0' + runs-on: ubuntu-24.04-riscv + timeout-minutes: 2880 # 48h: a monolithic v8 build from scratch, the scale build-nodejs-wheel-binaries.yml measured at ~23h + container: + image: quay.io/pypa/manylinux_2_39_riscv64 + + steps: + - name: Checkout stpyv8 v${{ env.STPYV8_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: cloudflare/stpyv8 + ref: v${{ env.STPYV8_VERSION }} + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Patch stpyv8 source + run: git apply python-wheels/patches/stpyv8/${{ env.STPYV8_VERSION }}/*.patch + + - name: Provide a riscv64-linux-gnu- prefixed toolchain + run: | + mkdir -p "${RUNNER_TEMP}/riscv64-toolchain/bin" + for tool in gcc g++ ar as ld nm objcopy objdump ranlib readelf strip; do + real=$(command -v "${tool}" || true) + [ -n "${real}" ] && ln -sf "${real}" "${RUNNER_TEMP}/riscv64-toolchain/bin/riscv64-linux-gnu-${tool}" + done + echo "${RUNNER_TEMP}/riscv64-toolchain/bin" >> "${GITHUB_PATH}" + + - name: Clone depot_tools + # setup.py bootstraps depot_tools' hermetic python only for a checkout that already exists, and `fetch` refuses to run without it. + run: | + git clone --depth 1 https://chromium.googlesource.com/chromium/tools/depot_tools.git depot_tools + depot_tools/gclient --version + + - name: Build v8 + run: | + /opt/python/cp312-cp312/bin/pip install setuptools wheel + /opt/python/cp312-cp312/bin/python setup.py v8 + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: stpyv8-${{ env.STPYV8_VERSION }}-v8-riscv64 + path: | + v8/include + v8/out.gn/x64.release.sample/obj/libv8_monolith.a + v8/out.gn/x64.release.sample/icudtl.dat + if-no-files-found: error + + build_wheel: + name: Build stpyv8 ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64 + needs: [setup, build_v8] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + # Boost.Python links the version-specific CPython ABI and has no free-threaded build. + python: ["cp312", "cp313", "cp314"] + env: + STPYV8_VERSION: ${{ matrix.version }} + # Newer than upstream's 1.87.0, which predates CPython 3.14. + BOOST_VERSION: 1.90.0 + PYBIN: /opt/python/${{ matrix.python }}-${{ matrix.python }}/bin + runs-on: ubuntu-24.04-riscv + timeout-minutes: 720 + container: + image: quay.io/pypa/manylinux_2_39_riscv64 + + steps: + - name: Checkout stpyv8 v${{ env.STPYV8_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: cloudflare/stpyv8 + ref: v${{ env.STPYV8_VERSION }} + persist-credentials: false + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: stpyv8-${{ env.STPYV8_VERSION }}-v8-riscv64 + path: v8 + + - name: Install Boost + run: | + archive="boost_$(echo "${BOOST_VERSION}" | tr . _)" + curl -fsSL -o boost.tar.gz "https://archives.boost.io/release/${BOOST_VERSION}/source/${archive}.tar.gz" + tar xf boost.tar.gz + cd "${archive}" + ./bootstrap.sh --with-python="${PYBIN}/python" + ./b2 install -j"$(nproc)" --with-system --with-python --with-filesystem --with-iostreams --with-date_time --with-thread + ldconfig /usr/local/lib + + - name: Build wheel + run: | + "${PYBIN}/pip" install setuptools wheel + "${PYBIN}/python" setup.py bdist_wheel --skip-build-v8 -d wheelhouse + + - name: Repair wheel + run: LD_LIBRARY_PATH=/usr/local/lib auditwheel repair --plat manylinux_2_39_riscv64 -w dist wheelhouse/*.whl + + - name: Test wheel + # Staged elsewhere: at the repository root the checkout's own STPyV8.py shadows the installed one. + run: | + "${PYBIN}/pip" install dist/*.whl pytest pytest-order + mkdir -p "${RUNNER_TEMP}/tests" + cp -r tests pytest.ini "${RUNNER_TEMP}/tests" + cd "${RUNNER_TEMP}/tests" + "${PYBIN}/python" -m pytest -v + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: stpyv8-${{ env.STPYV8_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: dist/*.whl + if-no-files-found: error + + publish: + name: Publish stpyv8 ${{ matrix.version }} + needs: [setup, build_wheel] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + secrets: + app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }} + with: + artifact-pattern: stpyv8-${{ matrix.version }}-*-manylinux_riscv64 diff --git a/docs/packages/stpyv8.yaml b/docs/packages/stpyv8.yaml new file mode 100644 index 00000000000..a3d0ad88bd8 --- /dev/null +++ b/docs/packages/stpyv8.yaml @@ -0,0 +1,5 @@ +package-name: stpyv8 +source-code: https://github.com/cloudflare/stpyv8 +license: Apache-2.0 +versions: +- version: 13.1.201.22 diff --git a/patches/stpyv8/13.1.201.22/0001-setup-make-the-V8-checkout-work-on-a-riscv64-host.patch b/patches/stpyv8/13.1.201.22/0001-setup-make-the-V8-checkout-work-on-a-riscv64-host.patch new file mode 100644 index 00000000000..d8f6b62e8a5 --- /dev/null +++ b/patches/stpyv8/13.1.201.22/0001-setup-make-the-V8-checkout-work-on-a-riscv64-host.patch @@ -0,0 +1,151 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sat, 19 Sep 2026 21:10:00 +0200 +Subject: [PATCH] setup: make the V8 checkout work on a riscv64 host + +checkout_v8() cannot complete on riscv64 for three independent reasons, +all of them in the gclient half of the recipe rather than in V8 itself: + +* Two of V8's CIPD packages have no linux-riscv64 build: buildtools/ + reclient (infra/rbe/client) and third_party/siso (infra/build/siso). + Their DEPS conditions exclude s390/ppc/zos and arm64-on-Linux but not + riscv64, so gclient sync dies on "no such package" -- first inside + `fetch`, then again in the explicit sync. Both are optional build + accelerators that plain gn + ninja never asks for, and gn + (gn/gn/linux-${arch}) and ninja (infra/3pp/tools/ninja/${platform}) + share the very same guard clause while *having* linux-riscv64 + packages, so this cannot be fixed by overriding gclient's host_cpu: + that would stop the two tools we do need from being fetched. Patch + the two conditions out of the checked-out DEPS instead, after the + release tag is checked out and before the sync that reads it. + +* That DEPS edit, and the V8_GIT_TAG checkout it follows, only survive + the sync if the v8 solution is unmanaged: depot_tools' fetch config + for V8 declares no "managed" key, so gclient defaults it to true and + re-resolves the solution to its remote default branch on every sync, + which either moves the checkout off the release tag or refuses to run + at all over a locally modified DEPS ("You have uncommitted changes"). + +* Parallel syncs lose a gsutil bootstrap lockfile race on riscv64 + ([Errno 11] Resource temporarily unavailable), the same class of + flakiness depot_tools already serializes 32-bit arm boards for. + +Upstream-Status: To upstream [not yet submitted to cloudflare/stpyv8] + +Signed-off-by: Ludovic Henry +--- + setup.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- + 1 file changed, 67 insertions(+), 2 deletions(-) + +diff --git a/setup.py b/setup.py +index fb80c38..0162b2c 100644 +--- a/setup.py ++++ b/setup.py +@@ -1,6 +1,7 @@ + #!/usr/bin/env python + + import logging ++import re + import shutil + import subprocess + +@@ -16,6 +17,8 @@ log = logging.getLogger() + ICU_DATA_PACKAGE_FOLDER = os.path.join(os.pardir, os.pardir, "stpyv8-icu") + ICU_DATA_V8_FILE_PATH = os.path.join("v8", "out.gn", "x64.release.sample", "icudtl.dat") + ++RISCV64 = platform.machine() in ("riscv64",) ++ + + def exec_cmd(cmdline, *args, **kwargs): + msg = kwargs.get("msg") +@@ -75,6 +78,58 @@ def install_depot(): + exit(1) + + ++def unmanage_v8_solution(): ++ # gclient only honors a solution's checked-out state (and any local edit to ++ # its DEPS, see skip_unavailable_cipd_packages below) when the solution is ++ # unmanaged; a managed one is re-resolved to its remote default branch on ++ # every sync, which either discards the V8_GIT_TAG checkout done above or ++ # refuses to run over the DEPS edit ("You have uncommitted changes"). ++ gclient_file = os.path.join(os.path.dirname(V8_HOME), ".gclient") ++ ++ with open(gclient_file, encoding="utf-8", mode="r") as fd: ++ text = fd.read() ++ ++ if "managed" in text: ++ return ++ ++ text = re.sub( ++ r"""["']custom_deps["']\s*:\s*\{\s*\}\s*,""", ++ '"custom_deps": {}, "managed": False,', ++ text, ++ count=1, ++ ) ++ ++ with open(gclient_file, encoding="utf-8", mode="w") as fd: ++ fd.write(text) ++ ++ ++def skip_unavailable_cipd_packages(): ++ # Two of V8's CIPD packages, buildtools/reclient (infra/rbe/client, the ++ # remote execution client) and third_party/siso (the siso build tool), are ++ # unconditioned on riscv64 -- their conditions exclude s390/ppc/zos and ++ # arm64-on-Linux -- but neither is published for linux-riscv64, so gclient ++ # sync fails on them. Both are optional build accelerators plain gn + ninja ++ # never asks for, and gn and ninja themselves share the same guard clause ++ # and *do* have linux-riscv64 packages, so the host_cpu they resolve to ++ # cannot simply be overridden. ++ deps_file = os.path.join(V8_HOME, "DEPS") ++ ++ with open(deps_file, encoding="utf-8", mode="r") as fd: ++ text = fd.read() ++ ++ for condition in ( ++ '(host_os == "linux" or host_os == "mac" or host_os == "win") and ' ++ 'host_cpu != "s390" and host_os != "zos" and host_cpu != "ppc" and ' ++ '(host_cpu != "arm64" or host_os == "mac")', ++ 'not build_with_chromium and host_cpu != "s390" and ' ++ 'host_os != "zos" and host_cpu != "ppc"', ++ ): ++ text = text.replace(condition, f'{condition} and host_cpu != "riscv64"', 1) ++ ++ with open(deps_file, encoding="utf-8", mode="w") as fd: ++ fd.write(text) ++ ++ + def checkout_v8(): + install_depot() + +@@ -87,7 +142,10 @@ def checkout_v8(): + msg="Fetching Google V8 code", + ) + +- if not success: ++ # fetch's own gclient sync fails on riscv64 over the CIPD packages ++ # skip_unavailable_cipd_packages() disables below; V8 itself is cloned ++ # by then, and the sync below completes what this one left undone. ++ if not success and not RISCV64: + exit(1) + + success, _, __ = exec_cmd( +@@ -106,11 +164,18 @@ def checkout_v8(): + if not success: + exit(1) + ++ if RISCV64: ++ unmanage_v8_solution() ++ skip_unavailable_cipd_packages() ++ + success, _, __ = exec_cmd( + os.path.join(DEPOT_HOME, "gclient"), + "sync", + "-D", +- "-j8", ++ # depot_tools serializes the sync on 32-bit arm boards because parallel ++ # syncs are flaky there; riscv64 boards lose the same gsutil bootstrap ++ # lockfile race ([Errno 11] Resource temporarily unavailable). ++ "-j1" if RISCV64 else "-j8", + cwd=os.path.dirname(V8_HOME), + msg="Syncing Google V8 code", + ) diff --git a/patches/stpyv8/13.1.201.22/0002-settings-build-V8-with-the-riscv64-gcc-toolchain.patch b/patches/stpyv8/13.1.201.22/0002-settings-build-V8-with-the-riscv64-gcc-toolchain.patch new file mode 100644 index 00000000000..6b28833474f --- /dev/null +++ b/patches/stpyv8/13.1.201.22/0002-settings-build-V8-with-the-riscv64-gcc-toolchain.patch @@ -0,0 +1,54 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sat, 19 Sep 2026 21:12:00 +0200 +Subject: [PATCH] settings: build V8 with the riscv64 gcc toolchain + +gn defaults is_clang to true, which points the build at Chromium's +prebuilt clang in third_party/llvm-build. That toolchain is published +for amd64/arm64/mac hosts only, and V8's DEPS conditions its Linux +object on host_os alone, so a riscv64 host happily downloads an x86_64 +clang it can never execute. + +V8 already carries the alternative: build/toolchain/linux/BUILD.gn +defines gcc_toolchain("riscv64") with toolprefix "riscv64-linux-gnu-" +(the arrangement the RISC-V V8 port documents at +https://github.com/riscv-collab/v8/wiki/Cross-compiled-Build), and +is_clang=false is what routes gn at it. The build host must provide +riscv64-linux-gnu-prefixed tools; on a native riscv64 builder those are +the system gcc/binutils. + +The other riscv64-relevant gn arguments are already set unconditionally +by this file: treat_warnings_as_errors=false (V8 gates -Werror on clang +everywhere but config("chromium_code"), and GCC emits diagnostics clang +does not) and use_custom_libcxx=false (V8's bundled libc++ needs GCC 15+ +to build with GCC). + +Upstream-Status: To upstream [not yet submitted to cloudflare/stpyv8] + +Signed-off-by: Ludovic Henry +--- + settings.py | 11 +++++++++++ + 1 file changed, 11 insertions(+) + +diff --git a/settings.py b/settings.py +index 76059a8..c0e908a 100644 +--- a/settings.py ++++ b/settings.py +@@ -34,6 +34,17 @@ gn_args = { + "v8_use_external_startup_data": "false", + } + ++if platform.machine() in ("riscv64",): ++ # Chromium's prebuilt clang (third_party/llvm-build, fetched by gclient from ++ # the chromium-browser-clang bucket) is only published for amd64/arm64/mac ++ # hosts, and its Linux object is unconditioned on host_cpu, so a riscv64 ++ # host downloads an x86_64 binary it can never run. V8's own ++ # build/toolchain/linux/BUILD.gn already defines a gcc_toolchain("riscv64") ++ # (toolprefix riscv64-linux-gnu-, see ++ # https://github.com/riscv-collab/v8/wiki/Cross-compiled-Build); is_clang=false ++ # routes gn at it. ++ gn_args["is_clang"] = "false" ++ + source_files = [ + "Exception.cpp", + "Platform.cpp", From fe7a49790815338ec4212b75cda346b499a09813 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 19 Sep 2026 23:54:06 +0000 Subject: [PATCH 2/6] stpyv8: get the V8 checkout past gsutil and luci-go on riscv64 The first build_v8 run died 25 minutes in, in `gclient sync`, on the gcs dep v8/third_party/llvm-build/Release+Asserts: Because crcmod==1.7+chromium.4 has no wheels with a matching platform tag (e.g., `manylinux_2_39_riscv64`) [...] your requirements are unsatisfiable. Every gcs dep, and every hook that calls download_from_google_storage, runs gsutil under vpython3, whose spec (depot_tools/gsutil.vpython.toml) pins crcmod==1.7+chromium.4, and chromium's wheel mirror builds that wheel for x86_64/aarch64/arm/mac/windows only. The rest of depot_tools is fine here -- the cipd client, the hermetic cpython3 and vpython3 itself all have linux-riscv64 packages -- so skip the venv, not the tool: VPYTHON_BYPASS is depot_tools' own escape hatch for a manually managed python, and makes vpython3 exec the container's python3, where gsutil treats crcmod as an optional accelerator. It also turns the DEPS vpython3_common hook, which would resolve the same riscv64-less wheel set, into a no-op. Behind that sat a second wall of the kind patch 0001 already handles: tools/luci-go's packages (infra/tools/luci/{isolate,swarming}) have no linux-riscv64 build either, while gn and ninja, whose conditions are the same shape, do. Add its condition to the ones the patch rewrites. Invoke gclient through an absolute path as well: depot_tools' cipd bootstrap derives its own directory from $0, so through a relative path it looks for cipd_client_version.digests in the wrong place, which is what printed the misleading "Platform linux-riscv64 is not supported by the CIPD client bootstrap" in the same log. Rehearsed under qemu-riscv64 in quay.io/pypa/manylinux_2_39_riscv64 (python3.12, no crcmod): gclient's own gcs code path, download_from_google_storage.Gsutil.check_call("cp", ...) on the object that failed in CI, returns 0 with VPYTHON_BYPASS set and reproduces the resolver error without it. --- .github/workflows/build-stpyv8.yml | 5 +- ...e-V8-checkout-work-on-a-riscv64-host.patch | 54 ++++++++++--------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build-stpyv8.yml b/.github/workflows/build-stpyv8.yml index 1beade1d2a9..de5811d2035 100644 --- a/.github/workflows/build-stpyv8.yml +++ b/.github/workflows/build-stpyv8.yml @@ -49,6 +49,8 @@ jobs: env: STPYV8_VERSION: ${{ matrix.version }} V8_DEPS_LINUX: '0' + # depot_tools downloads every gclient gcs dep through gsutil under vpython, whose venv pins a crcmod wheel chromium does not build for riscv64; the bypass runs those scripts on the container python instead. + VPYTHON_BYPASS: manually managed python not supported by chrome operations runs-on: ubuntu-24.04-riscv timeout-minutes: 2880 # 48h: a monolithic v8 build from scratch, the scale build-nodejs-wheel-binaries.yml measured at ~23h container: @@ -84,7 +86,8 @@ jobs: # setup.py bootstraps depot_tools' hermetic python only for a checkout that already exists, and `fetch` refuses to run without it. run: | git clone --depth 1 https://chromium.googlesource.com/chromium/tools/depot_tools.git depot_tools - depot_tools/gclient --version + # An absolute path: depot_tools' cipd bootstrap resolves its own directory from $0 and cannot find its digests file when invoked through a relative one. + "${PWD}/depot_tools/gclient" --version - name: Build v8 run: | diff --git a/patches/stpyv8/13.1.201.22/0001-setup-make-the-V8-checkout-work-on-a-riscv64-host.patch b/patches/stpyv8/13.1.201.22/0001-setup-make-the-V8-checkout-work-on-a-riscv64-host.patch index d8f6b62e8a5..69b7d55f587 100644 --- a/patches/stpyv8/13.1.201.22/0001-setup-make-the-V8-checkout-work-on-a-riscv64-host.patch +++ b/patches/stpyv8/13.1.201.22/0001-setup-make-the-V8-checkout-work-on-a-riscv64-host.patch @@ -6,18 +6,20 @@ Subject: [PATCH] setup: make the V8 checkout work on a riscv64 host checkout_v8() cannot complete on riscv64 for three independent reasons, all of them in the gclient half of the recipe rather than in V8 itself: -* Two of V8's CIPD packages have no linux-riscv64 build: buildtools/ - reclient (infra/rbe/client) and third_party/siso (infra/build/siso). - Their DEPS conditions exclude s390/ppc/zos and arm64-on-Linux but not - riscv64, so gclient sync dies on "no such package" -- first inside - `fetch`, then again in the explicit sync. Both are optional build - accelerators that plain gn + ninja never asks for, and gn +* Three of V8's CIPD packages have no linux-riscv64 build: buildtools/ + reclient (infra/rbe/client), third_party/siso (infra/build/siso) and + tools/luci-go (infra/tools/luci/{isolate,swarming}). Their DEPS + conditions exclude s390/ppc/zos and arm64-on-Linux but not riscv64, so + gclient sync dies on "no such package" -- first inside `fetch`, then + again in the explicit sync. None of the three is ever asked for by a + plain gn + ninja build -- two are remote-execution accelerators and the + third only ships tests to a swarming pool -- while gn (gn/gn/linux-${arch}) and ninja (infra/3pp/tools/ninja/${platform}) - share the very same guard clause while *having* linux-riscv64 - packages, so this cannot be fixed by overriding gclient's host_cpu: - that would stop the two tools we do need from being fetched. Patch - the two conditions out of the checked-out DEPS instead, after the - release tag is checked out and before the sync that reads it. + share the very same guard clauses *and* have linux-riscv64 packages, so + this cannot be fixed by overriding gclient's host_cpu: that would stop + the two tools we do need from being fetched. Patch the three conditions + out of the checked-out DEPS instead, after the release tag is checked + out and before the sync that reads it. * That DEPS edit, and the V8_GIT_TAG checkout it follows, only survive the sync if the v8 solution is unmanaged: depot_tools' fetch config @@ -34,11 +36,11 @@ Upstream-Status: To upstream [not yet submitted to cloudflare/stpyv8] Signed-off-by: Ludovic Henry --- - setup.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- - 1 file changed, 67 insertions(+), 2 deletions(-) + setup.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- + 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py -index fb80c38..0162b2c 100644 +index fb80c38..94509b4 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ @@ -58,7 +60,7 @@ index fb80c38..0162b2c 100644 def exec_cmd(cmdline, *args, **kwargs): msg = kwargs.get("msg") -@@ -75,6 +78,58 @@ def install_depot(): +@@ -75,6 +78,60 @@ def install_depot(): exit(1) @@ -88,14 +90,15 @@ index fb80c38..0162b2c 100644 + + +def skip_unavailable_cipd_packages(): -+ # Two of V8's CIPD packages, buildtools/reclient (infra/rbe/client, the -+ # remote execution client) and third_party/siso (the siso build tool), are -+ # unconditioned on riscv64 -- their conditions exclude s390/ppc/zos and -+ # arm64-on-Linux -- but neither is published for linux-riscv64, so gclient -+ # sync fails on them. Both are optional build accelerators plain gn + ninja -+ # never asks for, and gn and ninja themselves share the same guard clause -+ # and *do* have linux-riscv64 packages, so the host_cpu they resolve to -+ # cannot simply be overridden. ++ # Three of V8's CIPD deps are unconditioned on riscv64 -- their conditions ++ # exclude s390/ppc/zos and arm64-on-Linux -- but none of the three is ++ # published for linux-riscv64, so gclient sync fails on them: ++ # buildtools/reclient (infra/rbe/client, the remote execution client), ++ # third_party/siso (the siso build tool) and tools/luci-go (the isolate and ++ # swarming clients, which only ship tests to a swarming pool). None of them ++ # is asked for by a plain gn + ninja build, and gn and ninja themselves ++ # share the same guard clauses while *having* linux-riscv64 packages, so the ++ # host_cpu they resolve to cannot simply be overridden. + deps_file = os.path.join(V8_HOME, "DEPS") + + with open(deps_file, encoding="utf-8", mode="r") as fd: @@ -107,6 +110,7 @@ index fb80c38..0162b2c 100644 + '(host_cpu != "arm64" or host_os == "mac")', + 'not build_with_chromium and host_cpu != "s390" and ' + 'host_os != "zos" and host_cpu != "ppc"', ++ 'host_cpu != "s390" and host_os != "zos" and host_os != "aix"', + ): + text = text.replace(condition, f'{condition} and host_cpu != "riscv64"', 1) + @@ -117,7 +121,7 @@ index fb80c38..0162b2c 100644 def checkout_v8(): install_depot() -@@ -87,7 +142,10 @@ def checkout_v8(): +@@ -87,7 +144,10 @@ def checkout_v8(): msg="Fetching Google V8 code", ) @@ -129,7 +133,7 @@ index fb80c38..0162b2c 100644 exit(1) success, _, __ = exec_cmd( -@@ -106,11 +164,18 @@ def checkout_v8(): +@@ -106,11 +166,18 @@ def checkout_v8(): if not success: exit(1) From 5c64affc0106c3fbd6c9f8e5466e47501da5bda8 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sun, 20 Sep 2026 00:52:55 +0000 Subject: [PATCH 3/6] stpyv8: skip V8's gsutil test-data hooks on riscv64 gclient sync now reaches V8's DEPS hooks and dies in the first one that shells out to download_from_google_storage.py: the gsutil that V8's own DEPS-pinned depot_tools bootstraps is 4.68, which vendors six 1.12, whose meta path importer only implements find_module() -- the API CPython removed in 3.12. Its gsutil.vpython3 spec asks for python 3.8, so on a bot it never meets a 3.12 interpreter; the VPYTHON_BYPASS that riscv64 needs to run gsutil at all hands it the container python 3.12 instead, and the hook fails with "No module named 'six.moves'". Only wasm_spec_tests and wasm_js are reached with V8's default variables, and both only unpack test suites that building v8_monolith never reads, so condition them off on riscv64 with the DEPS edit that already drops the riscv64-less CIPD packages. Evaluating the patched DEPS through gclient_eval leaves the two hooks off for host_cpu riscv64 and every other hook, and all of x64, untouched. --- ...e-V8-checkout-work-on-a-riscv64-host.patch | 62 ++++++++++++++++--- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/patches/stpyv8/13.1.201.22/0001-setup-make-the-V8-checkout-work-on-a-riscv64-host.patch b/patches/stpyv8/13.1.201.22/0001-setup-make-the-V8-checkout-work-on-a-riscv64-host.patch index 69b7d55f587..4b9842cf1b3 100644 --- a/patches/stpyv8/13.1.201.22/0001-setup-make-the-V8-checkout-work-on-a-riscv64-host.patch +++ b/patches/stpyv8/13.1.201.22/0001-setup-make-the-V8-checkout-work-on-a-riscv64-host.patch @@ -3,7 +3,7 @@ From: Ludovic Henry Date: Sat, 19 Sep 2026 21:10:00 +0200 Subject: [PATCH] setup: make the V8 checkout work on a riscv64 host -checkout_v8() cannot complete on riscv64 for three independent reasons, +checkout_v8() cannot complete on riscv64 for four independent reasons, all of them in the gclient half of the recipe rather than in V8 itself: * Three of V8's CIPD packages have no linux-riscv64 build: buildtools/ @@ -21,6 +21,23 @@ all of them in the gclient half of the recipe rather than in V8 itself: out of the checked-out DEPS instead, after the release tag is checked out and before the sync that reads it. +* Two DEPS hooks download prebuilt test suites through the gsutil that + V8's own DEPS-pinned depot_tools bootstraps, which is gsutil 4.68, and + that gsutil vendors six 1.12 -- whose meta path importer only + implements the find_module() API CPython removed in 3.12. Its + gsutil.vpython3 spec asks for python 3.8, so it never meets a 3.12 + interpreter on a bot; but riscv64 needs VPYTHON_BYPASS to run gsutil at + all (the vpython venv pins a crcmod wheel chromium builds for no + riscv64 platform tag), and the bypass hands gsutil the container + python, where every download_from_google_storage.py hook then dies on + "ModuleNotFoundError: No module named 'six.moves'". Only wasm_spec_tests + and wasm_js are reached with V8's default variables, and both only + unpack test suites that building v8_monolith never reads, so condition + them off rather than reshuffle which interpreter gsutil gets. gclient's + own dep_type: 'gcs' deps are not affected: they go through the gsutil + of the depot_tools checkout driving the sync, new enough to vendor a + six that still imports. + * That DEPS edit, and the V8_GIT_TAG checkout it follows, only survive the sync if the v8 solution is unmanaged: depot_tools' fetch config for V8 declares no "managed" key, so gclient defaults it to true and @@ -36,11 +53,11 @@ Upstream-Status: To upstream [not yet submitted to cloudflare/stpyv8] Signed-off-by: Ludovic Henry --- - setup.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- - 1 file changed, 69 insertions(+), 2 deletions(-) + setup.py | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- + 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py -index fb80c38..94509b4 100644 +index fb80c38..e887710 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ @@ -60,7 +77,7 @@ index fb80c38..94509b4 100644 def exec_cmd(cmdline, *args, **kwargs): msg = kwargs.get("msg") -@@ -75,6 +78,60 @@ def install_depot(): +@@ -75,6 +78,90 @@ def install_depot(): exit(1) @@ -117,11 +134,41 @@ index fb80c38..94509b4 100644 + with open(deps_file, encoding="utf-8", mode="w") as fd: + fd.write(text) + ++ ++def skip_gsutil_hooks(): ++ # The gsutil that V8's DEPS-pinned depot_tools bootstraps is 4.68, and it ++ # vendors six 1.12, whose meta path importer only implements find_module(), ++ # which CPython removed in 3.12. Its own gsutil.vpython3 spec asks for ++ # python 3.8, so on a bot it never meets a 3.12 interpreter -- but the ++ # VPYTHON_BYPASS that gets gsutil running on riscv64 at all (its vpython ++ # venv pins a crcmod wheel chromium does not build for this platform) makes ++ # vpython3 run it on the container python, and every hook that shells out to ++ # download_from_google_storage.py then dies on "No module named ++ # 'six.moves'". Of those hooks only wasm_spec_tests and wasm_js are reached ++ # with V8's default variables, and both only unpack test suites that ++ # building v8_monolith never reads, so skip them rather than second-guess ++ # which interpreter gsutil gets. (gclient's own dep_type: 'gcs' downloads ++ # are unaffected: those run through the newer gsutil of the depot_tools ++ # checkout that drives the sync, which vendors a six that still imports.) ++ deps_file = os.path.join(V8_HOME, "DEPS") ++ ++ with open(deps_file, encoding="utf-8", mode="r") as fd: ++ text = fd.read() ++ ++ for hook in ("wasm_spec_tests", "wasm_js"): ++ anchor = f"'name': '{hook}',\n 'pattern': '.',\n" ++ text = text.replace( ++ anchor, f"{anchor} 'condition': 'host_cpu != \"riscv64\"',\n", 1 ++ ) ++ ++ with open(deps_file, encoding="utf-8", mode="w") as fd: ++ fd.write(text) ++ + def checkout_v8(): install_depot() -@@ -87,7 +144,10 @@ def checkout_v8(): +@@ -87,7 +174,10 @@ def checkout_v8(): msg="Fetching Google V8 code", ) @@ -133,13 +180,14 @@ index fb80c38..94509b4 100644 exit(1) success, _, __ = exec_cmd( -@@ -106,11 +166,18 @@ def checkout_v8(): +@@ -106,11 +196,19 @@ def checkout_v8(): if not success: exit(1) + if RISCV64: + unmanage_v8_solution() + skip_unavailable_cipd_packages() ++ skip_gsutil_hooks() + success, _, __ = exec_cmd( os.path.join(DEPOT_HOME, "gclient"), From f988dd4ad953ee1cda5465dade889d712109bd2a Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sun, 20 Sep 2026 11:55:44 +0000 Subject: [PATCH 4/6] stpyv8: unnest the V8 riscv64 stack helpers GCC rejects The first run to get past gclient sync spent 9h17m in ninja and reached 1146 of 2117 targets -- as far as baseline.o, the first translation unit that pulls in src/codegen/macro-assembler.h -- before GCC refused src/codegen/riscv/macro-assembler-riscv.h:389 and :537, "explicit specialization in non-namespace scope 'class v8::internal::MacroAssembler'". Not a wall of any kind: no OOM, no ENOSPC, no ICE, no signal in the whole log, exactly one FAILED edge, and the 48h timeout was under a fifth spent. MacroAssembler::push_helper and ::pop_helper end a variadic recursion over a register pack with a one-register explicit specialization written inside the class body, which only clang accepts; V8 builds its RISC-V port with nothing else, while settings.py has to choose GCC because no prebuilt Chromium clang runs on a riscv64 host. V8 itself replaced both with an empty zero-register overload in 13.8 -- the variadic overload has already stored the last register at sp+0 by the time the pack empties -- so backport that rather than invent a fix. Rewriting the pinned 13.1.201.22 header with this hunk reproduces 13.8's text byte for byte, and the two forms were compiled both ways to confirm GCC rejects only the old one and clang accepts both. It is the only blocker of its kind left in this V8: the riscv64 sources carry no other in-class specialization, and the two further GCC stoppers the mini-racer V8 build hit postdate 13.1.201.22 (unicode.h has no WriteLeadingAscii here, and highway is not in this BUILD.gn at all). The warning class is already disarmed -- settings.py declares treat_warnings_as_errors=false and no -Werror appears anywhere in the log. Still, pass ninja -k 1000 so the next run reports every remaining GCC-only failure at once instead of one per ten hours, and drop it once the build is green. --- ...V8-riscv64-stack-helpers-GCC-rejects.patch | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 patches/stpyv8/13.1.201.22/0003-setup-fix-the-V8-riscv64-stack-helpers-GCC-rejects.patch diff --git a/patches/stpyv8/13.1.201.22/0003-setup-fix-the-V8-riscv64-stack-helpers-GCC-rejects.patch b/patches/stpyv8/13.1.201.22/0003-setup-fix-the-V8-riscv64-stack-helpers-GCC-rejects.patch new file mode 100644 index 00000000000..73738c79ade --- /dev/null +++ b/patches/stpyv8/13.1.201.22/0003-setup-fix-the-V8-riscv64-stack-helpers-GCC-rejects.patch @@ -0,0 +1,123 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 20 Sep 2026 12:30:00 +0200 +Subject: [PATCH] setup: fix the V8 riscv64 stack helpers GCC rejects + +The first run to get past gclient sync and into the build spent 9h17m in +ninja, reached 1146 of 2117 targets -- as far as the first translation +unit that pulls in src/codegen/macro-assembler.h -- and stopped there: + + FAILED: obj/v8_base_without_compiler/baseline.o + ../../src/codegen/riscv/macro-assembler-riscv.h:389:13: error: explicit + specialization in non-namespace scope 'class v8::internal::MacroAssembler' + ../../src/codegen/riscv/macro-assembler-riscv.h:537:13: error: explicit + specialization in non-namespace scope 'class v8::internal::MacroAssembler' + +MacroAssembler::push_helper and ::pop_helper walk a register pack by +recursion and end it with a one-register explicit specialization declared +inside the class body. An explicit specialization has to be declared at +namespace scope; clang -- the only compiler V8 builds this port with -- +takes the in-class one as an extension, GCC rejects it, and settings.py +has to choose GCC here because no prebuilt Chromium clang runs on a +riscv64 host. Nothing riscv64-specific is wrong with the code: it is +simply the first time this header meets a compiler other than clang. + +V8 fixed it in 13.8 by replacing both specializations with an empty, +zero-register overload: once the pack is empty the variadic overload has +already stored the last register at sp+0, so the base case has nothing +left to do and the generated code is identical. Backport exactly that -- +rewriting the pinned 13.1.201.22 header with the hunk below reproduces +13.8's own text byte for byte, and both forms were compiled either way to +confirm which compiler accepts which. + +Refuse to build rather than rewrite nothing if a future V8 no longer +matches: silently skipping costs another ten hours to find out. + +Also pass ninja -k 1000 on riscv64. V8 is warning-clean under clang only, +which is why settings.py already declares treat_warnings_as_errors=false, +but a hard error like this one is still found one translation unit per +run, and this run costs ten hours before it even reaches this file. Let +one build collect as many of them as it can instead; drop it once the +build is green. + +https://github.com/riseproject-dev/python-wheels/actions/runs/35479941332/job/105995838025 + +Upstream-Status: To upstream [not yet submitted to cloudflare/stpyv8] + +Signed-off-by: Ludovic Henry +--- + setup.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- + 1 file changed, 50 insertions(+), 1 deletion(-) + +diff --git a/setup.py b/setup.py +index e887710..9586b8d 100644 +--- a/setup.py ++++ b/setup.py +@@ -236,7 +236,50 @@ def checkout_v8(): + exit(1) + + ++def unnest_riscv_stack_helper_specializations(): ++ # V8 compiles its RISC-V port with clang only, and clang takes an explicit ++ # specialization declared inside a class body as an extension. GCC, which ++ # settings.py routes this build at (is_clang=false, no prebuilt clang runs ++ # on riscv64), rejects it the way the standard asks -- an explicit ++ # specialization has to be declared at namespace scope: ++ # ++ # src/codegen/riscv/macro-assembler-riscv.h:389:13: error: explicit ++ # specialization in non-namespace scope 'class v8::internal::MacroAssembler' ++ # ++ # Both offenders are the one-register base case of a variadic recursion ++ # that pushes or pops a register list. V8 replaced them with an empty, ++ # zero-register overload in 13.8; the variadic overload already writes the ++ # last register at sp+0 once the pack is empty, so the emitted code does ++ # not change. Backport that shape. ++ header = os.path.join( ++ V8_HOME, "src", "codegen", "riscv", "macro-assembler-riscv.h" ++ ) ++ ++ with open(header, encoding="utf-8", mode="r") as fd: ++ text = fd.read() ++ ++ for helper, access in (("push", "StoreWord"), ("pop", "LoadWord")): ++ specialization = ( ++ " template <>\n" ++ f" void {helper}_helper(Register r) {{\n" ++ f" {access}(r, MemOperand(sp, 0));\n" ++ " }\n" ++ ) ++ ++ if specialization not in text: ++ log.error("No %s_helper specialization to unnest in %s", helper, header) ++ exit(1) ++ ++ text = text.replace(specialization, f" void {helper}_helper() {{}}\n", 1) ++ ++ with open(header, encoding="utf-8", mode="w") as fd: ++ fd.write(text) ++ ++ + def build_v8(): ++ if RISCV64: ++ unnest_riscv_stack_helper_specializations() ++ + args = f"gen {os.path.join('out.gn', 'x64.release.sample')} --args=\"{GN_ARGS}\"" + success, _, __ = exec_cmd( + os.path.join(DEPOT_HOME, "gn"), +@@ -248,9 +291,15 @@ def build_v8(): + if not success: + exit(1) + ++ # V8's own CI never compiles this tree with GCC, so a first riscv64 build ++ # meets GCC-only diagnostics one translation unit at a time -- and a single ++ # one of them costs the ten hours it takes to reach it. Collect them all in ++ # one run instead; drop this once the build is green. ++ keep_going = "-k 1000 " if RISCV64 else "" ++ + success, _, __ = exec_cmd( + os.path.join(DEPOT_HOME, "ninja"), +- f"-C {os.path.join('out.gn', 'x64.release.sample')} v8_monolith", ++ f"-C {os.path.join('out.gn', 'x64.release.sample')} {keep_going}v8_monolith", + cwd=V8_HOME, + msg="Build V8 with ninja", + ) From a894d4b7513c07d628e9948047304f3f3f50ac74 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Mon, 21 Sep 2026 13:48:44 +0000 Subject: [PATCH 5/6] stpyv8: drop the boost_system link Boost 1.89 removed The first run whose build_v8 job went green failed all three build_wheel jobs at the same final link: /usr/bin/ld: cannot find -lboost_system: No such file or directory -lv8_monolith and -lboost_python312 both resolved, so the V8 artifact and the Boost.Python half are wired up correctly; boost_system is the one library that no longer exists. Boost 1.89 removed the Boost.System stub compiled library (header-only since 1.69), and this port builds against 1.90.0 because upstream's 1.87.0 predates CPython 3.14 -- so settings.py's hardcoded "boost_system" entry has nothing to resolve against. Patch 0004 removes it; the extension references no Boost.System symbol, and boostorg/system has carried no build/ or src/ since boost-1.89.0. build_wheel had no patch step at all, so add the python-wheels checkout and `git apply` that build_v8 already does. Patches 0001-0003 only affect the V8 checkout/build, which --skip-build-v8 never reaches, so applying the whole set there is a no-op beyond this fix. Also restore the actions/cache around the V8 build that upstream's linux.yml has and this port dropped, over exactly the paths the artifact already collects. The ~23h monolith build is otherwise repeated in full for every wheel-side iteration. --- .github/workflows/build-stpyv8.yml | 23 +++++++++ ...-boost_system-link-Boost-1.89-remove.patch | 51 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 patches/stpyv8/13.1.201.22/0004-settings-drop-the-boost_system-link-Boost-1.89-remove.patch diff --git a/.github/workflows/build-stpyv8.yml b/.github/workflows/build-stpyv8.yml index de5811d2035..cdcd84189b6 100644 --- a/.github/workflows/build-stpyv8.yml +++ b/.github/workflows/build-stpyv8.yml @@ -73,7 +73,19 @@ jobs: - name: Patch stpyv8 source run: git apply python-wheels/patches/stpyv8/${{ env.STPYV8_VERSION }}/*.patch + - name: Restore v8 + id: v8-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + v8/include + v8/out.gn/x64.release.sample/obj/libv8_monolith.a + v8/out.gn/x64.release.sample/icudtl.dat + # Upstream keys this on hashFiles('v8/src/**'); that tree only exists once the build has run, so key on what pins it instead -- the version and the patches. + key: stpyv8-v8-${{ env.STPYV8_VERSION }}-riscv64-${{ hashFiles('python-wheels/patches/stpyv8/**') }} + - name: Provide a riscv64-linux-gnu- prefixed toolchain + if: steps.v8-cache.outputs.cache-hit != 'true' run: | mkdir -p "${RUNNER_TEMP}/riscv64-toolchain/bin" for tool in gcc g++ ar as ld nm objcopy objdump ranlib readelf strip; do @@ -83,6 +95,7 @@ jobs: echo "${RUNNER_TEMP}/riscv64-toolchain/bin" >> "${GITHUB_PATH}" - name: Clone depot_tools + if: steps.v8-cache.outputs.cache-hit != 'true' # setup.py bootstraps depot_tools' hermetic python only for a checkout that already exists, and `fetch` refuses to run without it. run: | git clone --depth 1 https://chromium.googlesource.com/chromium/tools/depot_tools.git depot_tools @@ -90,6 +103,7 @@ jobs: "${PWD}/depot_tools/gclient" --version - name: Build v8 + if: steps.v8-cache.outputs.cache-hit != 'true' run: | /opt/python/cp312-cp312/bin/pip install setuptools wheel /opt/python/cp312-cp312/bin/python setup.py v8 @@ -131,6 +145,15 @@ jobs: ref: v${{ env.STPYV8_VERSION }} persist-credentials: false + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Patch stpyv8 source + run: git apply python-wheels/patches/stpyv8/${{ env.STPYV8_VERSION }}/*.patch + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: stpyv8-${{ env.STPYV8_VERSION }}-v8-riscv64 diff --git a/patches/stpyv8/13.1.201.22/0004-settings-drop-the-boost_system-link-Boost-1.89-remove.patch b/patches/stpyv8/13.1.201.22/0004-settings-drop-the-boost_system-link-Boost-1.89-remove.patch new file mode 100644 index 00000000000..8126f7f3bd6 --- /dev/null +++ b/patches/stpyv8/13.1.201.22/0004-settings-drop-the-boost_system-link-Boost-1.89-remove.patch @@ -0,0 +1,51 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Mon, 21 Sep 2026 14:10:00 +0200 +Subject: [PATCH] settings: drop the boost_system link Boost 1.89 removed + +The first run whose V8 build went green then failed every build_wheel +job, all three at the same final link: + + g++ ... -lboost_system -lboost_iostreams -lboost_filesystem \ + -lv8_monolith -lboost_python312 -lrt -o _STPyV8...so + /usr/bin/ld: cannot find -lboost_system: No such file or directory + +Nothing else was missing: -lv8_monolith and -lboost_python312 both +resolved, so the V8 half and the Boost.Python half are fine and this is +the one library that no longer exists. + +Boost.System has been header-only since Boost 1.69; what remained was a +stub compiled library kept for compatibility, and Boost 1.89 removed it +("System: The stub compiled library has been removed; System has been +header-only since release 1.69 ... The easiest fix is to just remove +system from the list of components as it's no longer required", +https://www.boost.org/users/history/version_1_89_0.html). boostorg/system +has carried neither build/Jamfile.v2 nor src/ since the boost-1.89.0 tag, +so `b2 --with-system` builds nothing and no libboost_system is installed. +Upstream's linux.yml pins Boost 1.87.0, where the stub still exists, +which is why upstream has not met this; this port builds against 1.90.0 +because 1.87.0 predates CPython 3.14. + +Removing the entry is the whole fix: the extension references no symbol +from the stub, boost::system::error_code and the two categories being +inline in the headers. + +Upstream-Status: To upstream [not yet submitted to cloudflare/stpyv8] + +Signed-off-by: Ludovic Henry +--- + settings.py | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/settings.py b/settings.py +index c0e908a..6a9c0f4 100644 +--- a/settings.py ++++ b/settings.py +@@ -138,7 +138,6 @@ if os.name in ("nt",): + + elif os.name in ("posix",): + libraries = [ +- "boost_system", + "boost_iostreams", + "boost_filesystem", + "v8_monolith", From b6f5f4c2ee98a5ae870b1babcfa5e08f53095bb4 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Thu, 24 Sep 2026 03:45:26 +0000 Subject: [PATCH 6/6] stpyv8: patch testReferenceCount for CPython 3.14's borrowed-ref calling convention Only the cp314 leg failed, always on the same assertion: self.assertEqual(2, sys.getrefcount(obj)) AssertionError: 2 != 1 CPython 3.14 added a LOAD_FAST_BORROW opcode: when the compiler can prove a local's frame slot still owns the reference after a call returns (true here, since `obj` is read again by `fn(obj)` and then by the second getrefcount call), it loads that local for the call without an incref. Every baseline this test hardcodes is one lower on 3.14+ as a result -- a documented, interpreter-wide behavior change also hit by other projects' refcount-based tests (root-project/root#18988, pandas-dev/pandas#61368), not anything specific to stpyv8 or riscv64. 0005 branches both baselines on sys.version_info; the +2 delta the JSContext wrapper itself contributes is unrelated to the calling convention and left alone. Also tighten the build_v8 cache key: it was hashing every patch under patches/stpyv8/**, so this test-only patch would have forced the same ~24h V8 rebuild that adding the boost_system patch already did once. Only 0001-0003 touch checkout_v8()/build_v8() (DEPS, GN_ARGS, the V8 macroassembler source); 0004 and this one are read only by build_wheel and can't affect what that cache holds. --- .github/workflows/build-stpyv8.yml | 4 +- ...Count-for-CPython-3.14-borrowed-refs.patch | 67 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 patches/stpyv8/13.1.201.22/0005-tests-adjust-testReferenceCount-for-CPython-3.14-borrowed-refs.patch diff --git a/.github/workflows/build-stpyv8.yml b/.github/workflows/build-stpyv8.yml index cdcd84189b6..90936243a60 100644 --- a/.github/workflows/build-stpyv8.yml +++ b/.github/workflows/build-stpyv8.yml @@ -81,8 +81,8 @@ jobs: v8/include v8/out.gn/x64.release.sample/obj/libv8_monolith.a v8/out.gn/x64.release.sample/icudtl.dat - # Upstream keys this on hashFiles('v8/src/**'); that tree only exists once the build has run, so key on what pins it instead -- the version and the patches. - key: stpyv8-v8-${{ env.STPYV8_VERSION }}-riscv64-${{ hashFiles('python-wheels/patches/stpyv8/**') }} + # Upstream keys this on hashFiles('v8/src/**'); that tree only exists once the build has run, so key on what pins it instead -- the version and the patches. Only 0001-0003 touch checkout_v8()/build_v8() (DEPS, GN_ARGS, the V8 macroassembler source); later patches touch settings.py's link line or the test suite, read only by build_wheel, so hashing them here would bust this ~24h build for changes that can't affect it. + key: stpyv8-v8-${{ env.STPYV8_VERSION }}-riscv64-${{ hashFiles('python-wheels/patches/stpyv8/*/0001-*.patch', 'python-wheels/patches/stpyv8/*/0002-*.patch', 'python-wheels/patches/stpyv8/*/0003-*.patch') }} - name: Provide a riscv64-linux-gnu- prefixed toolchain if: steps.v8-cache.outputs.cache-hit != 'true' diff --git a/patches/stpyv8/13.1.201.22/0005-tests-adjust-testReferenceCount-for-CPython-3.14-borrowed-refs.patch b/patches/stpyv8/13.1.201.22/0005-tests-adjust-testReferenceCount-for-CPython-3.14-borrowed-refs.patch new file mode 100644 index 00000000000..58417b92f90 --- /dev/null +++ b/patches/stpyv8/13.1.201.22/0005-tests-adjust-testReferenceCount-for-CPython-3.14-borrowed-refs.patch @@ -0,0 +1,67 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Thu, 24 Sep 2026 00:00:00 +0200 +Subject: [PATCH] tests: adjust testReferenceCount for CPython 3.14's + borrowed-ref calling convention + +cp312/cp313 pass this test; only cp314 fails, always on the first +assertion: + + self.assertEqual(2, sys.getrefcount(obj)) + AssertionError: 2 != 1 + +CPython 3.14 added a `LOAD_FAST_BORROW` opcode: whenever the compiler +can prove a local variable's frame slot still owns the reference after +a call returns, it loads that local as a call argument without an +incref, instead of the owning load every earlier version always used. +`obj` is read again after each `sys.getrefcount(obj)` call in this +test (by `fn(obj)`, and then by the second `sys.getrefcount(obj)`), +so the compiler can prove it in both places, and both baselines this +test hardcodes are one lower on 3.14+ as a result. This is a documented +interpreter-wide behavior change, not anything version-specific to +stpyv8 or to riscv64: cpython/Doc/whatsnew/3.14.rst's "Optimizations" +section covers it, and the same "AssertionError: N != N-1" pattern hit +other projects' refcount-based tests on 3.14 (e.g. +root-project/root#18988, pandas-dev/pandas#61368). + +The `+ 2` delta the JSContext wrapper itself contributes when `obj` is +passed into the JS call is unrelated to the calling convention and +unaffected by it, so only the baseline needs a version branch. + +Upstream-Status: To upstream [cloudflare/stpyv8 was archived read-only on 2026-06-18, so there is no repository left to open a PR or issue against] + +Signed-off-by: Ludovic Henry +--- + tests/test_Wrapper.py | 16 +++++++++++++--- + 1 file changed, 13 insertions(+), 3 deletions(-) + +diff --git a/tests/test_Wrapper.py b/tests/test_Wrapper.py +index 16faca6..c9f4c3d 100644 +--- a/tests/test_Wrapper.py ++++ b/tests/test_Wrapper.py +@@ -941,11 +941,21 @@ class TestWrapper(unittest.TestCase): + + obj = Hello() + +- self.assertEqual(2, sys.getrefcount(obj)) ++ # CPython 3.14's LOAD_FAST_BORROW optimization (see the 3.14 ++ # what's-new "Optimizations" notes) skips the incref a ++ # sys.getrefcount(obj) call used to take for its own `obj` ++ # argument whenever the compiler can prove the frame's local ++ # slot still owns obj once the call returns -- true at both ++ # call sites below, since `obj` is read again afterward. Every ++ # baseline this test asserts is one lower on 3.14+ as a result; ++ # the delta the JSContext wrapper itself adds is unaffected. ++ base_refcount = 1 if sys.version_info >= (3, 14) else 2 ++ ++ self.assertEqual(base_refcount, sys.getrefcount(obj)) + + fn(obj) + +- self.assertEqual(4, sys.getrefcount(obj)) ++ self.assertEqual(base_refcount + 2, sys.getrefcount(obj)) + + del obj + +-- +2.55.0