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
165 changes: 165 additions & 0 deletions .github/workflows/build-igraph.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# Based on the manylinux/musllinux jobs of
# https://github.com/igraph/python-igraph/blob/1.0.0/.github/workflows/build.yml
name: Build igraph wheels (riscv64)

on:
workflow_dispatch:
inputs:
version:
description: 'igraph version to build (git tag, e.g. 1.0.0)'
required: true
default: '1.0.0'
pull_request:
paths:
- '.github/workflows/build-igraph.yml'
- 'patches/igraph/**'

concurrency:
group: ${{ github.workflow }}-${{ inputs.version || '1.0.0' }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read # to fetch code (actions/checkout)

env:
IGRAPH_VERSION: ${{ inputs.version || '1.0.0' }}
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64
MUSLLINUX_RISCV64_IMAGE: quay.io/pypa/musllinux_1_2_riscv64

jobs:
setup:
uses: $/.github/workflows/_setup.yml

build_wheels:
needs: [setup]
name: Build igraph ${{ inputs.version || '1.0.0' }} cp39-abi3-${{ matrix.libc }}_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
libc: [manylinux, musllinux]

steps:
- name: Checkout python-igraph ${{ env.IGRAPH_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: igraph/python-igraph
ref: ${{ env.IGRAPH_VERSION }}
submodules: true
# fetch-depth: 0 also governs the vendor/source/igraph submodule clone
# depth (gotcha 268): a shallow submodule has no tags for the C core's
# CMake git_describe() version detection, and it has no IGRAPH_VERSION
# file either since it isn't a release tarball.
fetch-depth: 0
persist-credentials: false

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

- name: Patch python-igraph source
run: git apply python-wheels/patches/igraph/${{ env.IGRAPH_VERSION }}/*.patch

- uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0
with:
output-dir: wheelhouse/
env:
# setup.py's bdist_wheel_abi3 always tags the wheel cp39-abi3 (gotcha 34),
# so build on that floor and let cibuildwheel reuse+retest the same wheel
# on cp312-cp314 (gotcha 96). No free-threaded wheel exists upstream either
# (their own CIBW_SKIP: cp314t-*, since Py_LIMITED_API can't target it).
CIBW_BUILD: >-
cp39-${{ matrix.libc }}_riscv64 cp312-${{ matrix.libc }}_riscv64
cp313-${{ matrix.libc }}_riscv64 cp314-${{ matrix.libc }}_riscv64
CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }}
CIBW_MUSLLINUX_RISCV64_IMAGE: ${{ env.MUSLLINUX_RISCV64_IMAGE }}
CIBW_BEFORE_BUILD_LINUX: |
if command -v apk > /dev/null; then
apk add flex bison libxml2-dev zlib-dev cairo-dev
elif command -v yum > /dev/null; then
yum install -y flex bison libxml2-devel zlib-devel cairo-devel
fi
pip install -U cmake pip setuptools wheel
python setup.py build_c_core
CIBW_ENVIRONMENT: PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/
CIBW_ENVIRONMENT_PASS_LINUX: PYTEST_TIMEOUT
PYTEST_TIMEOUT: 60
# numpy/scipy/pandas/matplotlib/Pillow (upstream's `test` extra) have no
# riscv64 wheel below cp312 on our registry; `test-musl` needs none of
# them, so only the manylinux cp39 leg loses test coverage.
CIBW_TEST_SKIP: cp39-manylinux_riscv64
# test_labels renders vertex labels and diffs against a baseline PNG
# (matplotlib image_comparison, tol=4.0); the riscv64 image's font stack
# anti-aliases just enough differently to push RMS to 6.084 (gotcha 282).
#
# testHubScore's vendored-ARPACK non-convergence on musllinux only (not
# manylinux) is upstream's own known ARPACK starting-vector flake
# (igraph/python-igraph#379, #728), not riscv64-specific -- deselect on
# musllinux only (gotcha 286).
CIBW_TEST_COMMAND: >-
cd {project} &&
pip install --prefer-binary ".[${{ matrix.libc == 'musllinux' && 'test-musl' || 'test' }}]" &&
python -m pytest -v tests --deselect tests/drawing/matplotlib/test_graph.py::GraphTestRunner::test_labels
${{ matrix.libc == 'musllinux' && '--deselect tests/test_atlas.py::GraphAtlasTests::testHubScore' || '' }}

- name: Check the wheel is abi3 and carries the compiled extension + LICENSE
run: |
python3 - wheelhouse/*.whl <<'EOF'
import sys, zipfile
for whl in sys.argv[1:]:
assert "-cp39-abi3-" in whl, whl
names = zipfile.ZipFile(whl).namelist()
assert any(n.endswith("_igraph.abi3.so") for n in names), names
licenses = {n.rsplit("/", 1)[1] for n in names if ".dist-info/licenses/" in n} - {""}
assert licenses == {"LICENSE"}, (whl, licenses)
print(whl, "ok")
EOF

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: igraph-${{ env.IGRAPH_VERSION }}-cp39-abi3-${{ matrix.libc }}_riscv64
path: wheelhouse/*.whl
if-no-files-found: error

gpl_sources:
needs: [setup]
name: Collect GPL sources
runs-on: ubuntu-24.04-riscv

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

# plfit (vendored inside the igraph C core) links OpenMP when available, so
# auditwheel vendors the image's libgomp into igraph.libs/.
- uses: ./actions/collect-gpl-sources
with:
image: ${{ env.MANYLINUX_RISCV64_IMAGE }}
packages: gcc
output: gpl-sources.tar

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: igraph-${{ env.IGRAPH_VERSION }}-gpl-sources
path: gpl-sources.tar
if-no-files-found: error

publish:
name: Publish igraph ${{ inputs.version || '1.0.0' }}
needs: [setup, build_wheels, gpl_sources]
permissions:
contents: write
pull-requests: write
uses: $/.github/workflows/_publish-wheel.yml
with:
artifact-pattern: igraph-${{ inputs.version || '1.0.0' }}-*riscv64
gpl-sources-artifact: igraph-${{ inputs.version || '1.0.0' }}-gpl-sources
gpl-sources-description: gcc
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Sun, 6 Sep 2026 16:13:57 +0200
Subject: [PATCH] Give internal PyLong/PyUnicode helpers non-exported linkage

Upstream-Status: To upstream [not yet submitted; upstream's own CI does not run abi3audit so this leak is invisible to them]

PyLong_AsInt_OutArg() and PyLong_to_integer_t() in convert.c are
declared with default (extern) visibility but are only ever called
from within convert.c itself; PyUnicode_CopyAsString() and
PyUnicode_IsEqualToUTF8String() in pyhelpers.c are declared in the
shared pyhelpers.h and called across several translation units, so
they need internal-to-the-library linkage rather than none at all.

Because the extension is built with Py_LIMITED_API (setup.py sets
py_limited_api=True), abi3audit --strict flags all four as non-abi3
symbols leaking from _igraph.abi3.so: their names collide with real
(non-limited-API) CPython C-API symbols, so a wheel claiming forward
ABI compatibility must not export them. Only PyInit__igraph needs to
be visible from the shared object. Mark the two convert.c-only
helpers static, and give the two pyhelpers.c ones hidden visibility
via __attribute__((visibility("hidden"))) since they are still
called from other translation units within the extension.

Signed-off-by: Ludovic Henry <git@ludovic.dev>
---
src/_igraph/convert.c | 4 ++--
src/_igraph/pyhelpers.c | 2 ++
src/_igraph/pyhelpers.h | 2 ++
3 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c
index a36d055..795b1f7 100644
--- a/src/_igraph/convert.c
+++ b/src/_igraph/convert.c
@@ -52,7 +52,7 @@
*
* Returns -1 if there was an error, 0 otherwise.
*/
-int PyLong_AsInt_OutArg(PyObject* obj, int* result) {
+static int PyLong_AsInt_OutArg(PyObject* obj, int* result) {
long dummy = PyLong_AsLong(obj);
if (dummy < INT_MIN) {
PyErr_SetString(PyExc_OverflowError, "long integer too small for conversion to C int");
@@ -962,7 +962,7 @@ int igraphmodule_PyObject_to_igraph_t(PyObject *o, igraph_t **result) {
* \param v the result is stored here
* \return 0 if everything was OK, 1 otherwise
*/
-int PyLong_to_integer_t(PyObject* obj, igraph_int_t* v) {
+static int PyLong_to_integer_t(PyObject* obj, igraph_int_t* v) {
if (IGRAPH_INTEGER_SIZE == 64) {
/* here the assumption is that sizeof(long long) == 64 bits; anyhow, this
* is the widest integer type that we can convert a PyLong to so we cannot
diff --git a/src/_igraph/pyhelpers.c b/src/_igraph/pyhelpers.c
index 6f0afaf..aab8833 100644
--- a/src/_igraph/pyhelpers.c
+++ b/src/_igraph/pyhelpers.c
@@ -166,6 +166,7 @@ PyObject* igraphmodule_PyRange_create(Py_ssize_t start, Py_ssize_t stop, Py_ssiz
return result;
}

+__attribute__((visibility("hidden")))
char* PyUnicode_CopyAsString(PyObject* string) {
PyObject* bytes;
char* result;
@@ -196,6 +197,7 @@ char* PyUnicode_CopyAsString(PyObject* string) {
return result;
}

+__attribute__((visibility("hidden")))
int PyUnicode_IsEqualToUTF8String(PyObject* py_string,
const char* c_string) {
PyObject* c_string_conv;
diff --git a/src/_igraph/pyhelpers.h b/src/_igraph/pyhelpers.h
index 68d63d0..c40e43f 100644
--- a/src/_igraph/pyhelpers.h
+++ b/src/_igraph/pyhelpers.h
@@ -34,12 +34,14 @@ PyObject* igraphmodule_PyList_NewFill(Py_ssize_t len, PyObject* item);
PyObject* igraphmodule_PyList_Zeroes(Py_ssize_t len);
char* igraphmodule_PyObject_ConvertToCString(PyObject* string);
PyObject* igraphmodule_PyRange_create(Py_ssize_t start, Py_ssize_t stop, Py_ssize_t step);
+__attribute__((visibility("hidden")))
int PyUnicode_IsEqualToUTF8String(PyObject* py_string, const char* c_string);
long igraphmodule_Py_HashPointer(void *p);

#define PyBaseString_Check(o) (PyUnicode_Check(o) || PyBytes_Check(o))
#define PyUnicode_IsEqualToASCIIString(uni, string) \
(PyUnicode_CompareWithASCIIString(uni, string) == 0)
+__attribute__((visibility("hidden")))
char* PyUnicode_CopyAsString(PyObject* string);

#define PY_IGRAPH_ASSERT_AT_BUILD_TIME(condition) \