From fddd4dd0542e819c74a10b358096a9c9e0167bc5 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 19 Sep 2026 08:38:32 +0000 Subject: [PATCH 1/3] json-tools-rs: add riscv64 wheel build for 0.9.30 json-tools-rs is a maturin/PyO3 extension (SIMD JSON flatten/unflatten with Rayon parallelism and Arrow-backed DataFrame support). Upstream publishes wheels for x86_64/x86/aarch64/armv7/ppc64le but none for riscv64. Based on the `linux` job of upstream's maturin-ci.yml, narrowed to manylinux_riscv64: musllinux is dropped because rustup.rs ships no riscv64 musl toolchain, and cibuildwheel replaces maturin-action so the Rust toolchain is installed in-container. pyo3 0.29 carries no abi3 feature, so the build is per-interpreter. cp314t is left out: orjson, a hard runtime dependency, publishes no free-threaded wheel on PyPI or pypi.riseproject.dev. --- .github/workflows/build-json-tools-rs.yml | 117 ++++++++++++++++++++++ docs/packages/json-tools-rs.yaml | 5 + 2 files changed, 122 insertions(+) create mode 100644 .github/workflows/build-json-tools-rs.yml create mode 100644 docs/packages/json-tools-rs.yaml diff --git a/.github/workflows/build-json-tools-rs.yml b/.github/workflows/build-json-tools-rs.yml new file mode 100644 index 0000000000..4ae922f985 --- /dev/null +++ b/.github/workflows/build-json-tools-rs.yml @@ -0,0 +1,117 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on the `linux` job of +# https://github.com/amaye15/JSON-Tools-rs/blob/v0.9.30/.github/workflows/maturin-ci.yml +name: Build json-tools-rs wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/json-tools-rs.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-json-tools-rs.yml' + - 'docs/packages/json-tools-rs.yaml' + push: + branches: [main] + paths: + - '.github/workflows/build-json-tools-rs.yml' + - 'docs/packages/json-tools-rs.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: json-tools-rs + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build json-tools-rs ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 360 + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + # No abi3 feature on pyo3 in Cargo.toml, so upstream ships one wheel per + # interpreter. cp314t is dropped: orjson - a hard runtime dependency - + # publishes no free-threaded wheel on PyPI or pypi.riseproject.dev + # (gotcha 84). + python: ["cp312", "cp313", "cp314"] + + env: + JSON_TOOLS_RS_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout json-tools-rs v${{ env.JSON_TOOLS_RS_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: amaye15/JSON-Tools-rs + ref: v${{ env.JSON_TOOLS_RS_VERSION }} + persist-credentials: false + + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + output-dir: wheelhouse/ + # musllinux is dropped: rustup.rs ships no riscv64 musl toolchain. + only: ${{ matrix.python }}-manylinux_riscv64 + env: + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + # json-tools-rs ships no [tool.cibuildwheel], so the Rust toolchain + # its maturin backend needs is installed in-container. + CIBW_BEFORE_ALL_LINUX: >- + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + CIBW_ENVIRONMENT_LINUX: >- + PATH="$PATH:$HOME/.cargo/bin" + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + # Without it pip prefers PyPI's orjson, which has no riscv64 wheel, + # and source-builds it in the container. + CIBW_TEST_ENVIRONMENT: PIP_ONLY_BINARY=orjson + CIBW_TEST_REQUIRES: pytest orjson + # pyproject.toml carries the [tool.pytest.ini_options] that adds + # tests.py to python_files; without it pytest collects nothing. + CIBW_TEST_SOURCES: pyproject.toml python/tests + CIBW_TEST_COMMAND: >- + python -c "import json_tools_rs; assert json_tools_rs.json_tools_rs.__file__.endswith('.so'), json_tools_rs.json_tools_rs.__file__" && + python -m pytest -v + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: json-tools-rs-${{ env.JSON_TOOLS_RS_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish json-tools-rs ${{ matrix.version }} + needs: [setup, build_wheels] + 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: json-tools-rs-${{ matrix.version }}-*-manylinux_riscv64 diff --git a/docs/packages/json-tools-rs.yaml b/docs/packages/json-tools-rs.yaml new file mode 100644 index 0000000000..c88d865566 --- /dev/null +++ b/docs/packages/json-tools-rs.yaml @@ -0,0 +1,5 @@ +package-name: json-tools-rs +source-code: https://github.com/amaye15/JSON-Tools-rs +license: MIT OR Apache-2.0 +versions: +- version: 0.9.30 From aaf2ad8036c28c50faf97fdde233e15eb203e67f Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 19 Sep 2026 08:38:41 +0000 Subject: [PATCH 2/3] ci_scripts: add check_riscv64_deps.py Every port has to settle which interpreters its dependencies actually resolve for on riscv64 before writing the matrix, and the pip incantation for it is easy to get wrong: the manylinux_2_NN_riscv64 platform list differs per wheel, and without `--abi abi3 --abi none` pip rejects every abi3 and py3-none-any wheel so a resolvable set looks impossible. This wraps it as a loop over interpreter tags, printing the wheel each requirement resolves to or naming the interpreters to drop. json-tools-rs is the first user: it reports cp312/cp313/cp314 ok and cp314t unresolvable, because orjson publishes no free-threaded wheel. --- ci_scripts/check_riscv64_deps.py | 116 +++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 ci_scripts/check_riscv64_deps.py diff --git a/ci_scripts/check_riscv64_deps.py b/ci_scripts/check_riscv64_deps.py new file mode 100644 index 0000000000..20713f65bb --- /dev/null +++ b/ci_scripts/check_riscv64_deps.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +# /// script +# requires-python = ">=3.10" +# dependencies = ["pip"] +# /// +""" +Resolve requirements for riscv64, per interpreter, against PyPI + our registry. + +Answers "which interpreters can this port actually test on?" before a CI cycle: +a dependency we host for cp312/cp313 but not cp314 caps the build matrix, and +pip is the only honest oracle for it. Runs `pip download --only-binary=:all:` +once per interpreter tag with every manylinux_2_NN_riscv64 platform the +registry uses, and prints the wheel each requirement resolves to, or the +interpreter as unresolvable. + +Nothing is installed and nothing is kept: each run downloads into a temporary +directory that is removed afterwards. + +`--abi abi3 --abi none` is passed alongside the interpreter's own tag on +purpose: with the literal tag only, pip silently rejects every `cpNN-abi3` and +`py3-none-any` wheel and a perfectly resolvable set looks impossible. A +free-threaded tag (`314t`) gets no `abi3`, which that build does not support. + +Usage: + uv run ci_scripts/check_riscv64_deps.py pytest orjson + uv run ci_scripts/check_riscv64_deps.py --python 312 313 314 314t -- pytest 'numpy<3' + uv run ci_scripts/check_riscv64_deps.py --no-registry pytest # PyPI only +""" + +import argparse +import re +import subprocess +import sys +import tempfile + +REGISTRY = "https://pypi.riseproject.dev/simple/" +# The glibc floors our wheels are built against; not all are the same. +PLATFORMS = [ + "manylinux_2_39_riscv64", + "manylinux_2_38_riscv64", + "manylinux_2_35_riscv64", + "manylinux_2_34_riscv64", + "manylinux_2_31_riscv64", + "manylinux_2_17_riscv64", +] +DEFAULT_PYTHONS = ["312", "313", "314", "314t"] + + +def resolve(python, requirements, registry): + """Return (ok, lines): the wheels pip picked, or its error output.""" + free_threaded = python.endswith("t") + version = python[:-1] if free_threaded else python + abis = [f"cp{python}", "none"] + if not free_threaded: + abis.insert(1, "abi3") + + cmd = [sys.executable, "-m", "pip", "download", "--only-binary=:all:"] + for platform in PLATFORMS: + cmd += ["--platform", platform] + for abi in abis: + cmd += ["--abi", abi] + cmd += ["--python-version", version, "--implementation", "cp"] + if registry: + cmd += ["--extra-index-url", REGISTRY] + + with tempfile.TemporaryDirectory() as dest: + proc = subprocess.run( + cmd + ["-d", dest, *requirements], + capture_output=True, + text=True, + ) + if proc.returncode != 0: + errors = [ln for ln in proc.stderr.splitlines() if ln.startswith("ERROR")] + return False, errors or proc.stderr.splitlines()[-3:] + wheels = sorted(re.findall(r"^Saved .*?([^/\\]+\.whl)$", proc.stdout, re.M)) + return True, wheels + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--python", + nargs="+", + default=DEFAULT_PYTHONS, + metavar="TAG", + help="interpreter tags without the 'cp' prefix (default: %(default)s)", + ) + parser.add_argument( + "--no-registry", + action="store_true", + help="resolve against PyPI alone, to show what the registry adds", + ) + parser.add_argument("requirements", nargs="+", help="pip requirement specifiers") + args = parser.parse_args() + + failed = [] + for python in args.python: + ok, lines = resolve(python, args.requirements, not args.no_registry) + print(f"cp{python}: {'ok' if ok else 'UNRESOLVABLE'}") + for line in lines: + print(f" {line}") + if not ok: + failed.append(python) + + if failed: + print( + "\nDrop from the matrix (or port the missing dependency first): " + + ", ".join(f"cp{p}" for p in failed) + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 48520b903947a5f235aee44d6f137a72d32a5715 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 19 Sep 2026 09:08:44 +0000 Subject: [PATCH 3/3] Revert "ci_scripts: add check_riscv64_deps.py" General-purpose tooling doesn't belong in a package port PR (the skill's own rule: a port adds files only under .github/workflows/, docs/packages/ and patches/). Landed on main directly instead. --- ci_scripts/check_riscv64_deps.py | 116 ------------------------------- 1 file changed, 116 deletions(-) delete mode 100644 ci_scripts/check_riscv64_deps.py diff --git a/ci_scripts/check_riscv64_deps.py b/ci_scripts/check_riscv64_deps.py deleted file mode 100644 index 20713f65bb..0000000000 --- a/ci_scripts/check_riscv64_deps.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: 2026 The RISE Project -# SPDX-License-Identifier: MIT -# /// script -# requires-python = ">=3.10" -# dependencies = ["pip"] -# /// -""" -Resolve requirements for riscv64, per interpreter, against PyPI + our registry. - -Answers "which interpreters can this port actually test on?" before a CI cycle: -a dependency we host for cp312/cp313 but not cp314 caps the build matrix, and -pip is the only honest oracle for it. Runs `pip download --only-binary=:all:` -once per interpreter tag with every manylinux_2_NN_riscv64 platform the -registry uses, and prints the wheel each requirement resolves to, or the -interpreter as unresolvable. - -Nothing is installed and nothing is kept: each run downloads into a temporary -directory that is removed afterwards. - -`--abi abi3 --abi none` is passed alongside the interpreter's own tag on -purpose: with the literal tag only, pip silently rejects every `cpNN-abi3` and -`py3-none-any` wheel and a perfectly resolvable set looks impossible. A -free-threaded tag (`314t`) gets no `abi3`, which that build does not support. - -Usage: - uv run ci_scripts/check_riscv64_deps.py pytest orjson - uv run ci_scripts/check_riscv64_deps.py --python 312 313 314 314t -- pytest 'numpy<3' - uv run ci_scripts/check_riscv64_deps.py --no-registry pytest # PyPI only -""" - -import argparse -import re -import subprocess -import sys -import tempfile - -REGISTRY = "https://pypi.riseproject.dev/simple/" -# The glibc floors our wheels are built against; not all are the same. -PLATFORMS = [ - "manylinux_2_39_riscv64", - "manylinux_2_38_riscv64", - "manylinux_2_35_riscv64", - "manylinux_2_34_riscv64", - "manylinux_2_31_riscv64", - "manylinux_2_17_riscv64", -] -DEFAULT_PYTHONS = ["312", "313", "314", "314t"] - - -def resolve(python, requirements, registry): - """Return (ok, lines): the wheels pip picked, or its error output.""" - free_threaded = python.endswith("t") - version = python[:-1] if free_threaded else python - abis = [f"cp{python}", "none"] - if not free_threaded: - abis.insert(1, "abi3") - - cmd = [sys.executable, "-m", "pip", "download", "--only-binary=:all:"] - for platform in PLATFORMS: - cmd += ["--platform", platform] - for abi in abis: - cmd += ["--abi", abi] - cmd += ["--python-version", version, "--implementation", "cp"] - if registry: - cmd += ["--extra-index-url", REGISTRY] - - with tempfile.TemporaryDirectory() as dest: - proc = subprocess.run( - cmd + ["-d", dest, *requirements], - capture_output=True, - text=True, - ) - if proc.returncode != 0: - errors = [ln for ln in proc.stderr.splitlines() if ln.startswith("ERROR")] - return False, errors or proc.stderr.splitlines()[-3:] - wheels = sorted(re.findall(r"^Saved .*?([^/\\]+\.whl)$", proc.stdout, re.M)) - return True, wheels - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--python", - nargs="+", - default=DEFAULT_PYTHONS, - metavar="TAG", - help="interpreter tags without the 'cp' prefix (default: %(default)s)", - ) - parser.add_argument( - "--no-registry", - action="store_true", - help="resolve against PyPI alone, to show what the registry adds", - ) - parser.add_argument("requirements", nargs="+", help="pip requirement specifiers") - args = parser.parse_args() - - failed = [] - for python in args.python: - ok, lines = resolve(python, args.requirements, not args.no_registry) - print(f"cp{python}: {'ok' if ok else 'UNRESOLVABLE'}") - for line in lines: - print(f" {line}") - if not ok: - failed.append(python) - - if failed: - print( - "\nDrop from the matrix (or port the missing dependency first): " - + ", ".join(f"cp{p}" for p in failed) - ) - return 0 - - -if __name__ == "__main__": - sys.exit(main())