diff --git a/.github/workflows/build-pyhmmer.yml b/.github/workflows/build-pyhmmer.yml new file mode 100644 index 000000000..655fdf134 --- /dev/null +++ b/.github/workflows/build-pyhmmer.yml @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on upstream's own cibuildwheel-driven +# https://github.com/althonos/pyhmmer/blob/v0.12.3/.github/workflows/package.yml +# (the `wheel-linux-x86_64`/`wheel-linux-aarch64` jobs), narrowed to Linux riscv64. +name: Build pyhmmer wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'pyhmmer version to build (git tag, e.g. 0.12.3)' + required: true + default: '0.12.3' + pull_request: + paths: + - '.github/workflows/build-pyhmmer.yml' + - 'patches/pyhmmer/**' + +concurrency: + group: ${{ github.workflow }}-${{ inputs.version || '0.12.3' }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + PYHMMER_VERSION: ${{ inputs.version || '0.12.3' }} + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + + build_wheels: + needs: [setup] + name: Build pyhmmer ${{ inputs.version || '0.12.3' }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + # Matches the interpreters upstream actually publishes wheels for + # (cp38/cp39/cp310/cp311, then a single cp312-abi3 build, then the + # free-threaded cp314t), minus cp38: manylinux_2_39_riscv64 has no + # cp38 interpreter to build it with. cp313/cp314 aren't listed + # separately since they load the cp312-abi3 wheel already built. + python: ["cp39", "cp310", "cp311", "cp312", "cp314", "cp314t"] + + steps: + - name: Checkout pyhmmer v${{ env.PYHMMER_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: althonos/pyhmmer + ref: v${{ env.PYHMMER_VERSION }} + 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 pyhmmer source + run: git apply python-wheels/patches/pyhmmer/${{ env.PYHMMER_VERSION }}/*.patch + + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + output-dir: wheelhouse/ + only: ${{ matrix.python }}-manylinux_riscv64 + env: + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + # psutil (pyhmmer's only runtime dependency) has no riscv64 wheel on + # PyPI; our registry already carries it. + CIBW_ENVIRONMENT: PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pyhmmer-${{ env.PYHMMER_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish pyhmmer ${{ inputs.version || '0.12.3' }} + needs: [setup, build_wheels] + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + with: + artifact-pattern: pyhmmer-${{ inputs.version || '0.12.3' }}-*-manylinux_riscv64 diff --git a/patches/pyhmmer/0.12.3/0001-CMake-add-a-portable-SIMDe-based-SSE-fallback-for-ri.patch b/patches/pyhmmer/0.12.3/0001-CMake-add-a-portable-SIMDe-based-SSE-fallback-for-ri.patch new file mode 100644 index 000000000..fb422e09c --- /dev/null +++ b/patches/pyhmmer/0.12.3/0001-CMake-add-a-portable-SIMDe-based-SSE-fallback-for-ri.patch @@ -0,0 +1,85 @@ +From 9976743d8d3398eefced9abe15d9ced114d19faf Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Tue, 8 Sep 2026 06:01:51 +0200 +Subject: [PATCH 1/3] CMake: add a portable SIMDe-based SSE fallback for + riscv64 (and any other unrecognized architecture) + +HMMER/Easel's fast profile-HMM filters (MSVFilter, ViterbiFilter, the +striped Forward/Backward implementation) only exist as hand-written +SSE, NEON and AltiVec/VMX kernels; there is no portable scalar +implementation of the optimized P7_OPROFILE/P7_OMX pipeline that +pyhmmer's Cython bindings wrap (the "generic" P7 algorithms in +vendor/hmmer/src/generic_*.c are a separate, unoptimized reference +implementation used for calibration/testing, not what Pipeline +dispatches to). CMakeLists.txt's SIMD-detection cascade reflects +this: it hard-fails with a FATAL_ERROR on any architecture where none +of SSE2/NEON/VMX is available, which is every riscv64 host. + +Rather than leave riscv64 unbuildable, fetch SIMDe (a header-only, +BSD-licensed library purpose-built for this: it translates x86 +SSE/SSE2/SSE3 intrinsics to a portable C implementation, and is +tested in SIMDe's own CI on riscv64) and reuse the SSE implementation +on any architecture where no native backend is detected. Every +intrinsic the impl_sse sources and esl_sse.[ch] use resolves at the +SSE/SSE2 level (eslENABLE_SSE4, which would need SSE4.1, is never +turned on for this path since the host has no FindSSE4 match to +begin with), so a single generated shim header directory that +redirects /// to + (which itself pulls in SIMDe's sse.h/sse2.h), with +SIMDE_ENABLE_NATIVE_ALIASES so no call site needs to change, is +enough. The shim lives entirely in the build directory rather than +patching the vendored sources, because pyhmmer's own +patches/impl_sse/*.c.patch apply against them by absolute line number +(src/hmmer/CMakeLists.txt) and would mis-apply if those files' line +counts changed. + +Upstream-Status: Inappropriate [SIMDe is a third-party portability shim we chose for our riscv64 build; upstream may prefer a native RVV backend or no riscv64 support at all, and hasn't been asked] +--- + CMakeLists.txt | 31 ++++++++++++++++++++++++++++++- + 1 file changed, 30 insertions(+), 1 deletion(-) + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 0aed183..308b142 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -32,7 +32,36 @@ elseif(HAVE_VMX) + set(HMMER_IMPL "VMX") + string(REPLACE " " ";" IMPL_FLAGS "${VMX_C_FLAGS}") + else() +- message(FATAL_ERROR "Unsupported platform, none of SSE2, NEON or AltiVec available.") ++ # None of SSE2, NEON or AltiVec are available natively (e.g. riscv64). ++ # Reuse the SSE implementation, and satisfy the x86 intrinsics headers ++ # it includes (all at the SSE/SSE2/SSE3 level) with SIMDe's portable ++ # translation instead, by shimming those header names to redirect to ++ # it. This is done with a generated include directory rather than by ++ # editing the vendored .c/.h files, because pyhmmer's own ++ # patches/impl_sse/*.c.patch apply against those files by absolute ++ # line number at build time (src/hmmer/CMakeLists.txt) and would ++ # silently mis-apply if the files' line counts changed. ++ include(FetchContent) ++ FetchContent_Declare( ++ simde ++ GIT_REPOSITORY https://github.com/simd-everywhere/simde.git ++ GIT_TAG 71fd833d9666141edcd1d3c109a80e228303d8d7 # v0.8.2 ++ GIT_SHALLOW true ++ ) ++ FetchContent_MakeAvailable(simde) ++ ++ set(SIMDE_SHIM_DIR "${CMAKE_CURRENT_BINARY_DIR}/simde-shim-include") ++ file(MAKE_DIRECTORY "${SIMDE_SHIM_DIR}") ++ foreach(_hdr IN ITEMS xmmintrin.h emmintrin.h pmmintrin.h x86intrin.h) ++ file(WRITE "${SIMDE_SHIM_DIR}/${_hdr}" ++ "#define SIMDE_ENABLE_NATIVE_ALIASES\n#include \n") ++ endforeach() ++ include_directories(BEFORE "${SIMDE_SHIM_DIR}" "${simde_SOURCE_DIR}") ++ ++ add_compile_definitions(HMMER_VECTOR_SIMDE) ++ set(eslENABLE_SSE true) ++ set(HMMER_IMPL "SSE") ++ set(IMPL_FLAGS "") + endif() + + foreach(flag IN LISTS IMPL_FLAGS) +-- +2.50.1 (Apple Git-155) + diff --git a/patches/pyhmmer/0.12.3/0002-easel-don-t-compile-x86-CPUID-assembly-when-SSE-is-r.patch b/patches/pyhmmer/0.12.3/0002-easel-don-t-compile-x86-CPUID-assembly-when-SSE-is-r.patch new file mode 100644 index 000000000..9b512e703 --- /dev/null +++ b/patches/pyhmmer/0.12.3/0002-easel-don-t-compile-x86-CPUID-assembly-when-SSE-is-r.patch @@ -0,0 +1,100 @@ +From 35676e94c3c2fff8ad578b136205f99c929d7ccb Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Tue, 8 Sep 2026 06:02:05 +0200 +Subject: [PATCH 2/3] easel: don't compile x86 CPUID assembly when SSE is + routed through SIMDe on a non-x86 host + +esl_cpu.c's cpu_run_id()/cpu_has_sse() use raw `__asm__("cpuid" ...)` +whenever eslENABLE_SSE is defined, on the assumption that Easel's SSE +vector code can only ever be compiled on real x86/x86-64 hardware. +That assumption breaks once eslENABLE_SSE is turned on for a SIMDe- +emulated build on another architecture (riscv64): the inline asm +does not assemble there, so the build fails. + +None of the compiled sources actually call esl_cpu_has_sse() (only +esl_avx512.c calls the AVX-512 counterpart, and that file is never +part of this build), so this diagnostic path is dead weight on the +riscv64/SIMDe configuration; skip compiling the CPUID-dependent +functions whenever HMMER_VECTOR_SIMDE is set, and have +esl_cpu_has_sse() fall back to its existing "not supported" return of +0, same as when eslENABLE_SSE isn't defined at all. + +Upstream-Status: To upstream [a real portability gap in esl_cpu.c independent of our SIMDe choice, but not yet submitted -- needs review from Easel/HMMER maintainers on whether they even want a non-x86 SSE-compatible path] +--- + vendor/easel/esl_cpu.c | 22 +++++++++++++--------- + 1 file changed, 13 insertions(+), 9 deletions(-) + +diff --git a/vendor/easel/esl_cpu.c b/vendor/easel/esl_cpu.c +index 0957831..66655b2 100644 +--- a/vendor/easel/esl_cpu.c ++++ b/vendor/easel/esl_cpu.c +@@ -25,10 +25,14 @@ + #include "esl_cpu.h" + + /* declarations of static functions that come in section (2) */ +-#if defined(eslENABLE_SSE) || defined(eslENABLE_SSE4) || defined(eslENABLE_AVX) || defined(eslENABLE_AVX512) ++/* eslENABLE_SSE can be set with the SSE code routed through SIMDe on a ++ * non-x86 host (HMMER_VECTOR_SIMDE); CPUID is real x86 assembly, so it ++ * only makes sense to declare/call it when we are compiling for x86. ++ */ ++#if (defined(eslENABLE_SSE) || defined(eslENABLE_SSE4) || defined(eslENABLE_AVX) || defined(eslENABLE_AVX512)) && !defined(HMMER_VECTOR_SIMDE) + static void cpu_run_id(uint32_t eax, uint32_t ecx, uint32_t *abcd); + #endif +-#ifdef eslENABLE_SSE ++#if defined(eslENABLE_SSE) && !defined(HMMER_VECTOR_SIMDE) + static int cpu_has_sse(void); + #endif + #ifdef eslENABLE_SSE4 +@@ -65,7 +69,7 @@ static int cpu_has_avx512(void); + int + esl_cpu_has_sse(void) + { +-#ifdef eslENABLE_SSE ++#if defined(eslENABLE_SSE) && !defined(HMMER_VECTOR_SIMDE) + static int sse_support = -1; + if (sse_support < 0) + sse_support = cpu_has_sse(); +@@ -185,7 +189,7 @@ esl_cpu_Get(void) + * 2. Internal code used in x86 vector code checks + *****************************************************************/ + +-#if defined(eslENABLE_SSE) || defined(eslENABLE_SSE4) || defined(eslENABLE_AVX) || defined(eslENABLE_AVX512) ++#if (defined(eslENABLE_SSE) || defined(eslENABLE_SSE4) || defined(eslENABLE_AVX) || defined(eslENABLE_AVX512)) && !defined(HMMER_VECTOR_SIMDE) + /* cpu_run_id() + * + * Bit flags in EAX (and maybe ECX) registers specify the information +@@ -213,8 +217,8 @@ cpu_run_id(uint32_t eax, uint32_t ecx, uint32_t *abcd) + #endif + abcd[0] = eax; abcd[1] = ebx; abcd[2] = ecx; abcd[3] = edx; + #endif // ! _MSC_VER +-} +-#endif // eslENABLE_SSE | eslENABLE_SSE4 | eslENABLE_AVX | eslENABLE_AVX512 ++} ++#endif // (eslENABLE_SSE | eslENABLE_SSE4 | eslENABLE_AVX | eslENABLE_AVX512) && !HMMER_VECTOR_SIMDE + + + +@@ -271,9 +275,9 @@ cpu_check_xcr0_zmm(void) + #endif + + +-#ifdef eslENABLE_SSE ++#if defined(eslENABLE_SSE) && !defined(HMMER_VECTOR_SIMDE) + /* cpu_has_sse() +- * ++ * + * Test whether processor supports SSE/SSE2 instructions. + * Note that Easel's "SSE" vector code means SSE+SSE2. + */ +@@ -289,7 +293,7 @@ cpu_has_sse(void) + return 0; + return 1; + } +-#endif // eslENABLE_SSE ++#endif // eslENABLE_SSE && !HMMER_VECTOR_SIMDE + + + #ifdef eslENABLE_SSE4 +-- +2.50.1 (Apple Git-155) + diff --git a/patches/pyhmmer/0.12.3/0003-package-the-licences-of-the-vendored-HMMER-and-Easel.patch b/patches/pyhmmer/0.12.3/0003-package-the-licences-of-the-vendored-HMMER-and-Easel.patch new file mode 100644 index 000000000..439e66259 --- /dev/null +++ b/patches/pyhmmer/0.12.3/0003-package-the-licences-of-the-vendored-HMMER-and-Easel.patch @@ -0,0 +1,38 @@ +From cedd3c809378cbb2a07a9388d0ba365559d1fb3c Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Tue, 8 Sep 2026 06:02:16 +0200 +Subject: [PATCH 3/3] package the licences of the vendored HMMER and Easel + sources + +pyhmmer statically compiles vendor/hmmer and vendor/easel into +libhmmer/libeasel, which get linked into the pyhmmer.plan7/daemon +extension modules (and installed standalone under pyhmmer.libs). +Both are BSD-licensed (HMMER: 3-clause, Easel: 2-clause) and require +their copyright notice to travel with binary redistributions, but +pyproject.toml's license-files names only pyhmmer's own COPYING, so +the wheel ships pyhmmer's MIT notice alone even though its own +license expression already declares "MIT AND BSD-3-Clause AND +BSD-2-Clause". Add both vendored LICENSE files so they land in +.dist-info/licenses/ alongside COPYING. + +Upstream-Status: To upstream [not yet submitted; the same gap exists in every pyhmmer wheel on PyPI today, so this needs a maintainer decision rather than a drive-by PR] +--- + pyproject.toml | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/pyproject.toml b/pyproject.toml +index d7e874a..61c6f5d 100644 +--- a/pyproject.toml ++++ b/pyproject.toml +@@ -10,7 +10,7 @@ description = "Cython bindings and Python interface to HMMER3." + readme = "README.md" + requires-python = ">=3.7" + license = "MIT AND BSD-3-Clause AND BSD-2-Clause" +-license-files = ["COPYING"] ++license-files = ["COPYING", "vendor/hmmer/LICENSE", "vendor/easel/LICENSE"] + authors = [ + { name = "Martin Larralde", email = "martin.larralde@embl.de" }, + ] +-- +2.50.1 (Apple Git-155) +