From 799963cec1a37c919e3dc4faf59f8507e9250537 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 19 Sep 2026 07:49:50 +0000 Subject: [PATCH 1/5] mediapipe: Add version 1.0.1 mediapipe publishes no sdist and tags no release; PyPI's 1.0.1 wheels come from commit 02d83cb8, where mediapipe/version.bzl reads 1.0.1. The build mirrors upstream's Dockerfile.manylinux_2_28_x86_64 recipe: a static core+imgproc OpenCV 4.10, then setup.py bdist_wheel driving bazel to build //mediapipe/tasks/c:libmediapipe.so with the GPU disabled. bazel publishes no riscv64 binary, so it is bootstrapped from the dist archive in a cached job, and the wheel is retagged py3-none to match the interpreter-agnostic artifact upstream publishes. --- .github/workflows/build-mediapipe.yml | 273 ++++++++++++++++++++++++++ docs/packages/mediapipe.yaml | 5 + 2 files changed, 278 insertions(+) create mode 100644 .github/workflows/build-mediapipe.yml create mode 100644 docs/packages/mediapipe.yaml diff --git a/.github/workflows/build-mediapipe.yml b/.github/workflows/build-mediapipe.yml new file mode 100644 index 00000000000..ca8ef841059 --- /dev/null +++ b/.github/workflows/build-mediapipe.yml @@ -0,0 +1,273 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# mediapipe tags no release and publishes no sdist; PyPI's 1.0.1 wheels were +# built from commit 02d83cb8, where mediapipe/version.bzl reads 1.0.1 and which +# is the last commit before the upload. The build follows upstream's own +# manylinux recipe: +# https://github.com/google/mediapipe/blob/master/Dockerfile.manylinux_2_28_x86_64 +# https://github.com/google/mediapipe/blob/master/build_manylinux_wheel.sh +name: Build mediapipe wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/mediapipe.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-mediapipe.yml' + - 'docs/packages/mediapipe.yaml' + push: + branches: [main] + paths: + - '.github/workflows/build-mediapipe.yml' + - 'docs/packages/mediapipe.yaml' + +run-name: build-mediapipe ${{ inputs.version && format('- {0}', inputs.version) || '' }} + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +env: + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + # No upstream tag exists for this version; move together with the version in + # docs/packages/mediapipe.yaml. + MEDIAPIPE_REF: 02d83cb8eb451099dfb24c02a8784ed996a1710c + OPENCV_VERSION: '4.10.0' + # Upstream downloads a bazel release binary, which exists for no riscv64; + # 7.5.0 is the version this repo bootstraps from source. + BAZEL_VERSION: '7.5.0' + RULES_PYTHON_VERSION: '0.33.2' + RULES_JAVA_VERSION: '7.6.5' + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: mediapipe + version: ${{ inputs.version }} + + bazel: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Bootstrap bazel (riscv64) + runs-on: ubuntu-24.04-riscv + timeout-minutes: 720 + + steps: + - name: Restore bazel binary + id: cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: bazel-bin + key: bazel-${{ env.BAZEL_VERSION }}-manylinux_riscv64 + + - name: Bootstrap bazel ${{ env.BAZEL_VERSION }} + if: steps.cache.outputs.cache-hit != 'true' + run: | + mkdir -p bazel-bin + docker run --rm -i --network=host \ + -v "${GITHUB_WORKSPACE}:/work" \ + -w /work \ + -e BAZEL_VERSION \ + -e RULES_PYTHON_VERSION \ + -e RULES_JAVA_VERSION \ + "${MANYLINUX_RISCV64_IMAGE}" \ + bash <<'SCRIPT' + set -eux + + dnf install -y --disablerepo=extras --setopt=install_weak_deps=False java-21-openjdk-devel zip unzip + JAVA_HOME="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")" + export JAVA_HOME + + # rules_python 0.33.2's PLATFORMS has no riscv64 entry, aborting the bootstrap + # (bazelbuild/bazel#23018). Any linux entry is a safe stand-in: the toolchain it names + # is never selected on a riscv64 host. Fixed in bazel 8.2.0; the 7.x backport is open. + mkdir -p /tmp/rules_python + curl -fsSLo /tmp/rules_python.tar.gz "https://github.com/bazel-contrib/rules_python/releases/download/${RULES_PYTHON_VERSION}/rules_python-${RULES_PYTHON_VERSION}.tar.gz" + tar -xzf /tmp/rules_python.tar.gz -C /tmp/rules_python --strip-components=1 + sed -i 's|fail("No platform declared for host OS {} on arch {}".format(os_name, arch))|return "x86_64-unknown-linux-gnu"|' \ + /tmp/rules_python/python/private/toolchains_repo.bzl + + # rules_java 7.x maps riscv64 to a stray-colon include path, so a JNI library + # can't find jni_md.h. Fixed in rules_java 8.x, never backported. + mkdir -p /tmp/rules_java + curl -fsSLo /tmp/rules_java.tar.gz "https://github.com/bazelbuild/rules_java/releases/download/${RULES_JAVA_VERSION}/rules_java-${RULES_JAVA_VERSION}.tar.gz" + tar -xzf /tmp/rules_java.tar.gz -C /tmp/rules_java + sed -i 's|\[":include/linux"\]|["include/linux"]|g' /tmp/rules_java/toolchains/BUILD + + mkdir -p /tmp/bazel-src + cd /tmp/bazel-src + curl -fsSLo dist.zip "https://github.com/bazelbuild/bazel/releases/download/${BAZEL_VERSION}/bazel-${BAZEL_VERSION}-dist.zip" + unzip -q dist.zip + + EXTRA_BAZEL_ARGS="--tool_java_runtime_version=local_jdk \ + --override_module=rules_python=/tmp/rules_python \ + --override_module=rules_java=/tmp/rules_java" \ + bash ./compile.sh + install -m 0755 output/bazel /work/bazel-bin/bazel + SCRIPT + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: bazel-${{ env.BAZEL_VERSION }}-riscv64 + path: bazel-bin/bazel + if-no-files-found: error + + build_wheels: + name: Build mediapipe ${{ matrix.version }} py3-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 720 + needs: [setup, bazel] + if: needs.setup.outputs.versions != '[]' + + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + + env: + MEDIAPIPE_VERSION: ${{ matrix.version }} + + steps: + - name: Checkout google/mediapipe @ ${{ env.MEDIAPIPE_REF }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: google/mediapipe + ref: ${{ env.MEDIAPIPE_REF }} + path: mediapipe + fetch-depth: 1 + persist-credentials: false + + - name: Download bazel + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: bazel-${{ env.BAZEL_VERSION }}-riscv64 + path: bazel-bin + + - name: Build wheel + run: | + mkdir -p wheelhouse + set -o pipefail + docker run --rm -i --network=host \ + -v "${GITHUB_WORKSPACE}:/work" \ + -w /work \ + -e MEDIAPIPE_VERSION \ + -e OPENCV_VERSION \ + "${MANYLINUX_RISCV64_IMAGE}" \ + bash <<'SCRIPT' 2>&1 | tee build.log + set -eux + + dnf install -y --disablerepo=extras --setopt=install_weak_deps=False java-21-openjdk-devel zip unzip + JAVA_HOME="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")" + export JAVA_HOME + install -m 0755 /work/bazel-bin/bazel /usr/local/bin/bazel + + export PYTHON_BIN="/opt/python/cp312-cp312/bin/python" + export PATH="/opt/python/cp312-cp312/bin:${PATH}" + "${PYTHON_BIN}" -m pip install -q -U pip setuptools wheel auditwheel + ln -sf "${PYTHON_BIN}" /usr/local/bin/python + ln -sf "${PYTHON_BIN}" /usr/local/bin/python3 + + git clone -q --depth 1 --branch "${OPENCV_VERSION}" https://github.com/opencv/opencv /tmp/opencv + cmake -S /tmp/opencv -B /tmp/opencv/release \ + -DCMAKE_BUILD_TYPE=RELEASE -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DBUILD_SHARED_LIBS=OFF -DBUILD_LIST=imgproc,core \ + -DWITH_ITT=OFF -DWITH_IPP=OFF -DBUILD_EXAMPLES=OFF -DBUILD_opencv_apps=OFF \ + -DBUILD_TESTS=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_opencv_ts=OFF \ + -DCV_ENABLE_INTRINSICS=ON -DWITH_EIGEN=ON -DWITH_PTHREADS=ON -DWITH_PTHREADS_PF=ON + cmake --build /tmp/opencv/release -j "$(nproc)" + cmake --install /tmp/opencv/release + + cd /work/mediapipe + + MP_VERSION="$(awk '/MEDIAPIPE_FULL_VERSION/ {split($0, a, "="); print a[2]}' mediapipe/version.bzl | tr -d '" ')" + test "${MP_VERSION}" = "${MEDIAPIPE_VERSION}" + sed -i "s/__version__ = 'dev'/__version__ = '${MP_VERSION}'/g" setup.py + + echo 'cc_library(name = "opencv", srcs = ["local/lib64/libopencv_imgproc.a", "local/lib64/libopencv_core.a"], hdrs = glob(["local/include/opencv4/opencv2/**/*.h*"]), includes = ["local/include/opencv4/"], linkstatic = 1, visibility = ["//visibility:public"])' > third_party/opencv_linux.BUILD + sed -i "s|bazel_command.append('--define=OPENCV=source')|pass|g" setup.py + + { + # The image has no remote JDK for riscv64, and .bazelrc's 128 jobs + # would run one C++ compile per 120 MB of RAM on these runners. + echo "build --java_runtime_version=local_jdk" + echo "build --tool_java_runtime_version=local_jdk" + echo "build --jobs=HOST_CPUS" + echo "common --curses=no --show_progress_rate_limit=60" + } >> .bazelrc + + "${PYTHON_BIN}" setup.py bdist_wheel + auditwheel repair --plat manylinux_2_39_riscv64 -w /work/wheelhouse dist/*.whl + + # The payload is a ctypes-loaded libmediapipe.so with no extension + # module, which is why every wheel upstream publishes is py3-none. + "${PYTHON_BIN}" -m wheel tags --python-tag py3 --abi-tag none --remove /work/wheelhouse/*.whl + SCRIPT + + - name: Upload build log + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: mediapipe-${{ env.MEDIAPIPE_VERSION }}-build-log + path: build.log + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: mediapipe-${{ env.MEDIAPIPE_VERSION }}-py3-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + - name: Test wheel + run: | + docker run --rm -i --network=host \ + -v "${GITHUB_WORKSPACE}:/work" \ + -e PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ \ + "${MANYLINUX_RISCV64_IMAGE}" \ + bash <<'SCRIPT' + set -eux + + PYTHON_BIN="/opt/python/cp312-cp312/bin/python" + "${PYTHON_BIN}" -m pip install --only-binary=:all: /work/wheelhouse/*.whl + + cd /tmp + "${PYTHON_BIN}" - <<'PY' + import numpy as np + + import mediapipe as mp + from mediapipe.tasks.python.core import mediapipe_c_bindings + + lib = mediapipe_c_bindings.load_raw_library() + assert lib._name.endswith("libmediapipe.so"), lib._name + + data = np.zeros((6, 4, 3), dtype=np.uint8) + image = mp.Image(image_format=mp.ImageFormat.SRGB, data=data) + assert (image.width, image.height) == (4, 6), (image.width, image.height) + assert np.array_equal(image.numpy_view(), data) + PY + SCRIPT + + publish: + name: Publish mediapipe ${{ 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: mediapipe-${{ matrix.version }}-py3-manylinux_riscv64 diff --git a/docs/packages/mediapipe.yaml b/docs/packages/mediapipe.yaml new file mode 100644 index 00000000000..8e59f546bbe --- /dev/null +++ b/docs/packages/mediapipe.yaml @@ -0,0 +1,5 @@ +package-name: mediapipe +source-code: https://github.com/google/mediapipe +license: Apache-2.0 +versions: +- version: 1.0.1 From ff5537cbca0f86f8dcdaa526c0701e3e6b5542bc Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 19 Sep 2026 07:49:50 +0000 Subject: [PATCH 2/5] skill: add gotcha 381 on reproducing a py3-none platform wheel --- .../references/gotchas-index.md | 1 + .../gotchas/compiled-vs-pure-detection.md | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/skills/python-project-porting/references/gotchas-index.md b/skills/python-project-porting/references/gotchas-index.md index 3da4263fcea..06e64776092 100644 --- a/skills/python-project-porting/references/gotchas-index.md +++ b/skills/python-project-porting/references/gotchas-index.md @@ -342,6 +342,7 @@ The porting gotchas (370 of them) live in [`references/gotchas/`](gotchas/), spl - **292** — Gotcha 81's "diff the wheel `size` field" test can pass on a real per-arch binary - **295** — A require-extension knob that reaches the container correctly (gotcha 129's - **308** — A maturin shim whose star-import name collides with the compiled submodule's +- **381** — Reproducing a `py3-none-` wheel takes an explicit retag — setuptools' ### Dependencies & the registry — [`gotchas/dependencies-and-registry.md`](gotchas/dependencies-and-registry.md) diff --git a/skills/python-project-porting/references/gotchas/compiled-vs-pure-detection.md b/skills/python-project-porting/references/gotchas/compiled-vs-pure-detection.md index 6aa7a9ecf0b..1e73807b511 100644 --- a/skills/python-project-porting/references/gotchas/compiled-vs-pure-detection.md +++ b/skills/python-project-porting/references/gotchas/compiled-vs-pure-detection.md @@ -20,6 +20,7 @@ To pull up one entry: `grep -n '^N\. ' references/gotchas/compiled-vs-pure-detec - **292** — Gotcha 81's "diff the wheel `size` field" test can pass on a real per-arch binary - **295** — A require-extension knob that reaches the container correctly (gotcha 129's - **308** — A maturin shim whose star-import name collides with the compiled submodule's +- **381** — Reproducing a `py3-none-` wheel takes an explicit retag — setuptools' --- @@ -391,3 +392,28 @@ To pull up one entry: `grep -n '^N\. ' references/gotchas/compiled-vs-pure-detec 9/56's standing advice) — `murmurhash2/__init__.py` + `murmurhash2/murmurhash2.abi3.so` next to each other names both the shim and the real extension before a single CI cycle is spent on the wrong probe. + +381. **Reproducing a `py3-none-` wheel takes an explicit retag — setuptools' + `bdist_wheel` ignores `--python-tag` the moment `ext_modules` is non-empty (the + mediapipe case; see `build-mediapipe.yml`).** Gotchas 81/145/292 settle how to *read* + such a tag; this is the other half, producing one. mediapipe declares a single + `BazelExtension('//mediapipe/tasks/c:libmediapipe.so')` purely so `build_ext` shells + out to bazel, and the artifact it copies in is a ctypes-loaded C-API library — + `mediapipe/tasks/python/core/mediapipe_c_bindings.py` does + `ctypes.CDLL(resources.files('mediapipe.tasks.c') / 'libmediapipe.so')`, and no + `PyInit_*` exists anywhere (gotcha 33's shape) — so every wheel upstream publishes is + `py3-none-`. A plain `setup.py bdist_wheel` nonetheless emits + `cp312-cp312-linux_`: `bdist_wheel.get_tag()` only honours `--python-tag` while + `root_is_pure` holds, and any `ext_modules` entry clears it. Restore upstream's tag in + one command, **after** `auditwheel repair` (auditwheel picks its policy off the ABI + tag, so retagging first confuses it): + `python -m wheel tags --python-tag py3 --abi-tag none --remove wheelhouse/*.whl`, + which rewrites `WHEEL`, re-signs `RECORD` and renames the file in place. + - **The payoff is the matrix, and for a heavy C++ port it is the whole budget**: one + interpreter-agnostic artifact serves every interpreter, so the workflow builds the + C++ world once and carries no `python:` matrix at all — one multi-hour job instead + of four. + - **Prove the interpreter-independence rather than inferring it from upstream's tag.** + `unzip -l` the published wheel for a lone non-`.cpython-3XX` `.so`, and grep the + package for the `ctypes`/`cffi` loader that opens it. Retagging a pybind11 payload + this way ships a wheel that installs on interpreters it cannot load. From 7360cdddb65257dce46906c97993445e3f09fd85 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 19 Sep 2026 07:53:18 +0000 Subject: [PATCH 3/5] mediapipe: Patch rules_python's riscv64 platform gap --- .github/workflows/build-mediapipe.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-mediapipe.yml b/.github/workflows/build-mediapipe.yml index ca8ef841059..8a44b7aa213 100644 --- a/.github/workflows/build-mediapipe.yml +++ b/.github/workflows/build-mediapipe.yml @@ -47,6 +47,7 @@ env: BAZEL_VERSION: '7.5.0' RULES_PYTHON_VERSION: '0.33.2' RULES_JAVA_VERSION: '7.6.5' + PROJECT_RULES_PYTHON_VERSION: '0.34.0' jobs: setup: @@ -162,6 +163,7 @@ jobs: -w /work \ -e MEDIAPIPE_VERSION \ -e OPENCV_VERSION \ + -e PROJECT_RULES_PYTHON_VERSION \ "${MANYLINUX_RISCV64_IMAGE}" \ bash <<'SCRIPT' 2>&1 | tee build.log set -eux @@ -187,6 +189,15 @@ jobs: cmake --build /tmp/opencv/release -j "$(nproc)" cmake --install /tmp/opencv/release + # MODULE.bazel's rules_python registers a hermetic interpreter, and its + # PLATFORMS still has no riscv64 entry at this version; any linux entry + # is a safe stand-in because it is never the one selected here. + mkdir -p /tmp/rules_python + curl -fsSLo /tmp/rules_python.tar.gz "https://github.com/bazel-contrib/rules_python/releases/download/${PROJECT_RULES_PYTHON_VERSION}/rules_python-${PROJECT_RULES_PYTHON_VERSION}.tar.gz" + tar -xzf /tmp/rules_python.tar.gz -C /tmp/rules_python --strip-components=1 + sed -i 's|fail("No platform declared for host OS {} on arch {}".format(os_name, arch))|return "x86_64-unknown-linux-gnu"|' \ + /tmp/rules_python/python/private/toolchains_repo.bzl + cd /work/mediapipe MP_VERSION="$(awk '/MEDIAPIPE_FULL_VERSION/ {split($0, a, "="); print a[2]}' mediapipe/version.bzl | tr -d '" ')" @@ -197,11 +208,12 @@ jobs: sed -i "s|bazel_command.append('--define=OPENCV=source')|pass|g" setup.py { - # The image has no remote JDK for riscv64, and .bazelrc's 128 jobs - # would run one C++ compile per 120 MB of RAM on these runners. + # No remote JDK is published for riscv64, and .bazelrc's 128 jobs + # overcommits these runners for a build this size. echo "build --java_runtime_version=local_jdk" echo "build --tool_java_runtime_version=local_jdk" echo "build --jobs=HOST_CPUS" + echo "common --override_module=rules_python=/tmp/rules_python" echo "common --curses=no --show_progress_rate_limit=60" } >> .bazelrc From a41e5014af801e0efe0f122602f8f5f61bc3782e Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 19 Sep 2026 09:23:14 +0000 Subject: [PATCH 4/5] Remove skills/ changes - port PRs must only touch workflow/docs/patches Restores skills/python-project-porting/references/{gotchas-index.md, gotchas/compiled-vs-pure-detection.md} to main's current content. The gotcha this PR had added (381, the py3-none- explicit retag case) collided with a gotcha number another agent had already taken on main in the meantime - it'll be re-added on main directly with a fresh number. --- .../references/gotchas-index.md | 62 ++++++++++++++++++- .../gotchas/compiled-vs-pure-detection.md | 26 -------- 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/skills/python-project-porting/references/gotchas-index.md b/skills/python-project-porting/references/gotchas-index.md index 06e64776092..eb61f532409 100644 --- a/skills/python-project-porting/references/gotchas-index.md +++ b/skills/python-project-porting/references/gotchas-index.md @@ -1,6 +1,6 @@ # Gotchas index — router for the themed gotcha files -The porting gotchas (370 of them) live in [`references/gotchas/`](gotchas/), split by theme so only the relevant slice loads. Every gotcha keeps a **permanent number** cited elsewhere as "gotcha N" (and in workflow comments as "CLAUDE.md gotcha N"). Numbers are stable IDs — **not sequential**, and four are **reused** with different content (two each of 33, 55, 56, 57), disambiguated by theme below. +The porting gotchas (374 of them) live in [`references/gotchas/`](gotchas/), split by theme so only the relevant slice loads. Every gotcha keeps a **permanent number** cited elsewhere as "gotcha N" (and in workflow comments as "CLAUDE.md gotcha N"). Numbers are stable IDs — **not sequential**, and four are **reused** with different content (two each of 33, 55, 56, 57), disambiguated by theme below. ## How to find the gotcha you need @@ -93,6 +93,47 @@ The porting gotchas (370 of them) live in [`references/gotchas/`](gotchas/), spl - **376** — A permissive `License:` field on the wrapper package says nothing about whether the payload it ships has any source at all — check the binary's own content, not the metadata's license family (the tableauhyperapi case). +- **381** — A third-party *vendor release* of a project this repo has already ruled out + inherits that verdict — resolve the redistribution to its upstream before triaging anything + else (the tokenspeed-triton case). +- **382** — Several PyPI distributions carved out of *one* build are one unit of work, not + one port each — check the allowed `--build-type` values before writing any YAML, and let + `requires_dist` (not the most "core-sounding" name) fix the order; complements gotcha 380 + (how to publish them once the combined port exists) (the + pyside6/pyside6-essentials/pyside6-addons case). +- **383** — The *umbrella* distribution of a split family carries no compiled code at all, + gets its platform+`abi3` tag from a deliberately fake `Extension`, and its payload is + generated stubs for the union of its siblings' modules — so it cannot be cut from a + different build than they were; also, check the in-image SDK's *minor version* against the + binding release (the pyside6 meta-wheel case). +- **385** — A no-sdist vendor wheel can still have a fully public build recipe — read + `dist-info/WHEEL`'s `Generator:` before parking it for "no source anywhere"; a + vendor-named generator is usually a *repackager*, which moves the stop to whether the + vendor publishes the payload for our arch (the pyqt6-qt6 case). +- **386** — A GPU-only package can be small, source-open and blob-free and still be + unportable: in a JIT kernel library the compiled part is a few-hundred-KB shim, so gotcha + 41's vendor-payload tell is absent and the wall is what that shim links — `libtorch_cuda.so`, + which our CPU-only riscv64 torch can never provide; refines gotchas 249 and 284 (the + humming-kernels case). +- **387** — A GPU-toolkit-suffixed distribution name (`-cuda12x`, `-rocm-7-0`) is a toolkit + selector whose name can be injected from a *separate* release-tools repo; check the vendor's + redist index for our arch, and treat a documented stub/no-CUDA build mode as a docs build, + not a port (the cupy-cuda12x case). +- **388** — The queue entry's wheel shape is a snapshot — re-read the *latest* release's tag + set before triaging the queued version, because upstream can delete the arch-specific + payload and erase the gap outright; also, a `py3-none-any` dependency can be a facade for + platform-only payload wheels (the tokenspeed-mla case). +- **392** — With no project URL and a stock `Generator:`, the *conda-forge feedstock* is the + cheapest source-availability oracle (a feedstock whose `source:` is the PyPI wheels is a + repackager, so there is nothing to build); `readelf -S` splits a real compiled extension + into engine vs embedded model weights (`.text` ~280 KB, `.rodata` ~34.8 MB); a compound + `License: AND LicenseRef-*` is gotcha 372's second lock; and an open-source + org's monorepo hits can all be the closed-source package's *consumer* + (the livekit-local-inference case). +- **393** — The bindings half of a "bindings wheel + vendored-SDK wheel" pair looks unblocked + from its sdist and is not: the blocking pin is added by the vendor's release step, not by the + sources, and the coupling is a `RUNPATH` into the sibling wheel's directory; a distro-SDK + build is defeated by the sibling's dlopened plugin/QML payload (the pyqt6 case). ### Sdist source & versioning — [`gotchas/sdist-source-and-versioning.md`](gotchas/sdist-source-and-versioning.md) @@ -162,6 +203,11 @@ The porting gotchas (370 of them) live in [`references/gotchas/`](gotchas/), spl - **360** — A `setup.py`'s own `bdist_wheel --plat-name` insertion can hardcode `manylinux1_` + `platform.machine()` regardless of the actual container libc, making musllinux unbuildable no matter how the CMake/C++ side is patched. +- **391** — A project's real cibuildwheel recipe can live in a *separate packaging repo* that the + source tree never references — the source repo can carry no GitHub Actions at all. +- **396** — A `cpXY-none-` wheel is the third plat-name shape: `setup.py` declares + no `ext_modules` at all, and a sibling CMake build both compiles the extension modules and + hands `bdist_wheel` the tag (the coremltools case). ### Rust, maturin & PyO3 — [`gotchas/rust-maturin-and-pyo3.md`](gotchas/rust-maturin-and-pyo3.md) @@ -257,6 +303,9 @@ The porting gotchas (370 of them) live in [`references/gotchas/`](gotchas/), spl - **202** — A monorepo's "regenerate deps from Bazel" helper may already tolerate a missing - **219** — GDAL's cmake build produces no `gdal-config` script — a second consumer of the - **233** — A package can have no Python build backend at all — the wheel comes from an +- **397** — A CMake build that shells out to a bare `python3` for one vendored sub-extension + silently builds it for the container's default interpreter, not the one the wheel is for + (the coremltools/kmeans1d case). ### The manylinux image & toolchain — [`gotchas/manylinux-image-and-toolchain.md`](gotchas/manylinux-image-and-toolchain.md) @@ -305,6 +354,9 @@ The porting gotchas (370 of them) live in [`references/gotchas/`](gotchas/), spl - **378** — A newer libstdc++ on the manylinux image can deprecate calls a project's own `-DCMAKE_COMPILE_WARNING_AS_ERROR=ON` CI flag then turns into hard errors, purely from a toolchain-version gap upstream's own (older) runners never see. +- **390** — libev is one of the `-devel` packages that *is* in Rocky 10's riscv64 repos, so an + upstream `yum install -y libev libev-devel` needs no replacement — but its header is + `/usr/include/ev.h`. ### Native dependencies & linking — [`gotchas/native-deps-and-linking.md`](gotchas/native-deps-and-linking.md) @@ -325,6 +377,7 @@ The porting gotchas (370 of them) live in [`references/gotchas/`](gotchas/), spl - **278** — A vendored, direct-copy (not submodule) header can be missing riscv64 from its - **363** — A `libraries=[...]` entry can go missing from the link line with *no* error — - **368** — Linking several codecs against Rocky 10's system libraries instead of +- **395** — When a project dlopen()s a differently-named shared library per major ### Compiled-vs-pure detection & the require-extension knob — [`gotchas/compiled-vs-pure-detection.md`](gotchas/compiled-vs-pure-detection.md) @@ -342,7 +395,6 @@ The porting gotchas (370 of them) live in [`references/gotchas/`](gotchas/), spl - **292** — Gotcha 81's "diff the wheel `size` field" test can pass on a real per-arch binary - **295** — A require-extension knob that reaches the container correctly (gotcha 129's - **308** — A maturin shim whose star-import name collides with the compiled submodule's -- **381** — Reproducing a `py3-none-` wheel takes an explicit retag — setuptools' ### Dependencies & the registry — [`gotchas/dependencies-and-registry.md`](gotchas/dependencies-and-registry.md) @@ -420,6 +472,9 @@ The porting gotchas (370 of them) live in [`references/gotchas/`](gotchas/), spl - **329** — A test suite that shells out to the package's own installed CLI binaries at a - **347** — A test that asserts "you're running against an editable/in-place install" can - **348** — A `glcontext`-based package's `create_context(standalone=True)` defaults to the +- **389** — A test `.pyx` that Cython-`include`s a checkout-root-relative path can be satisfied by + staging just those files; a staged package dir with no `__init__.py` is a namespace + portion and does not shadow the wheel. ### Testing: pytest config, servers & test selection — [`gotchas/pytest-config-servers-and-selection.md`](gotchas/pytest-config-servers-and-selection.md) @@ -524,6 +579,9 @@ The porting gotchas (370 of them) live in [`references/gotchas/`](gotchas/), spl - **223** — For a `bindings = "bin"` CLI's test assertions, `cargo build --release` the tool - **298** — A local rehearsal's `pip`-resolved cibuildwheel can be too old for - **369** — Without docker, fetch Rocky 10's own dnf repodata over plain HTTPS to +- **384** — `dnf` failing in the image with `Curl error (60) ... self-signed certificate` is + your egress proxy, not the image — install the proxy CA into the container trust store +- **394** — A libtorch-linking project cannot be rehearsed on x86_64 with PyPI's `torch` ### PR, CI, triggers, publishing & maintainer signals — [`gotchas/pr-ci-and-maintainer.md`](gotchas/pr-ci-and-maintainer.md) diff --git a/skills/python-project-porting/references/gotchas/compiled-vs-pure-detection.md b/skills/python-project-porting/references/gotchas/compiled-vs-pure-detection.md index 1e73807b511..6aa7a9ecf0b 100644 --- a/skills/python-project-porting/references/gotchas/compiled-vs-pure-detection.md +++ b/skills/python-project-porting/references/gotchas/compiled-vs-pure-detection.md @@ -20,7 +20,6 @@ To pull up one entry: `grep -n '^N\. ' references/gotchas/compiled-vs-pure-detec - **292** — Gotcha 81's "diff the wheel `size` field" test can pass on a real per-arch binary - **295** — A require-extension knob that reaches the container correctly (gotcha 129's - **308** — A maturin shim whose star-import name collides with the compiled submodule's -- **381** — Reproducing a `py3-none-` wheel takes an explicit retag — setuptools' --- @@ -392,28 +391,3 @@ To pull up one entry: `grep -n '^N\. ' references/gotchas/compiled-vs-pure-detec 9/56's standing advice) — `murmurhash2/__init__.py` + `murmurhash2/murmurhash2.abi3.so` next to each other names both the shim and the real extension before a single CI cycle is spent on the wrong probe. - -381. **Reproducing a `py3-none-` wheel takes an explicit retag — setuptools' - `bdist_wheel` ignores `--python-tag` the moment `ext_modules` is non-empty (the - mediapipe case; see `build-mediapipe.yml`).** Gotchas 81/145/292 settle how to *read* - such a tag; this is the other half, producing one. mediapipe declares a single - `BazelExtension('//mediapipe/tasks/c:libmediapipe.so')` purely so `build_ext` shells - out to bazel, and the artifact it copies in is a ctypes-loaded C-API library — - `mediapipe/tasks/python/core/mediapipe_c_bindings.py` does - `ctypes.CDLL(resources.files('mediapipe.tasks.c') / 'libmediapipe.so')`, and no - `PyInit_*` exists anywhere (gotcha 33's shape) — so every wheel upstream publishes is - `py3-none-`. A plain `setup.py bdist_wheel` nonetheless emits - `cp312-cp312-linux_`: `bdist_wheel.get_tag()` only honours `--python-tag` while - `root_is_pure` holds, and any `ext_modules` entry clears it. Restore upstream's tag in - one command, **after** `auditwheel repair` (auditwheel picks its policy off the ABI - tag, so retagging first confuses it): - `python -m wheel tags --python-tag py3 --abi-tag none --remove wheelhouse/*.whl`, - which rewrites `WHEEL`, re-signs `RECORD` and renames the file in place. - - **The payoff is the matrix, and for a heavy C++ port it is the whole budget**: one - interpreter-agnostic artifact serves every interpreter, so the workflow builds the - C++ world once and carries no `python:` matrix at all — one multi-hour job instead - of four. - - **Prove the interpreter-independence rather than inferring it from upstream's tag.** - `unzip -l` the published wheel for a lone non-`.cpython-3XX` `.so`, and grep the - package for the `ctypes`/`cffi` loader that opens it. Retagging a pybind11 payload - this way ships a wheel that installs on interpreters it cannot load. From 7fae8e1f8e0425466858678abf3b102338e982be Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Sat, 19 Sep 2026 21:39:59 +0000 Subject: [PATCH 5/5] mediapipe: Disable XNNPACK's RISC-V fp16 vector microkernels XNNPACK builds its rvvfp16arith microkernels with -march=rv64gc_zvfh. The manylinux_2_39_riscv64 image is Rocky 10, whose binutils is 2.41, and zvfh only landed in 2.42 - the assembler rejects the whole ISA string rather than the one unknown extension, so all 71 translation units of @@XNNPACK//:rvvfp16arith_prod_microkernels failed with "unknown prefixed ISA extension `zvfh'" and took //mediapipe/tasks/c:libmediapipe.so down with them. GCC 14.3.1 itself accepts the flag, which is what makes it read like a compiler bug. --define=xnn_enable_riscv_fp16_vector=false is XNNPACK's own switch for this microkernel family, the same one it uses for Android, and it also sets XNN_ENABLE_RISCV_FP16_VECTOR=0 so the dispatch code stops referencing them. The plain rvv kernels keep building: -march=rv64gcv assembles fine on 2.41, so this gives up only fp16 arithmetic vector kernels, not RVV. --- .github/workflows/build-mediapipe.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-mediapipe.yml b/.github/workflows/build-mediapipe.yml index 8a44b7aa213..80c6863e206 100644 --- a/.github/workflows/build-mediapipe.yml +++ b/.github/workflows/build-mediapipe.yml @@ -213,6 +213,10 @@ jobs: echo "build --java_runtime_version=local_jdk" echo "build --tool_java_runtime_version=local_jdk" echo "build --jobs=HOST_CPUS" + # XNNPACK's rvvfp16arith microkernels build with -march=rv64gc_zvfh, and + # the image's binutils 2.41 assembler rejects the whole ISA string (zvfh + # landed in 2.42); this is XNNPACK's own off-switch, as it uses for Android. + echo "build --define=xnn_enable_riscv_fp16_vector=false" echo "common --override_module=rules_python=/tmp/rules_python" echo "common --curses=no --show_progress_rate_limit=60" } >> .bazelrc