Skip to content
Open
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
94 changes: 94 additions & 0 deletions .github/workflows/build-pyhmmer.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
From 9976743d8d3398eefced9abe15d9ced114d19faf Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
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 <xmmintrin.h>/<emmintrin.h>/<pmmintrin.h>/<x86intrin.h> to
<simde/x86/sse3.h> (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 <simde/x86/sse3.h>\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)

Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
From 35676e94c3c2fff8ad578b136205f99c929d7ccb Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
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)

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
From cedd3c809378cbb2a07a9388d0ba365559d1fb3c Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
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
<wheel>.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)

Loading