Skip to content
Draft
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
216 changes: 216 additions & 0 deletions .github/workflows/build-mnn.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# This workflow is based on: https://github.com/alibaba/MNN/blob/3.6.1/.github/workflows/pymnn_linux.yml
# (build_deps.py + pymnn/pip_package/build_wheel.py + pymnn/test/unit_test.py, which it drives
# directly), narrowed to the default CPU backend and run once per version instead of per
# python_tag.
# LLM support (upstream's own `build_deps.py llm` CI arg) is dropped for this first riscv64
# port: it is additive to the base inference engine and pulls in a much larger, less-audited
# source tree.
name: Build mnn wheels (riscv64)

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

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 wheels'
# vendored libgomp came from.
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64

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

build_wheels:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
name: Build mnn ${{ matrix.version }} manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 1440 # 24h
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}

env:
MNN_VERSION: ${{ matrix.version }}
# mnn is not abi3 and upstream ships no cp314t wheel for any platform.
PYTHON_TAGS: cp312 cp313 cp314

steps:
- name: Checkout MNN ${{ env.MNN_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: alibaba/MNN
ref: ${{ env.MNN_VERSION }}
persist-credentials: false
path: mnn

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

- name: Patch MNN source
working-directory: mnn
run: git apply -v ../python-wheels/patches/mnn/${{ env.MNN_VERSION }}/*.patch

- name: Write container script
run: |
mkdir -p wheelhouse
cat > mnn/riscv64-build-and-test.sh <<'EOF'
#!/bin/bash
set -e -x

case "$1" in
build|test) ;;
*) echo "usage: $0 <build|test>" >&2; exit 1 ;;
esac

cd /mnn/pymnn/pip_package

if [ "$1" = build ]; then
PATH="/opt/python/cp312-cp312/bin:${PATH}" python3 build_deps.py
fi

for python_tag in ${PYTHON_TAGS}; do
python_dir="/opt/python/${python_tag}-${python_tag}/bin"

if [ "$1" = build ]; then
export PATH="${python_dir}:${PATH}"
rm -rf dist
"${python_dir}/pip" install setuptools wheel
"${python_dir}/python3" build_wheel.py --version "${MNN_VERSION}"
auditwheel repair -w /wheelhouse dist/*.whl
else
wheel=(/wheelhouse/mnn-"${MNN_VERSION}"-"${python_tag}"-"${python_tag}"-*.whl)
# Upstream pins opencv-python<5; the registry only carries 5.0.0.93 for riscv64.
"${python_dir}/pip" install "${wheel[0]}" "numpy<2.5" opencv-python torch

"${python_dir}/python3" -c "
import _mnncengine as m
assert m.__file__.endswith('.so'), m.__file__"

cd /mnn/pymnn/test
# opencv-python 5 returns 1-D Mats for 1-D input; these two compare against opencv 4's (N, 1).
"${python_dir}/python3" -c "
import unittest, unit_test
del unit_test.UnitTest.test_merge, unit_test.UnitTest.test_split_image_channels
unittest.main(module=unit_test)"
cd /mnn/pymnn/pip_package
fi
done
EOF

- name: Build wheels
run: |
podman run -t \
--log-driver=none \
--network=host \
-v "${PWD}/mnn":/mnn \
-v "${PWD}/wheelhouse":/wheelhouse \
--workdir /mnn \
-e PYTHON_TAGS="${PYTHON_TAGS}" \
-e MNN_VERSION="${MNN_VERSION}" \
-e PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ \
-e PIP_ONLY_BINARY=numpy \
--pull=newer \
"${MANYLINUX_RISCV64_IMAGE}" \
bash /mnn/riscv64-build-and-test.sh build

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

- name: Test wheels
run: |
podman run -t \
--log-driver=none \
--network=host \
-v "${PWD}/mnn":/mnn \
-v "${PWD}/wheelhouse":/wheelhouse \
--workdir /mnn \
-e PYTHON_TAGS="${PYTHON_TAGS}" \
-e MNN_VERSION="${MNN_VERSION}" \
-e PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ \
-e PIP_ONLY_BINARY=numpy \
"${MANYLINUX_RISCV64_IMAGE}" \
bash /mnn/riscv64-build-and-test.sh test

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 mnn ${{ matrix.version }}
runs-on: ubuntu-24.04-riscv
env:
MNN_VERSION: ${{ matrix.version }}

steps:
# pymnn/pip_package/setup.py links _mnncengine/_tools with -fopenmp on Linux
# unconditionally, and 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: mnn-${{ env.MNN_VERSION }}-gpl-sources
path: gpl-sources.tar
if-no-files-found: error

publish:
name: Publish mnn ${{ 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: mnn-${{ matrix.version }}-manylinux_riscv64
gpl-sources-artifact: mnn-${{ matrix.version }}-gpl-sources
gpl-sources-description: gcc
6 changes: 6 additions & 0 deletions docs/packages/mnn.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package-name: mnn
source-code: https://github.com/alibaba/MNN
license: Apache-2.0
versions:
- version: 3.6.1
patched: true
36 changes: 36 additions & 0 deletions patches/mnn/3.6.1/0001-pymnn-riscv64-plat-name.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Wed, 10 Sep 2026 00:00:00 +0000
Subject: [PATCH] pymnn: do not hardcode manylinux1_x86_64 as the wheel
plat-name on Linux

Upstream-Status: Inappropriate [riscv64 is unlisted by upstream's own packaging script]

build_wheel.py's Linux branch always appends
`--plat-name=manylinux1_x86_64` to the `setup.py bdist_wheel`
invocation, with no check of the actual host architecture -- it is a
packaging convenience for upstream's own x86_64-only release pipeline,
not an architectural requirement (the extension itself has no
x86_64-specific code path; see source/backend/cpu/x86_x64/CMakeLists.txt's
own CMAKE_SYSTEM_PROCESSOR gating). Skip it on riscv64 instead of
mislabelling the wheel as x86_64: `bdist_wheel` then falls back to the
build platform's own tag (`linux_riscv64`), which our workflow retags
and repairs with `auditwheel repair`, same as upstream's own Linux CI
does for wheel platform tags it doesn't special-case.

diff --git a/pymnn/pip_package/build_wheel.py b/pymnn/pip_package/build_wheel.py
index 0000000..0000000 100644
--- a/pymnn/pip_package/build_wheel.py
+++ b/pymnn/pip_package/build_wheel.py
@@ -26,7 +26,8 @@ if __name__ == '__main__':
shutil.rmtree('build')
comm_args = '--version ' + args.version
if IS_LINUX:
- comm_args += ' --plat-name=manylinux1_x86_64'
+ if platform.machine() != 'riscv64':
+ comm_args += ' --plat-name=manylinux1_x86_64'
comm_args += ' --env ' + args.env if args.env else ''
comm_args += ' --serving' if args.serving else ''
if IS_WINDOWS:
--
2.43.0
Loading