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
172 changes: 172 additions & 0 deletions .github/workflows/build-lib3mf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# Based on: https://github.com/3MFConsortium/lib3mf_python/blob/v2.5.0/.github/workflows/python-package.yml
name: Build lib3mf wheels (riscv64)

on:
workflow_dispatch:
inputs:
version:
description: 'Version glob to (re)build; empty builds every version of docs/packages/lib3mf.yaml not released yet'
required: false
default: ''
pull_request:
branches: [main]
paths:
- '.github/workflows/build-lib3mf.yml'
- 'docs/packages/lib3mf.yaml'
push:
branches: [main]
paths:
- '.github/workflows/build-lib3mf.yml'
- 'docs/packages/lib3mf.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: lib3mf
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 lib3mf ${{ matrix.version }} py3-none-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 360

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

steps:
- name: Checkout lib3mf_python v${{ env.LIB3MF_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: 3MFConsortium/lib3mf_python
ref: v${{ env.LIB3MF_VERSION }}
persist-credentials: false

# lib3mf/lib3mf.so is committed to the packaging repo as an x86_64 binary,
# lifted by prepare_pypi_release.py out of the lib3mf_sdk_v<version>.zip
# release asset. That SDK is published for x86_64 Linux only, so build the
# library from its own sources instead (gotcha 77).
- name: Checkout lib3mf v${{ env.LIB3MF_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: 3MFConsortium/lib3mf
ref: v${{ env.LIB3MF_VERSION }}
path: lib3mf-src
submodules: true
persist-credentials: false

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

- name: Patch lib3mf_python source
run: git apply python-wheels/patches/lib3mf/${{ env.LIB3MF_VERSION }}/*.patch

# lib3mf.so statically links zlib, libzip and cpp-base64 and inlines
# fast_float, but the wheel ships only the binding's own BSD licence.
- name: Stage the statically linked libraries' licences
run: |
cp lib3mf-src/submodules/zlib/LICENSE LICENSE.zlib
cp lib3mf-src/submodules/libzip/LICENSE LICENSE.libzip
cp lib3mf-src/submodules/cpp-base64/LICENSE LICENSE.cpp-base64
cp lib3mf-src/submodules/fast_float/LICENSE-MIT LICENSE.fast_float

- name: Pull manylinux_riscv64 image
run: docker pull "${MANYLINUX_RISCV64_IMAGE}"

# LIB3MF_TESTS=OFF because the default target also builds the C++ test
# suite and the vendored libressl it links, neither of which reaches the
# wheel; the wheel's own tests cover the library below.
- name: Build lib3mf.so and the wheel
run: |
docker run --rm -v "$(pwd):/work" -w /work "${MANYLINUX_RISCV64_IMAGE}" bash -c '
set -euxo pipefail
cd /work/lib3mf-src
sh cmake/GenerateMake.sh -DLIB3MF_TESTS=OFF
cmake --build build -j"$(nproc)"
readelf -hd build/lib3mf.so
cd /work
install -m644 -T lib3mf-src/build/lib3mf.so lib3mf/lib3mf.so
export PATH=/opt/python/cp312-cp312/bin:$PATH
pip install -q -U setuptools wheel
python build_wheels.py
pip install -q dist/*.whl
cd /tmp
python -c "from lib3mf import get_wrapper; print(get_wrapper().GetLibraryVersion())"
python /work/examples/create_cube_example_complete.py
python - <<PYEOF
import lib3mf

model = lib3mf.get_wrapper().CreateModel()
model.QueryReader("3mf").ReadFromFile("cube.3mf")
meshes = model.GetMeshObjects()
assert meshes.MoveNext()
cube = meshes.GetCurrentMeshObject()
assert (cube.GetVertexCount(), cube.GetTriangleCount()) == (8, 12)
assert not meshes.MoveNext()
PYEOF
python /work/tests/lib3mf_py313_exception.py
'

- name: Check the wheel carries the riscv64 lib3mf.so and every licence
run: |
python3 - dist/*.whl <<'EOF'
import sys, zipfile

expected = {"LICENSE", "LICENSE.zlib", "LICENSE.libzip",
"LICENSE.cpp-base64", "LICENSE.fast_float"}
for whl in sys.argv[1:]:
zf = zipfile.ZipFile(whl)
names = zf.namelist()
assert "lib3mf/lib3mf.so" in names, whl
e_machine = int.from_bytes(zf.read("lib3mf/lib3mf.so")[18:20], "little")
assert e_machine == 243, f"{whl}: e_machine {e_machine} is not riscv64"
licenses = {n.split(".dist-info/licenses/", 1)[1] for n in names
if ".dist-info/licenses/" in n and not n.endswith("/")}
assert licenses == expected, f"{whl}: {sorted(licenses)}"
print(whl, "ok")
EOF

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

publish:
name: Publish lib3mf ${{ 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: lib3mf-${{ matrix.version }}-*-manylinux_riscv64
5 changes: 5 additions & 0 deletions docs/packages/lib3mf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package-name: lib3mf
source-code: https://github.com/3MFConsortium/lib3mf_python
license: BSD-3-Clause
versions:
- version: 2.5.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Mon, 21 Sep 2026 00:00:00 +0000
Subject: [PATCH] Support building the wheel on linux riscv64

The Linux wheel is a per-architecture artifact: lib3mf/lib3mf.so is a
compiled binary and build_wheels.py forces the platform tag by hand, so
the two hardcoded x86_64 assumptions are what stop a riscv64 host from
producing its own wheel.

setup.py's SUPPORTED_PLATFORMS gate raises before setuptools runs, and
get_platform_tag() returns manylinux2014_x86_64 for every Linux host.
manylinux2014 is a glibc 2.17 baseline that predates riscv64 entirely -
the oldest manylinux profile defined for that architecture is glibc 2.39
- so the tag has to be selected per architecture rather than shared.

Nothing else in the packaging is architecture-specific, and x86_64 keeps
the exact tag it had.

Upstream-Status: To upstream [not yet submitted: the same change is only useful once 3MFConsortium/lib3mf publishes a riscv64 SDK build for prepare_pypi_release.py to pick up, so it needs a maintainer discussion rather than a drive-by PR]
---
build_wheels.py | 12 +++++++++++-
setup.py | 2 +-
2 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/build_wheels.py b/build_wheels.py
index eb80ae0..a4457ee 100644
--- a/build_wheels.py
+++ b/build_wheels.py
@@ -6,11 +6,21 @@ import os
MANIFEST_FILE = "manifest.json"
TEMP_MANIFEST_IN = "MANIFEST.in"

+# manylinux tag per Linux architecture: the manylinux2014 (glibc 2.17) baseline
+# predates riscv64, whose oldest manylinux profile is glibc 2.39.
+LINUX_PLATFORM_TAGS = {
+ "x86_64": "manylinux2014_x86_64",
+ "riscv64": "manylinux_2_39_riscv64",
+}
+
def get_platform_tag():
"""Return the appropriate platform tag for wheel building."""
system = platform.system()
if system == "Linux":
- return "manylinux2014_x86_64"
+ machine = platform.machine()
+ if machine not in LINUX_PLATFORM_TAGS:
+ raise RuntimeError(f"Unsupported Linux architecture: {machine}")
+ return LINUX_PLATFORM_TAGS[machine]
elif system == "Windows":
return "win_amd64"
elif system == "Darwin":
diff --git a/setup.py b/setup.py
index 059882e..97a51c4 100644
--- a/setup.py
+++ b/setup.py
@@ -24,7 +24,7 @@ else:

# Restrict installation to only supported platforms and architectures
SUPPORTED_PLATFORMS = {
- "Linux": ["x86_64"],
+ "Linux": ["x86_64", "riscv64"],
"Windows": ["AMD64"],
"Darwin": ["x86_64", "arm64"] # macOS universal (Intel & Apple Silicon)
}
Loading