diff --git a/.github/workflows/build-lib3mf.yml b/.github/workflows/build-lib3mf.yml new file mode 100644 index 0000000000..a472201d26 --- /dev/null +++ b/.github/workflows/build-lib3mf.yml @@ -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.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 - < +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) + }