diff --git a/.github/workflows/build-stpyv8.yml b/.github/workflows/build-stpyv8.yml new file mode 100644 index 00000000000..90936243a60 --- /dev/null +++ b/.github/workflows/build-stpyv8.yml @@ -0,0 +1,210 @@ +# 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' + # 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: + 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: 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. 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' + 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 + 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 + # 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 + 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 + + - 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 + + - 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 + 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..4b9842cf1b3 --- /dev/null +++ b/patches/stpyv8/13.1.201.22/0001-setup-make-the-V8-checkout-work-on-a-riscv64-host.patch @@ -0,0 +1,203 @@ +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 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/ + 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 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. + +* 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 + 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 | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- + 1 file changed, 100 insertions(+), 2 deletions(-) + +diff --git a/setup.py b/setup.py +index fb80c38..e887710 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,90 @@ 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(): ++ # 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: ++ 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"', ++ 'host_cpu != "s390" and host_os != "zos" and host_os != "aix"', ++ ): ++ 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 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 +174,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 +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"), + "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", 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", + ) 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", 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