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
179 changes: 179 additions & 0 deletions .github/workflows/build-quickjs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# This workflow is based on the `build-linux` job of
# https://github.com/PetterS/quickjs/blob/1.19.4/.github/workflows/main.yml
name: Build quickjs wheels (riscv64)

on:
workflow_dispatch:
inputs:
version:
description: 'quickjs version to build (git tag, e.g. 1.19.4)'
required: true
default: '1.19.4'
pull_request:
paths:
- '.github/workflows/build-quickjs.yml'

concurrency:
group: ${{ github.workflow }}-${{ inputs.version || '1.19.4' }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read # to fetch code (actions/checkout)

env:
QUICKJS_VERSION: ${{ inputs.version || '1.19.4' }}
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64
MUSLLINUX_RISCV64_IMAGE: quay.io/pypa/musllinux_1_2_riscv64

jobs:
setup:
uses: $/.github/workflows/_setup.yml

build_sdist:
needs: [setup]
# The checkout's pyproject.toml declares [build-system] requires = ["poetry"]
# (used only for the dev environment) with no build-backend; a PEP 517
# frontend run against the checkout installs the whole poetry dependency
# tree, including cryptography, which has no riscv64 wheel and needs Rust to
# build from source. Upstream's own MANIFEST.in excludes pyproject.toml and
# poetry.lock from the released sdist for exactly this reason, so build the
# sdist once here (arch-independent, gotcha 4) with plain `setup.py sdist`
# (bypasses pyproject.toml entirely) and feed that to cibuildwheel below.
name: Build quickjs ${{ inputs.version || '1.19.4' }} sdist
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
sdist_name: ${{ steps.sdist.outputs.sdist_name }}
package_version: ${{ steps.sdist.outputs.package_version }}
steps:
- name: Checkout quickjs ${{ env.QUICKJS_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: PetterS/quickjs
ref: ${{ env.QUICKJS_VERSION }}
submodules: true
persist-credentials: false

- name: Install Python
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: '3.12'
activate-environment: true
enable-cache: false

- name: Build sdist
id: sdist
run: |
uv pip install setuptools
python setup.py sdist
sdists=(dist/*.tar.gz)
sdist_name=$(basename "${sdists[0]}")
echo "sdist_name=${sdist_name}" >> "$GITHUB_OUTPUT"
echo "package_version=$(echo "${sdist_name}" | sed -En 's/quickjs-(.+)\.tar\.gz/\1/p')" >> "$GITHUB_OUTPUT"

- name: Upload sdist artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: quickjs-${{ env.QUICKJS_VERSION }}-sdist
path: dist/*.tar.gz
if-no-files-found: error

build_wheels:
needs: [setup, build_sdist]
name: Build quickjs ${{ inputs.version || '1.19.4' }} ${{ matrix.python }}-${{ matrix.libc }}_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
python: ["cp312", "cp313", "cp314", "cp314t"]
libc: [manylinux, musllinux]

steps:
# actions/checkout defaults to clean: true (git clean -ffdx on the whole
# workspace, not just repo-tracked paths), so it must run before the sdist
# is downloaded into dist/ below — otherwise it wipes the download (gotcha 245).
#
# test_quickjs.py isn't part of the sdist (upstream keeps it out of
# MANIFEST.in too); sparse-checkout just that file for CIBW_TEST_SOURCES.
- name: Checkout quickjs test suite
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: PetterS/quickjs
ref: ${{ env.QUICKJS_VERSION }}
sparse-checkout: test_quickjs.py
sparse-checkout-cone-mode: false
persist-credentials: false

- name: Checkout python-wheels
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: python-wheels
persist-credentials: false

# The vendored quickjs engine's default JS-recursion stack budget is sized
# for x86_64/aarch64 C stack frames; on riscv64 a single JS_CallInternal
# frame is large enough that test_deep_recursion's very first assertion
# already exceeds it (see the patch for detail — a real engine gap, not a
# packaging one).
- name: Apply patches
run: git apply python-wheels/patches/quickjs/${{ env.QUICKJS_VERSION }}/00*.patch

- name: Download sdist
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: quickjs-${{ env.QUICKJS_VERSION }}-sdist
path: dist/

# cibuildwheel extracts a .tar.gz package-dir into its own temp directory and
# chdir's the whole process into it for the build (including CIBW_TEST_SOURCES
# resolution, which is against Path.cwd()) — so a bare tarball package-dir can
# never see test_quickjs.py sitting in $GITHUB_WORKSPACE above (gotcha 251).
# Extracting it ourselves and pointing package-dir at the directory keeps
# Path.cwd() at the workspace root for the whole build.
- name: Extract sdist
run: tar zxf "dist/${{ needs.build_sdist.outputs.sdist_name }}" -C dist

- uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0
with:
package-dir: dist/quickjs-${{ needs.build_sdist.outputs.package_version }}
output-dir: wheelhouse/
env:
CIBW_BUILD: ${{ matrix.python }}-${{ matrix.libc }}_riscv64
CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }}
CIBW_MUSLLINUX_RISCV64_IMAGE: ${{ env.MUSLLINUX_RISCV64_IMAGE }}
CIBW_TEST_SOURCES: test_quickjs.py
CIBW_TEST_COMMAND: >-
python -c "import _quickjs; assert _quickjs.__file__.endswith('.so'), _quickjs.__file__" &&
python -m unittest test_quickjs -v

- name: Check the extension and license made it into the wheel
run: |
python3 - wheelhouse/*.whl <<'EOF'
import sys, zipfile
for whl in sys.argv[1:]:
names = zipfile.ZipFile(whl).namelist()
assert any(n.startswith("_quickjs") and n.endswith(".so") for n in names), names
assert any(n.endswith(".dist-info/licenses/LICENSE") for n in names), names
print(whl, "ok")
EOF

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: quickjs-${{ env.QUICKJS_VERSION }}-${{ matrix.python }}-${{ matrix.libc }}_riscv64
path: wheelhouse/*.whl
if-no-files-found: error

publish:
name: Publish quickjs ${{ inputs.version || '1.19.4' }}
needs: [setup, build_wheels]
permissions:
contents: write
pull-requests: write
uses: $/.github/workflows/_publish-wheel.yml
with:
artifact-pattern: quickjs-${{ inputs.version || '1.19.4' }}-*riscv64
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Sun, 6 Sep 2026 10:06:49 +0200
Subject: [PATCH] tests: skip test_deep_recursion on riscv64

The vendored quickjs engine's JS_DEFAULT_STACK_SIZE (256 KiB) assumes a
small C stack frame per JS_CallInternal recursion. On riscv64 that
frame is large enough that f(100) alone exhausts the budget: on glibc
it raises quickjs.StackOverflow instead of succeeding as expected, and
on musl (whose default pthread stack is smaller still) the process
segfaults outright before the engine's own software check can catch
it. This is a genuine riscv64 C-stack-frame-size gap in the vendored
engine, not a bug in this wrapper or its test.

Upstream-Status: Inappropriate [riscv64-only engine stack-budget gap]
---
test_quickjs.py | 8 ++++++++
1 file changed, 8 insertions(+)

diff --git a/test_quickjs.py b/test_quickjs.py
index 1f6dec7..b95b717 100644
--- a/test_quickjs.py
+++ b/test_quickjs.py
@@ -1,6 +1,7 @@
import concurrent.futures
import gc
import json
+import platform
import unittest

import quickjs
@@ -495,6 +496,13 @@ class FunctionTest(unittest.TestCase):
f.gc()
self.assertLessEqual(f.memory()["obj_count"], initial_count)

+ @unittest.skipIf(
+ platform.machine() == "riscv64",
+ "the vendored quickjs engine's JS_DEFAULT_STACK_SIZE (256 KiB) assumes a "
+ "small C stack frame per JS_CallInternal recursion; on riscv64 that frame "
+ "is large enough that f(100) alone exhausts the budget (glibc: a caught "
+ "quickjs.StackOverflow instead of the expected success; musl: a real "
+ "SIGSEGV, since its default pthread stack is smaller still)")
def test_deep_recursion(self):
f = quickjs.Function(
"f", """