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
129 changes: 129 additions & 0 deletions .github/workflows/build-xtcocotools.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# Based on the `build-n-publish-linux` job of
# https://github.com/jin-s13/xtcocoapi/blob/v1.14.3/.github/workflows/publish-to-pypi.yml
name: Build xtcocotools wheels (riscv64)

on:
workflow_dispatch:
inputs:
version:
description: 'Version glob to (re)build; empty builds every version of docs/packages/xtcocotools.yaml not released yet'
required: false
default: ''
pull_request:
branches: [main]
paths:
- '.github/workflows/build-xtcocotools.yml'
- 'docs/packages/xtcocotools.yaml'
- 'patches/xtcocotools/**'
push:
branches: [main]
paths:
- '.github/workflows/build-xtcocotools.yml'
- 'docs/packages/xtcocotools.yaml'
- 'patches/xtcocotools/**'

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: xtcocotools
version: ${{ inputs.version }}

build_wheels:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
name: Build xtcocotools ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
python: ["cp312", "cp313", "cp314", "cp314t"]

env:
XTCOCOTOOLS_VERSION: ${{ matrix.version }}

steps:
- name: Checkout xtcocoapi ${{ env.XTCOCOTOOLS_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: jin-s13/xtcocoapi
ref: v${{ env.XTCOCOTOOLS_VERSION }}
persist-credentials: false

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

- name: Patch xtcocoapi source
run: git apply python-wheels/patches/xtcocotools/${{ env.XTCOCOTOOLS_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 }}
CIBW_ENVIRONMENT: >-
PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/
PIP_ONLY_BINARY=numpy,matplotlib,contourpy,kiwisolver,pillow
# setup.py has no build-system table and imports numpy at top level;
# upstream installs requirements.txt into the build env first.
CIBW_BEFORE_BUILD: pip install -r {project}/requirements.txt wheel
CIBW_BUILD_FRONTEND: "pip; args: --no-build-isolation"
# Upstream has no test suite; its demos are the only runnable checks.
CIBW_TEST_COMMAND: >-
cd {project}/demos &&
python demo_coco.py &&
python demo_coco_wholebody.py &&
python demo_crowdpose.py &&
python -c "import numpy as np; from xtcocotools import mask; m = np.asfortranarray(np.tri(8, dtype=np.uint8)[:, :, None]); assert (mask.decode(mask.encode(m)) == m).all(); r = mask.encode(m); assert mask.area(r)[0] == 36; assert (mask.toBbox(r) == [[0, 0, 8, 8]]).all(); assert mask.iou(r, r, [0])[0, 0] == 1"

- name: Verify the wheel ships the compiled extension and licence
run: |
python3 - wheelhouse/*.whl <<'EOF'
import sys, zipfile
names = zipfile.ZipFile(sys.argv[1]).namelist()
assert any(n.startswith("xtcocotools/_mask.") and n.endswith(".so") for n in names), names
assert any(n.endswith(".dist-info/licenses/LICENSE") for n in names), names
print(sys.argv[1], "ok")
EOF

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

publish:
name: Publish xtcocotools ${{ matrix.version }}
needs: [setup, build_wheels]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
permissions:
contents: write
pull-requests: write
uses: $/.github/workflows/_publish-wheel.yml
secrets:
app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }}
with:
artifact-pattern: xtcocotools-${{ matrix.version }}-*-manylinux_riscv64
6 changes: 6 additions & 0 deletions docs/packages/xtcocotools.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package-name: xtcocotools
source-code: https://github.com/jin-s13/xtcocoapi
license: MIT AND BSD-2-Clause
versions:
- version: 1.14.3
patched: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
Upstream-Status: To upstream [not submitted from this automated port run; needs a pull request against jin-s13/xtcocoapi]

NumPy 2 removed the deprecated NPY_OWNDATA flag alias from its Cython
declarations (numpy/__init__.pxd). Cython then resolves np.NPY_OWNDATA as a
Python attribute at run time, so every mask function that hands a C buffer
to an ndarray (decode, area, iou, toBbox) raises
AttributeError: module 'numpy' has no attribute 'NPY_OWNDATA'.
NPY_ARRAY_OWNDATA is the same flag and is declared by NumPy 1.x as well.
pycocotools carries the identical change (ppwwyyxx/cocoapi).

diff --git a/xtcocotools/_mask.pyx b/xtcocotools/_mask.pyx
index 1b55292..428e5ec 100644
--- a/xtcocotools/_mask.pyx
+++ b/xtcocotools/_mask.pyx
@@ -96,7 +96,7 @@ cdef class Masks:
# Create a 1D array, and reshape it to fortran/Matlab column-major array
ndarray = np.PyArray_SimpleNewFromData(1, shape, np.NPY_UINT8, self._mask).reshape((self._h, self._w, self._n), order='F')
# The _mask allocated by Masks is now handled by ndarray
- PyArray_ENABLEFLAGS(ndarray, np.NPY_OWNDATA)
+ PyArray_ENABLEFLAGS(ndarray, np.NPY_ARRAY_OWNDATA)
return ndarray

# internal conversion from Python RLEs object to compressed RLE format
@@ -164,7 +164,7 @@ def area(rleObjs):
shape[0] = <np.npy_intp> Rs._n
a = np.array((Rs._n, ), dtype=np.uint8)
a = np.PyArray_SimpleNewFromData(1, shape, np.NPY_UINT32, _a)
- PyArray_ENABLEFLAGS(a, np.NPY_OWNDATA)
+ PyArray_ENABLEFLAGS(a, np.NPY_ARRAY_OWNDATA)
return a

# iou computation. support function overload (RLEs-RLEs and bbox-bbox).
@@ -234,7 +234,7 @@ def iou( dt, gt, pyiscrowd ):
iou = np.zeros((m*n, ), dtype=np.double)
shape[0] = <np.npy_intp> m*n
iou = np.PyArray_SimpleNewFromData(1, shape, np.NPY_DOUBLE, _iou)
- PyArray_ENABLEFLAGS(iou, np.NPY_OWNDATA)
+ PyArray_ENABLEFLAGS(iou, np.NPY_ARRAY_OWNDATA)
_iouFun(dt, gt, iscrowd, m, n, iou)
return iou.reshape((m,n), order='F')

@@ -247,7 +247,7 @@ def toBbox( rleObjs ):
shape[0] = <np.npy_intp> 4*n
bb = np.array((1,4*n), dtype=np.double)
bb = np.PyArray_SimpleNewFromData(1, shape, np.NPY_DOUBLE, _bb).reshape((n, 4))
- PyArray_ENABLEFLAGS(bb, np.NPY_OWNDATA)
+ PyArray_ENABLEFLAGS(bb, np.NPY_ARRAY_OWNDATA)
return bb

def frBbox(np.ndarray[np.double_t, ndim=2] bb, siz h, siz w ):
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
Upstream-Status: To upstream [not submitted from this automated port run; needs a pull request against jin-s13/xtcocoapi]

get_version() execs version.py and then reads its names back via locals().
Two separate bugs combine here. (1) It only takes the short_version branch
when 'sdist' or 'bdist_wheel' is literally in sys.argv, and otherwise reads
__version__ from the else branch; a modern pip calls
prepare_metadata_for_build_wheel with 'dist_info' in sys.argv, not
'bdist_wheel', hitting that else branch. (2) More fundamentally, the exec+
locals() pattern itself breaks under PEP 667 (Python 3.13+): locals() inside
a function is now always a fresh snapshot built from the function's static
fast-locals, so names that are only ever created by the exec'd code (never
assigned anywhere in get_version's own source, on either branch) are not
visible through a later locals() call at all, independent of sys.argv. That
is why cp313/cp314/cp314t fail at the metadata-prep step while cp312 (an
older Python without PEP 667) does not; it is not riscv64-specific and
reproduces with any 3.13+ interpreter on any arch, with any sufficiently new
pip. Fixing only (1), as this patch originally did, still failed here with
KeyError: 'short_version' instead of KeyError: '__version__' - same root
cause, different branch. Execing into an explicit namespace dict sidesteps
locals() entirely and fixes both.

diff --git a/setup.py b/setup.py
index 1111111..2222222 100644
--- a/setup.py
+++ b/setup.py
@@ -4,14 +4,23 @@
version_file = 'xtcocotools/version.py'

def get_version():
+ # Exec into an explicit namespace dict rather than relying on locals():
+ # under PEP 667 (Python 3.13+), locals() inside a function is always a
+ # fresh snapshot, so names only ever created by this exec (never assigned
+ # in get_version's own source) don't survive a later locals() call, on
+ # either branch below. Reproduces on any 3.13+ interpreter, independent
+ # of architecture.
+ version_locals = {}
with open(version_file, 'r') as f:
- exec(compile(f.read(), version_file, 'exec'))
+ exec(compile(f.read(), version_file, 'exec'), version_locals)
import sys
# return short version for sdist
- if 'sdist' in sys.argv or 'bdist_wheel' in sys.argv:
- return locals()['short_version']
+ # pip's build backend also runs prepare_metadata_for_build_wheel with
+ # 'dist_info'/'egg_info' in sys.argv, not 'bdist_wheel'; treat the same.
+ if {'sdist', 'bdist_wheel', 'dist_info', 'egg_info'} & set(sys.argv):
+ return version_locals['short_version']
else:
- return locals()['__version__']
+ return version_locals['__version__']

def parse_requirements(fname='requirements.txt', with_version=True):
"""Parse the package dependencies listed in a requirements file but strips
Loading