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 ): 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..555bd4e5f1b9 --- /dev/null +++ b/patches/xtcocotools/1.14.3/0002-setup-get_version-handle-dist_info-and-egg_info.patch @@ -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