diff --git a/.github/workflows/build-mediapipe.yml b/.github/workflows/build-mediapipe.yml new file mode 100644 index 0000000000..80c6863e20 --- /dev/null +++ b/.github/workflows/build-mediapipe.yml @@ -0,0 +1,289 @@ +# 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' + PROJECT_RULES_PYTHON_VERSION: '0.34.0' + +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 \ + -e PROJECT_RULES_PYTHON_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 + + # 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 '" ')" + 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 + + { + # 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" + # 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 + + "${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 0000000000..8e59f546bb --- /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