diff --git a/.github/workflows/build-yggdrasil-engine.yml b/.github/workflows/build-yggdrasil-engine.yml new file mode 100644 index 0000000000..2ca6282d1d --- /dev/null +++ b/.github/workflows/build-yggdrasil-engine.yml @@ -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, 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 diff --git a/patches/yggdrasil-engine/1.3.1/0001-yggdrasilffi-riscv64-syscall-numbers.patch b/patches/yggdrasil-engine/1.3.1/0001-yggdrasilffi-riscv64-syscall-numbers.patch new file mode 100644 index 0000000000..8cad8aea45 --- /dev/null +++ b/patches/yggdrasil-engine/1.3.1/0001-yggdrasilffi-riscv64-syscall-numbers.patch @@ -0,0 +1,47 @@ +From 79a1d5318f08a15066d691f90d263496edd4b227 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +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 +--- + 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) +