From 8b2770e7c5df5773a555a81791fc19622a34b156 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Wed, 14 Oct 2026 06:04:07 +0000 Subject: [PATCH 1/3] xtcocotools: Add version 1.14.3 --- .github/workflows/build-xtcocotools.yml | 129 ++++++++++++++++++ docs/packages/xtcocotools.yaml | 6 + ...sk-use-NPY_ARRAY_OWNDATA-for-numpy-2.patch | 50 +++++++ 3 files changed, 185 insertions(+) create mode 100644 .github/workflows/build-xtcocotools.yml create mode 100644 docs/packages/xtcocotools.yaml create mode 100644 patches/xtcocotools/1.14.3/0001-mask-use-NPY_ARRAY_OWNDATA-for-numpy-2.patch diff --git a/.github/workflows/build-xtcocotools.yml b/.github/workflows/build-xtcocotools.yml new file mode 100644 index 000000000000..76f8a4f3dec2 --- /dev/null +++ b/.github/workflows/build-xtcocotools.yml @@ -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 diff --git a/docs/packages/xtcocotools.yaml b/docs/packages/xtcocotools.yaml new file mode 100644 index 000000000000..28c3047332c4 --- /dev/null +++ b/docs/packages/xtcocotools.yaml @@ -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 diff --git a/patches/xtcocotools/1.14.3/0001-mask-use-NPY_ARRAY_OWNDATA-for-numpy-2.patch b/patches/xtcocotools/1.14.3/0001-mask-use-NPY_ARRAY_OWNDATA-for-numpy-2.patch new file mode 100644 index 000000000000..dd7726c7b74f --- /dev/null +++ b/patches/xtcocotools/1.14.3/0001-mask-use-NPY_ARRAY_OWNDATA-for-numpy-2.patch @@ -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] = 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] = 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] = 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 ): From 235b11b6544f9fb51b9fb5d14a614847b1020cc8 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Thu, 15 Oct 2026 12:17:02 +0000 Subject: [PATCH 2/3] xtcocotools: patch get_version for pip's metadata-prep step setup.py's get_version() only returns short_version when 'sdist' or 'bdist_wheel' is literally in sys.argv, else it reads __version__, which version.py never defines. A modern pip calls prepare_metadata_for_build_wheel first with 'dist_info' in sys.argv, hits that else branch, and the build fails with KeyError: '__version__' before any compilation happens - which is why cp313/cp314/cp314t failed here while cp312 (an older bundled pip that skips straight to bdist_wheel) did not. Broadening the check to 'dist_info'/'egg_info' too makes it match upstream's own intent on any pip version. --- ...ersion-handle-dist_info-and-egg_info.patch | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 patches/xtcocotools/1.14.3/0002-setup-get_version-handle-dist_info-and-egg_info.patch diff --git a/patches/xtcocotools/1.14.3/0002-setup-get_version-handle-dist_info-and-egg_info.patch b/patches/xtcocotools/1.14.3/0002-setup-get_version-handle-dist_info-and-egg_info.patch new file mode 100644 index 000000000000..a71beb3979e2 --- /dev/null +++ b/patches/xtcocotools/1.14.3/0002-setup-get_version-handle-dist_info-and-egg_info.patch @@ -0,0 +1,28 @@ +Upstream-Status: To upstream [not submitted from this automated port run; needs a pull request against jin-s13/xtcocoapi] + +get_version() only returns version.py's short_version when 'sdist' or +'bdist_wheel' is in sys.argv, and otherwise falls back to __version__, which +version.py does not define. That else branch is dead code under upstream's +own usage (always a literal `python setup.py sdist`/`bdist_wheel`), but a +modern pip first calls prepare_metadata_for_build_wheel with 'dist_info' (not +'bdist_wheel') in sys.argv, hitting the else branch and raising +KeyError: '__version__'. This is why the manylinux_riscv64 cp313/cp314/cp314t +legs fail at the metadata-prep step while cp312 (whose image ships an older +pip that skips straight to bdist_wheel) does not; it is not riscv64-specific +and reproduces with any sufficiently new pip on x86. + +diff --git a/setup.py b/setup.py +index 1111111..2222222 100644 +--- a/setup.py ++++ b/setup.py +@@ -7,7 +7,9 @@ def get_version(): + exec(compile(f.read(), version_file, 'exec')) + import sys + # return short version for sdist +- if 'sdist' in sys.argv or 'bdist_wheel' in sys.argv: ++ # 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 locals()['short_version'] + else: + return locals()['__version__'] From 9a307f2135cac521087812332d76c42b0489fd9c Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sun, 25 Oct 2026 17:59:16 +0000 Subject: [PATCH 3/3] xtcocotools: fix get_version patch for Python 3.13+'s locals() change The previous patch broadened the sys.argv check (sdist/bdist_wheel -> also dist_info/egg_info) but kept reading the exec'd version.py names back out via locals(). That still fails on cp313/cp314/cp314t, now with KeyError: 'short_version' instead of KeyError: '__version__': PEP 667 (Python 3.13+) makes locals() inside a function always return a fresh snapshot built from the function's static fast-locals, so names only ever created by the nested exec (never assigned anywhere in get_version's own source) never survive a later locals() call, on either branch. Reproduced locally on cp3.13/cp3.11 with a minimal extract of get_version() plus xtcocotools/version.py. Exec into an explicit namespace dict instead, which sidesteps locals() entirely and is correct on every Python version. --- ...ersion-handle-dist_info-and-egg_info.patch | 53 ++++++++++++++----- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/patches/xtcocotools/1.14.3/0002-setup-get_version-handle-dist_info-and-egg_info.patch b/patches/xtcocotools/1.14.3/0002-setup-get_version-handle-dist_info-and-egg_info.patch index a71beb3979e2..555bd4e5f1b9 100644 --- a/patches/xtcocotools/1.14.3/0002-setup-get_version-handle-dist_info-and-egg_info.patch +++ b/patches/xtcocotools/1.14.3/0002-setup-get_version-handle-dist_info-and-egg_info.patch @@ -1,28 +1,53 @@ Upstream-Status: To upstream [not submitted from this automated port run; needs a pull request against jin-s13/xtcocoapi] -get_version() only returns version.py's short_version when 'sdist' or -'bdist_wheel' is in sys.argv, and otherwise falls back to __version__, which -version.py does not define. That else branch is dead code under upstream's -own usage (always a literal `python setup.py sdist`/`bdist_wheel`), but a -modern pip first calls prepare_metadata_for_build_wheel with 'dist_info' (not -'bdist_wheel') in sys.argv, hitting the else branch and raising -KeyError: '__version__'. This is why the manylinux_riscv64 cp313/cp314/cp314t -legs fail at the metadata-prep step while cp312 (whose image ships an older -pip that skips straight to bdist_wheel) does not; it is not riscv64-specific -and reproduces with any sufficiently new pip on x86. +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 -@@ -7,7 +7,9 @@ def get_version(): - exec(compile(f.read(), version_file, 'exec')) +@@ -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 locals()['short_version'] ++ return version_locals['short_version'] else: - return locals()['__version__'] +- 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