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
111 changes: 111 additions & 0 deletions .github/workflows/build-fill-voids.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# Based on: https://github.com/seung-lab/fill_voids/blob/2.1.2/.github/workflows/build_wheel.yml
name: Build fill-voids wheels (riscv64)

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

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

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

env:
FILL_VOIDS_VERSION: ${{ inputs.version || '2.1.2' }}
MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64

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

build_wheels:
needs: [setup]
name: Build fill-voids ${{ inputs.version || '2.1.2' }} ${{ matrix.python }}-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
python: [cp312, cp313, cp314, cp314t]

steps:
- name: Checkout fill-voids ${{ env.FILL_VOIDS_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: seung-lab/fill_voids
ref: ${{ env.FILL_VOIDS_VERSION }}
persist-credentials: false

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

- name: Patch fill-voids source
run: git apply python-wheels/patches/fill-voids/${{ env.FILL_VOIDS_VERSION }}/*.patch

- 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 }}
# numpy is a build-time requirement (pyproject.toml's build-system.requires,
# via cython's `cimport numpy`) and an implicit runtime one too (the compiled
# extension calls numpy's C API at import time; pbr turns requirements.txt's
# numpy/fastremap into install_requires). Only our registry has numpy for
# riscv64; only-binary keeps a newer PyPI release from winning the resolution
# and then compiling from sdist.
CIBW_ENVIRONMENT: >-
PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/
PIP_ONLY_BINARY=numpy
# fastremap has no riscv64 wheel on either index, so pip builds it from
# sdist inside the container (same as connected-components-3d).
CIBW_TEST_REQUIRES: pytest numpy scipy fastremap
# automated_test.py sits next to the unbuilt fill_voids/ source package;
# staging only the test file keeps that directory out of test_cwd so
# `import fill_voids` resolves to the installed wheel (gotcha 25).
CIBW_TEST_SOURCES: automated_test.py
CIBW_TEST_COMMAND: python -m pytest -v automated_test.py

- name: Check wheel contents
run: |
python3 - wheelhouse/*.whl <<'EOF'
import sys, zipfile
names = zipfile.ZipFile(sys.argv[1]).namelist()
exts = {n.split("/")[-1].split(".")[0] for n in names if n.endswith(".so")}
assert exts == {"fill_voids"}, exts
licences = {n.rsplit("/", 1)[-1] for n in names if ".dist-info/licenses/" in n} - {""}
assert licences == {"AUTHORS", "COPYING", "COPYING.LESSER"}, licences
EOF

- name: Store wheels
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: fill-voids-${{ env.FILL_VOIDS_VERSION }}-${{ matrix.python }}-manylinux_riscv64
path: ./wheelhouse/*.whl
if-no-files-found: error

publish:
name: Publish fill-voids ${{ inputs.version || '2.1.2' }}
needs: [setup, build_wheels]
permissions:
contents: write
pull-requests: write
uses: $/.github/workflows/_publish-wheel.yml
with:
artifact-pattern: fill-voids-${{ inputs.version || '2.1.2' }}-*-manylinux_riscv64
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Tue, 8 Sep 2026 08:00:00 +0200
Subject: [PATCH] Drop crackle-codec test data dependency and fix a
mutated-array comparison

automated_test.py loads its EM segmentation fixture through crackle.load
at module scope, so the whole file fails to collect wherever crackle-codec
has no wheel: it needs pybind11 and google_crc32c, neither of which has a
riscv64 build anywhere, and building the chain from source is out of scope
for testing this package. Since the loaded fixture is only ever used to
generate a labeled volume with per-label binary images to fill, replace it
with an equivalent synthetic volume (a 3x3x3 grid of non-overlapping
hollow cubes, each with a thick wall and its own label id) built from
numpy alone, keeping every test function and assertion unchanged.

Separately, test_scipy_comparison2d (and 3d) call fill_voids.fill(binimg,
in_place=True) and then run scipy's reference binary_fill_holes on that
same binimg variable, which the in-place call has by then mutated; both
functions already snapshot the pristine input as orig_binimg first but
never use it. Reproduced on both riscv64 and linux/aarch64 with the
synthetic volume above: comparing against the mutated binimg fails
nondeterministically depending on which label/slice runs, while comparing
against orig_binimg (351 combinations checked exhaustively, 2D and 3D)
never does. Use orig_binimg for the scipy comparison in both functions.

Upstream-Status: To upstream [repo policy prohibits opening issues/PRs on third-party repositories; the mutated-array comparison bug is real and independent of riscv64]
---
automated_test.py | 36 +++++++++++++++++++++++++++---------
1 file changed, 27 insertions(+), 9 deletions(-)

diff --git a/automated_test.py b/automated_test.py
index 47aea34..22419e0 100644
--- a/automated_test.py
+++ b/automated_test.py
@@ -4,19 +4,37 @@ import fill_voids
import scipy.ndimage
from scipy.ndimage import binary_fill_holes

-from tqdm import tqdm
-
-import crackle
import numpy as np

-img = crackle.load('test_data.npy.ckl.gz')
+# riscv64 CI has no crackle-codec wheel (nor any dependency of one) to load
+# test_data.npy.ckl.gz, so this builds an equivalent synthetic 3D label
+# volume instead: a 3x3x3 grid of non-overlapping hollow cubes, each its
+# own label id, with a 3-voxel-thick wall so no diagonal-only pinch point
+# can make scipy's 4/6-connected flood fill and fill_voids disagree on
+# whether a cavity is enclosed (a real risk with round/thin shapes at
+# pixel resolution). Gives the same shape (a labeled volume with
+# cavities) the real tests need without requiring crackle.
+def _make_test_volume(size=56):
+ img = np.zeros((size, size, size), dtype=np.uint32)
+ centers = (16, 32, 48)
+ half_outer, half_inner = 6, 3
+ label = 1
+ for cz in centers:
+ for cy in centers:
+ for cx in centers:
+ img[cz-half_outer:cz+half_outer, cy-half_outer:cy+half_outer, cx-half_outer:cx+half_outer] = label
+ img[cz-half_inner:cz+half_inner, cy-half_inner:cy+half_inner, cx-half_inner:cx+half_inner] = 0
+ label += 1
+ return img
+
+img = _make_test_volume()
SEGIDS = np.unique(img)[1:]

def test_scipy_comparison3d():
segids = np.copy(SEGIDS)
np.random.shuffle(segids)

- for segid in tqdm(segids[:10]):
+ for segid in segids[:10]:
print(segid)
binimg = (img == segid).view(np.uint8)
slices = scipy.ndimage.find_objects(binimg)[0]
@@ -28,7 +46,7 @@ def test_scipy_comparison3d():

assert np.all(fv == fvip)

- spy = binary_fill_holes(binimg)
+ spy = binary_fill_holes(orig_binimg)

assert np.all(fv == spy)

@@ -36,9 +54,9 @@ def test_scipy_comparison2d():
segids = np.copy(SEGIDS)
np.random.shuffle(segids)

- for segid in tqdm(segids[:10]):
+ for segid in segids[:10]:
print(segid)
- for z in tqdm(range(img.shape[2])):
+ for z in range(img.shape[2]):
binimg = img[:,:,z] == segid

orig_binimg = np.copy(binimg, order='F')
@@ -47,7 +65,7 @@ def test_scipy_comparison2d():

assert np.all(fv == fvip)

- spy = binary_fill_holes(binimg)
+ spy = binary_fill_holes(orig_binimg)

assert np.all(fv == spy)