diff --git a/.github/workflows/build-decord.yml b/.github/workflows/build-decord.yml new file mode 100644 index 000000000..cd39e5826 --- /dev/null +++ b/.github/workflows/build-decord.yml @@ -0,0 +1,148 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# Based on upstream's own manylinux wheel job (builds FFmpeg from source, then decord against +# it, then retags the result py3-none since libdecord.so is ctypes-loaded): +# https://github.com/dmlc/decord/blob/v0.6.0/.github/workflows/pypi.yml +# https://github.com/dmlc/decord/blob/v0.6.0/tools/build_manylinux2010.sh +# Unlike upstream's script, FFmpeg is built here without --enable-gpl/--enable-nonfree/ +# libx264/libvpx: decord only ever decodes (no avcodec_find_encoder/AVOutputFormat use +# anywhere in src/), so a plain LGPL FFmpeg build already covers everything it links against. +name: Build decord wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'decord version to build (git tag without leading v, e.g. 0.6.0)' + required: true + default: '0.6.0' + pull_request: + paths: + - '.github/workflows/build-decord.yml' + - 'patches/decord/**' + +concurrency: + group: ${{ github.workflow }}-${{ inputs.version || '0.6.0' }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + DECORD_VERSION: ${{ inputs.version || '0.6.0' }} + # decord's own configure.py never gained riscv64 support (FFmpeg didn't add ARCH_RISCV + # until 5.0); 6.1.6 is the last release before the 7.0 ch_layout API removal that + # src/audio/audio_reader.cc's AVCodecParameters::channels/av_get_default_channel_layout + # calls would otherwise need patching for. + FFMPEG_VERSION: 6.1.6 + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + + build_wheels: + needs: [setup] + name: Build decord ${{ inputs.version || '0.6.0' }} py3-none-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 360 + + steps: + - name: Checkout decord v${{ env.DECORD_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: dmlc/decord + ref: v${{ env.DECORD_VERSION }} + submodules: true + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + persist-credentials: false + + - name: Patch decord source + run: git apply python-wheels/patches/decord/${{ env.DECORD_VERSION }}/00*.patch + + # setuptools' default license-files glob (LICEN[CSZ]E*) is rooted at setup.py's + # own directory (gotcha 146), which for decord is python/, not the outer checkout + # root where both decord's own LICENSE and this live -- neither is picked up + # without copying them alongside setup.py first. + - name: Stage licence texts for packaging + run: | + cp LICENSE python/LICENSE + curl -sL -o python/LICENSE.FFmpeg https://raw.githubusercontent.com/FFmpeg/FFmpeg/n${{ env.FFMPEG_VERSION }}/COPYING.LGPLv2.1 + + - name: Build wheel + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + package-dir: python + output-dir: wheelhouse/ + # wheel_include_libs in python/setup.py only ever bundles a ctypes-loaded + # libdecord.so, no interpreter-specific extension -- one build serves everyone + # (retagged py3-none below, same as upstream's own pypi.yml rename step). + only: cp312-manylinux_riscv64 + env: + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/ffmpeg-build/lib:$LD_LIBRARY_PATH + CIBW_BEFORE_BUILD: | + set -eux && \ + curl -L -O https://ffmpeg.org/releases/ffmpeg-${{ env.FFMPEG_VERSION }}.tar.bz2 && \ + tar xjf ffmpeg-${{ env.FFMPEG_VERSION }}.tar.bz2 && cd ffmpeg-${{ env.FFMPEG_VERSION }} && \ + ./configure --prefix=/tmp/ffmpeg-build --disable-static --enable-shared --enable-pic --disable-programs --disable-doc && \ + make -j$(nproc) && make install && \ + cd .. && rm -rf ffmpeg-${{ env.FFMPEG_VERSION }}* && \ + cmake -S {project} -B {project}/build -DUSE_CUDA=OFF -DFFMPEG_DIR=/tmp/ffmpeg-build -DCMAKE_BUILD_TYPE=Release && \ + cmake --build {project}/build --parallel $(nproc) + CIBW_TEST_REQUIRES: pytest numpy + CIBW_TEST_SOURCES: tests examples + # Mirrors upstream's own sanity check (all 3 of pypi.yml's platform jobs run + # this same file, historically via `nose` -- patched to pytest.raises since + # nose 1.3.7 imports the removed-in-3.12 stdlib `imp` module, unrelated to + # riscv64 (see patches/decord/0.6.0/0003-*.patch)). + CIBW_TEST_COMMAND: pytest -v tests/python/unittests/test_video_reader.py + + # setup.py's get_tag() has no py3-none override (unlike plotext's), so cibuildwheel + # produces a cp312-cp312 tag; retag it the same way upstream's own pypi.yml does + # (mv ... cp36-cp36m -> py3-none), just with the wheel/RECORD metadata kept correct. + - name: Retag the wheel py3-none (libdecord.so is ctypes-loaded, not interpreter-specific) + run: | + # The runner's system Python is PEP 668 externally-managed; a venv + # sidesteps that instead of --break-system-packages (gotcha precedent + # in build-pylerc.yml's "Install the built wheel" step). + sudo apt-get update -qq + sudo apt-get install -y -qq --no-install-recommends python3-venv + python3 -m venv .venv + . .venv/bin/activate + pip install -q "wheel>=0.42" + python3 -m wheel tags --python-tag py3 --abi-tag none --remove wheelhouse/decord-*.whl + + - name: Check the wheel carries libdecord and a vendored LGPL FFmpeg + run: | + python3 - wheelhouse/*.whl <<'EOF' + import sys, zipfile + + names = zipfile.ZipFile(sys.argv[1]).namelist() + for want in ("decord/libdecord.so", "libavcodec", "licenses/LICENSE", "licenses/LICENSE.FFmpeg"): + if not any(want in n for n in names): + raise SystemExit(f"error: {want} missing from {sys.argv[1]}") + print("\n".join(n for n in names if ".so" in n)) + EOF + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: decord-${{ env.DECORD_VERSION }}-py3-none-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish decord ${{ inputs.version || '0.6.0' }} + needs: [setup, build_wheels] + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + with: + artifact-pattern: decord-${{ inputs.version || '0.6.0' }}-*-manylinux_riscv64 diff --git a/patches/decord/0.6.0/0001-ffmpeg_common.h-explicitly-include-libavcodec-bsf.h.patch b/patches/decord/0.6.0/0001-ffmpeg_common.h-explicitly-include-libavcodec-bsf.h.patch new file mode 100644 index 000000000..6a5a9c6c6 --- /dev/null +++ b/patches/decord/0.6.0/0001-ffmpeg_common.h-explicitly-include-libavcodec-bsf.h.patch @@ -0,0 +1,39 @@ +From 1c3480c359dfd2ade99e9de7c82453992e6a7f53 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Fri, 11 Sep 2026 03:00:28 +0200 +Subject: [PATCH] ffmpeg_common.h: explicitly include libavcodec/bsf.h + +Upstream-Status: To upstream [decord is unmaintained: no commits addressing FFmpeg API compatibility since v0.6.0 (2021-06-14)] + +AVBSFContext and av_bsf_free() (used a few lines below to define +AVBSFContextPtr) were declared directly in libavcodec/avcodec.h through +FFmpeg 4.1.x, the version decord's own CI (tools/build_manylinux2010.sh) +built against. FFmpeg moved the whole bitstream-filter API out to its own +libavcodec/bsf.h and dropped avcodec.h's copy somewhere before 5.1 (still +missing in current FFmpeg master), so ffmpeg_common.h's include list needs +updating to build against any FFmpeg newer than upstream's own pin: + + ffmpeg_common.h:187:5: error: 'AVBSFContext' was not declared in this scope + ffmpeg_common.h:187:48: error: 'av_bsf_free' was not declared in this scope + +Not riscv64-specific: any build against FFmpeg 5.1+ hits this on any +architecture. +--- + src/video/ffmpeg/ffmpeg_common.h | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/src/video/ffmpeg/ffmpeg_common.h b/src/video/ffmpeg/ffmpeg_common.h +index b0b973f..f0f7316 100644 +--- a/src/video/ffmpeg/ffmpeg_common.h ++++ b/src/video/ffmpeg/ffmpeg_common.h +@@ -21,6 +21,7 @@ + extern "C" { + #endif + #include ++#include + #include + #include + #include +-- +2.50.1 (Apple Git-155) + diff --git a/patches/decord/0.6.0/0002-video_reader.cc-const-qualify-the-AVCodec-av_find_be.patch b/patches/decord/0.6.0/0002-video_reader.cc-const-qualify-the-AVCodec-av_find_be.patch new file mode 100644 index 000000000..56ddc027d --- /dev/null +++ b/patches/decord/0.6.0/0002-video_reader.cc-const-qualify-the-AVCodec-av_find_be.patch @@ -0,0 +1,41 @@ +From 941cde5866ec9e756b58dee2e08561661e375098 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Fri, 11 Sep 2026 04:28:44 +0200 +Subject: [PATCH] video_reader.cc: const-qualify the AVCodec* + av_find_best_stream fills + +Upstream-Status: To upstream [decord is unmaintained: no commits addressing FFmpeg API compatibility since v0.6.0 (2021-06-14)] + +FFmpeg's avcodec_find_decoder()/av_find_best_stream() family started +returning const AVCodec* around FFmpeg 5.x (part of the same +const-correctness pass that added AVCodecParameters' constness earlier); +SetVideoStream()'s local AVCodec *dec was never updated to match, and +&dec no longer converts to the const AVCodec** av_find_best_stream() +now expects: + + video_reader.cc:149:88: error: invalid conversion from 'AVCodec**' to + 'const AVCodec**' [-fpermissive] + +codecs_ (std::vector) and every other AVCodec* use in +this file were already const; grepped src/ and include/ for any other +non-const "AVCodec *" declaration and found none. +--- + src/video/video_reader.cc | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/video/video_reader.cc b/src/video/video_reader.cc +index af4858d..99c9635 100644 +--- a/src/video/video_reader.cc ++++ b/src/video/video_reader.cc +@@ -145,7 +145,7 @@ VideoReader::~VideoReader(){ + + void VideoReader::SetVideoStream(int stream_nb) { + if (!fmt_ctx_) return; +- AVCodec *dec; ++ const AVCodec *dec; + int st_nb = av_find_best_stream(fmt_ctx_.get(), AVMEDIA_TYPE_VIDEO, stream_nb, -1, &dec, 0); + // LOG(INFO) << "find best stream: " << st_nb; + CHECK_GE(st_nb, 0) << "ERROR cannot find video stream with wanted index: " << stream_nb; +-- +2.50.1 (Apple Git-155) + diff --git a/patches/decord/0.6.0/0003-test_video_reader-use-pytest.raises-not-nose.patch b/patches/decord/0.6.0/0003-test_video_reader-use-pytest.raises-not-nose.patch new file mode 100644 index 000000000..e9e5aab76 --- /dev/null +++ b/patches/decord/0.6.0/0003-test_video_reader-use-pytest.raises-not-nose.patch @@ -0,0 +1,38 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sat Sep 12 08:05:00 2026 +0200 +Subject: [PATCH] test_video_reader: use pytest.raises, not nose + +nose 1.3.7 (the newest release, unmaintained since 2015) imports the +stdlib `imp` module at collection time, which was removed in Python +3.12 -- `from nose.tools import assert_raises` fails on any Python +3.12+ interpreter regardless of platform, confirmed against a stock +Python 3.12 outside this container too. pytest.raises is nose's own +documented drop-in replacement for this single assert_raises(exc, fn, +*args) usage, so this restores real test coverage instead of skipping +the test. + +Upstream-Status: To upstream [not yet submitted; needs a decord maintainer decision on whether to drop the nose dependency project-wide] +--- + tests/python/unittests/test_video_reader.py | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/tests/python/unittests/test_video_reader.py b/tests/python/unittests/test_video_reader.py +index b5b0523..2e2e548 100644 +--- a/tests/python/unittests/test_video_reader.py ++++ b/tests/python/unittests/test_video_reader.py +@@ -55,9 +55,10 @@ def test_video_get_batch(): + frames = vr.get_batch(rand_lst) + + def test_video_corrupted_get_batch(): +- from nose.tools import assert_raises ++ import pytest + vr = _get_corrupted_test_video(ctx=cpu(0)) +- assert_raises(DECORDError, vr.get_batch, range(40)) ++ with pytest.raises(DECORDError): ++ vr.get_batch(range(40)) + + def test_rotated_video(): + # Input videos are all h=320 w=568 in metadata, but +-- +2.43.0