Skip to content
Open
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
207 changes: 207 additions & 0 deletions .github/workflows/build-treelite-runtime.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# treelite-runtime 3.9.1 is the last release of this distribution: starting with
# treelite 4.0.0, runtime/python was dropped from this repo and reborn as
# tl2cgen_runtime in dmlc/tl2cgen. Based on upstream's own manylinux wheel job,
# which builds both python/ and runtime/python/ from one CMake build and tags
# the runtime wheel py3-none via `bdist_wheel --universal`:
# https://github.com/dmlc/treelite/blob/3.9.1/ops/build-linux.sh
name: Build treelite-runtime wheels (riscv64)

on:
workflow_dispatch:
inputs:
version:
description: 'Version glob to (re)build; empty builds every version of docs/packages/treelite-runtime.yaml not released yet'
required: false
default: ''
pull_request:
branches: [main]
paths:
- '.github/workflows/build-treelite-runtime.yml'
- 'docs/packages/treelite-runtime.yaml'
- 'patches/treelite-runtime/**'
push:
branches: [main]
paths:
- '.github/workflows/build-treelite-runtime.yml'
- 'docs/packages/treelite-runtime.yaml'
- 'patches/treelite-runtime/**'

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

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

env:
# Pinned explicitly so the gpl_sources job pulls the same image the wheel's
# vendored libgomp came from.
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64

jobs:
setup:
uses: $/.github/workflows/_setup.yml
with:
package: treelite-runtime
version: ${{ inputs.version }}

build_wheels:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
name: Build treelite-runtime ${{ matrix.version }} py3-none-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 180

env:
TREELITE_RUNTIME_VERSION: ${{ matrix.version }}

steps:
- name: Checkout treelite ${{ env.TREELITE_RUNTIME_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: dmlc/treelite
ref: ${{ env.TREELITE_RUNTIME_VERSION }}
persist-credentials: false

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

- name: Patch treelite source
run: git apply python-wheels/patches/treelite-runtime/${{ env.TREELITE_RUNTIME_VERSION }}/0001-*.patch

# setuptools' default license-files glob (LICEN[CSZ]E*) is rooted at
# setup.py's own directory, which for the runtime package is
# runtime/python/, not the repository root where LICENSE actually lives.
- name: Stage licence text for packaging
run: cp LICENSE runtime/python/LICENSE

- name: Build wheel
uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0
with:
package-dir: runtime/python
output-dir: wheelhouse/
# runtime/python/setup.py declares ext_modules so cibuildwheel tags
# the wheel cp312-cp312; retagged py3-none below since
# libtreelite_runtime.so is ctypes-loaded, not interpreter-specific.
only: cp312-manylinux_riscv64
env:
CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }}
# CMAKE_POLICY_VERSION_MINIMUM covers the vendored fmtlib's cmake_minimum_required.
CIBW_ENVIRONMENT: PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ CMAKE_POLICY_VERSION_MINIMUM=3.5
# The root CMakeLists builds both the treelite and treelite_runtime
# shared libs in one pass; setup.py picks up the latter from
# {project}/build instead of rebuilding it.
CIBW_BEFORE_BUILD: |
cmake -S {project} -B {project}/build -DCMAKE_BUILD_TYPE=Release
cmake --build {project}/build --parallel
# python/ (the treelite package proper) is not published here -- it
# reuses the same {project}/build libtreelite.so and is installed
# only so a real model can be compiled and loaded, the way
# upstream's own test_basic.py does.
CIBW_BEFORE_TEST: pip install {project}/python
CIBW_TEST_ENVIRONMENT: PIP_ONLY_BINARY=numpy,scipy
CIBW_TEST_SOURCES: tests/examples
# test_basic.py itself is not run: every check builds its DMatrix
# from a SciPy CSR matrix via np.array(..., copy=False, dtype=cast),
# which numpy 2 (the only line with a riscv64 wheel) rejects outright
# on any platform, treelite-runtime predating numpy 2 -- not a
# riscv64 gap. DMatrix-from-dense-ndarray asks for no such cast.
CIBW_TEST_COMMAND: |
python3 - <<'PYEOF'
import numpy as np
import treelite, treelite_runtime

