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
167 changes: 167 additions & 0 deletions .github/workflows/build-yggdrasil-engine.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# Based on upstream's own Python build/publish tooling at this tag:
# https://github.com/Unleash/yggdrasil-bindings/blob/python-engine-v1.3.1/python-engine/{build.sh,tox.ini,scripts/repair_wheel.py}
# The PyPI package `yggdrasil-engine` is Unleash's ctypes binding to their
# Rust `yggdrasilffi` cdylib, not Google's Yggdrasil Decision Forests.
name: Build yggdrasil-engine wheels (riscv64)

on:
workflow_dispatch:
inputs:
version:
description: 'yggdrasil-engine version to build (git tag python-engine-v<version>, e.g. 1.3.1)'
required: true
default: '1.3.1'
pull_request:
paths:
- '.github/workflows/build-yggdrasil-engine.yml'

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

permissions:
contents: read

env:
YGGDRASIL_ENGINE_VERSION: ${{ inputs.version || '1.3.1' }}
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64

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

build_wheel:
needs: [setup]
name: Build yggdrasil-engine ${{ inputs.version || '1.3.1' }} cp312-abi3-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 60
steps:
- name: Checkout yggdrasil-bindings python-engine-v${{ env.YGGDRASIL_ENGINE_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Unleash/yggdrasil-bindings
ref: python-engine-v${{ env.YGGDRASIL_ENGINE_VERSION }}
persist-credentials: false

# build-python.yml's own "Get client spec" step.
- name: Get client spec
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Unleash/client-specification
ref: v6.1.0
path: client-specification
persist-credentials: false

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

# compat_glibc.rs defines SYS_GETTID/SYS_STATX for x86_64 and aarch64
# only but compiles unconditionally, so the crate fails to build on
# riscv64. See patches/yggdrasil-engine/1.3.1 for the full rationale.
- name: Patch yggdrasilffi source
run: git apply python-wheels/patches/yggdrasil-engine/${{ env.YGGDRASIL_ENGINE_VERSION }}/00*.patch

# LICENSE.md lives at the repo root; poetry-core only auto-bundles a
# licence file that sits beside pyproject.toml, and upstream's own
# published wheel carries none either (same gap build-valkey-glide.yml
# hit).
- name: Stage the project licence beside pyproject.toml
run: cp LICENSE.md python-engine/LICENSE.md

# Mirrors build-python.yml (build the ffi, `poetry run pytest`) and
# python-engine/build.sh + tox.ini (vendor the binary, `poetry build`,
# repair_wheel.py retags the resulting py3-none-any wheel): upstream
# downloads a prebuilt binary here instead of building it, but ships
# none for riscv64, so this builds it natively in-container. musllinux
# is skipped: rustup ships no riscv64gc-unknown-linux-musl host
# toolchain (gotcha 10).
- name: Write in-container build script
run: |
cat > "$RUNNER_TEMP/build.sh" <<'SCRIPT'
set -euxo pipefail
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
cargo build --release -p yggdrasilffi

pybin=/opt/python/cp312-cp312/bin
"$pybin/pip" install -q poetry-core setuptools build pytest
mkdir -p python-engine/yggdrasil_engine/lib python-engine/staging
cp target/release/libyggdrasilffi.so python-engine/yggdrasil_engine/lib/

cd python-engine
"$pybin/python3" -m pytest
"$pybin/python3" -m build --wheel --no-isolation
PLATFORM_TAG=manylinux_2_39_riscv64 "$pybin/python3" scripts/repair_wheel.py
SCRIPT

- name: Build wheel
run: |
docker run --rm \
-v "$(pwd)":/workspace \
-v "$RUNNER_TEMP/build.sh":/build.sh \
-w /workspace \
"${MANYLINUX_RISCV64_IMAGE}" \
bash /build.sh

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: yggdrasil-engine-${{ env.YGGDRASIL_ENGINE_VERSION }}-cp312-abi3-manylinux_riscv64
path: python-engine/staging/*.whl
if-no-files-found: error

test_wheel:
name: Test yggdrasil-engine ${{ inputs.version || '1.3.1' }} on Python ${{ matrix.python-version }}
needs: [setup, build_wheel]
runs-on: ubuntu-24.04-riscv
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
# abi3 wheels are not compatible with free-threaded interpreters
# (packaging.tags._abi3_applies), matching upstream, which ships no
# cp3XXt wheel either; 3.12/3.13/3.14 exercise the same cp312-abi3
# wheel via abi3 forward compatibility.
python-version: ['3.12', '3.13', '3.14']
steps:
- name: Download wheel
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: yggdrasil-engine-${{ env.YGGDRASIL_ENGINE_VERSION }}-cp312-abi3-manylinux_riscv64

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

- name: Install wheel
run: uv pip install --reinstall --no-index --find-links . yggdrasil-engine==${{ env.YGGDRASIL_ENGINE_VERSION }}

- name: Test wheel
run: |
python -c "
from yggdrasil_engine.engine import UnleashEngine

engine = UnleashEngine()
engine.take_state('{\"version\": 2, \"features\": [{\"name\": \"my-toggle\", \"enabled\": true, \"strategies\": [{\"name\": \"default\"}]}]}')
assert engine.is_enabled('my-toggle', {}) is True
assert engine.is_enabled('missing-toggle', {}) is None
print('Smoke test passed')
"

publish:
name: Publish yggdrasil-engine ${{ inputs.version || '1.3.1' }}
needs: [setup, build_wheel, test_wheel]
permissions:
contents: write
pull-requests: write
uses: $/.github/workflows/_publish-wheel.yml
with:
artifact-pattern: yggdrasil-engine-${{ inputs.version || '1.3.1' }}-cp312-abi3-manylinux_riscv64
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
From 79a1d5318f08a15066d691f90d263496edd4b227 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Sun, 13 Sep 2026 01:34:34 +0200
Subject: [PATCH] yggdrasilffi: add riscv64 syscall numbers for gettid/statx
compat shim

compat_glibc.rs hand-rolls gettid(2)/statx(2) via raw syscall(2) to
avoid depending on the libc crate's own bindings, which would require
a newer glibc than the zigbuild-targeted GLIBC_2.25 floor. It defines
SYS_GETTID/SYS_STATX for x86_64 and aarch64 only; mod compat_glibc is
included unconditionally (no target_arch gate), so on any other arch
- riscv64 included - the crate fails to compile with two
cannot-find-value errors for the two undefined consts.

riscv64 and aarch64 both use the Linux kernel's generic syscall table
(include/uapi/asm-generic/unistd.h: __NR_gettid 178, __NR_statx 291,
confirmed also in the libc crate's own riscv64 module), unlike x86_64,
which has its own legacy numbering. Add the same two constants under
target_arch = "riscv64".

Upstream-Status: To upstream [not filed upstream from this porting session, which does not open issues/PRs on repos other than riseproject-dev/python-wheels; genuinely worth upstreaming, it only adds an arch branch matching aarch64's already-supported case]

Signed-off-by: Ludovic Henry <git@ludovic.dev>
---
yggdrasilffi/src/compat_glibc.rs | 6 ++++++
1 file changed, 6 insertions(+)

diff --git a/yggdrasilffi/src/compat_glibc.rs b/yggdrasilffi/src/compat_glibc.rs
index c13446d..1a115ac 100644
--- a/yggdrasilffi/src/compat_glibc.rs
+++ b/yggdrasilffi/src/compat_glibc.rs
@@ -32,6 +32,12 @@ const SYS_GETTID: c_long = 178;
#[cfg(target_arch = "aarch64")]
const SYS_STATX: c_long = 291;

+// riscv64 syscall numbers (riscv64 shares aarch64's asm-generic/unistd.h table)
+#[cfg(target_arch = "riscv64")]
+const SYS_GETTID: c_long = 178;
+#[cfg(target_arch = "riscv64")]
+const SYS_STATX: c_long = 291;
+
#[no_mangle]
pub unsafe extern "C" fn gettid() -> pid_t {
syscall(SYS_GETTID) as pid_t
--
2.50.1 (Apple Git-155)