treelite.Model.load('tests/examples/mushroom/mushroom.model', model_format='xgboost').export_lib(
toolchain='gcc', libpath='/tmp/mushroom.so', verbose=True)
predictor = treelite_runtime.Predictor(libpath='/tmp/mushroom.so', verbose=True)
data = np.random.rand(5, predictor.num_feature).astype('float32')
out = predictor.predict(treelite_runtime.DMatrix(data))
assert out.shape == (5,) and np.isfinite(out).all(), out
print(out)
PYEOF

# setup.py's bdist_wheel has no py3-none override for pip-driven builds
# (upstream's own ops/build-linux.sh passes --universal by hand), so
# cibuildwheel produces a cp312-cp312 tag; restore upstream's tag here.
- name: Retag the wheel py3-none (libtreelite_runtime.so is ctypes-loaded)
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq --no-install-recommends python3-venv
python3 -m venv .venv
. .venv/bin/activate
pip install -q "wheel>=0.42"
python3 -m wheel tags --python-tag py3 --abi-tag none --remove wheelhouse/treelite_runtime-*.whl

- name: Check the wheel carries libtreelite_runtime and a vendored libgomp
run: |
python3 - wheelhouse/*.whl <<'EOF'
import sys, zipfile

names = zipfile.ZipFile(sys.argv[1]).namelist()
for want in ("treelite_runtime/lib/libtreelite_runtime.so", "libgomp"):
if not any(want in n for n in names):
raise SystemExit(f"error: {want} missing from {sys.argv[1]}")
print("\n".join(n for n in names if ".so" in n))
EOF

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: treelite-runtime-${{ env.TREELITE_RUNTIME_VERSION }}-py3-none-manylinux_riscv64
path: wheelhouse/*.whl
if-no-files-found: error

gpl_sources:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
name: Collect GPL sources (gcc) for treelite-runtime ${{ matrix.version }}
runs-on: ubuntu-24.04-riscv
env:
TREELITE_RUNTIME_VERSION: ${{ matrix.version }}

steps:
# auditwheel vendors the image's libgomp.so.1 into the wheel.
- name: Collect gcc source RPM from manylinux_riscv64
uses: riseproject-dev/python-wheels/actions/collect-gpl-sources@main
with:
image: ${{ env.MANYLINUX_RISCV64_IMAGE }}
packages: gcc
output: gpl-sources.tar

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: treelite-runtime-${{ env.TREELITE_RUNTIME_VERSION }}-gpl-sources
path: gpl-sources.tar
if-no-files-found: error

publish:
name: Publish treelite-runtime ${{ matrix.version }}
needs: [setup, build_wheels, gpl_sources]
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: treelite-runtime-${{ matrix.version }}-*-manylinux_riscv64
gpl-sources-artifact: treelite-runtime-${{ matrix.version }}-gpl-sources
gpl-sources-description: gcc
5 changes: 5 additions & 0 deletions docs/packages/treelite-runtime.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package-name: treelite-runtime
source-code: https://github.com/dmlc/treelite
license: Apache-2.0
versions:
- version: 3.9.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
From eaa233f4e012135829b4584d97b18a9bd534e4ee Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Mon, 5 Oct 2026 19:26:42 +0000
Subject: [PATCH] Pin vendored RapidJSON past v1.1.0's broken
GenericStringRef::operator=

GenericStringRef declares a copy-assignment operator that assigns to
its own const length member, which GCC 14 rejects outright instead of
just warning (operator= is a template-class member never odr-used
elsewhere in treelite, so older GCC never instantiated, and so never
type-checked, its body). Fixed 3 commits after v1.1.0 by explicitly
deleting the operator; pin to that commit instead of the tag.

Upstream-Status: Backport [https://github.com/Tencent/rapidjson/commit/862c39be371278a45a88d4d1d75164be57bb7e2d]
---
cmake/ExternalLibs.cmake | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/cmake/ExternalLibs.cmake b/cmake/ExternalLibs.cmake
index 996874f..4405f50 100644
--- a/cmake/ExternalLibs.cmake
+++ b/cmake/ExternalLibs.cmake
@@ -29,7 +29,7 @@ else()
FetchContent_Declare(
RapidJSON
GIT_REPOSITORY https://github.com/Tencent/rapidjson
- GIT_TAG v1.1.0
+ GIT_TAG 862c39be371278a45a88d4d1d75164be57bb7e2d
)
FetchContent_Populate(RapidJSON)
message(STATUS "RapidJSON was downloaded at ${rapidjson_SOURCE_DIR}.")
--
2.43.0

Loading