Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions .github/workflows/build-torchcodec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# This workflow is based on: https://github.com/pytorch/torchcodec/blob/main/.github/workflows/linux_wheel.yaml
name: Build torchcodec wheels (riscv64)

on:
workflow_dispatch:
inputs:
version:
description: 'Version glob to (re)build; empty builds every version of docs/packages/torchcodec.yaml not released yet'
required: false
default: ''
pull_request:
branches: [main]
paths:
- '.github/workflows/build-torchcodec.yml'
- 'docs/packages/torchcodec.yaml'
- 'patches/torchcodec/**'
push:
branches: [main]
paths:
- '.github/workflows/build-torchcodec.yml'
- 'docs/packages/torchcodec.yaml'
- 'patches/torchcodec/**'

run-name: build-torchcodec ${{ inputs.version && format('- {0}', inputs.version) || '' }}

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read # to fetch code (actions/checkout)

env:
# Upstream links against prebuilt non-GPL FFmpeg tarballs it hosts on S3, which have no
# riscv64 build, so FFmpeg is built from source here (LGPL: no --enable-gpl/--enable-nonfree)
# and never shipped. 6.1 because that is the libavutil ABI of the system FFmpeg on
# ubuntu-24.04-riscv, where these wheels are consumed: the FFmpeg major version is baked
# into the libtorchcodec_core<N>.so name that torchcodec dlopen()s at runtime.
FFMPEG_VERSION: 6.1.6

jobs:
setup:
uses: $/.github/workflows/_setup.yml
with:
package: torchcodec
version: ${{ inputs.version }}

build_wheels:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
name: Build torchcodec ${{ matrix.version }} cp${{ matrix.python-version }}-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 360
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
# Match torch's own riscv64 matrix on the registry: torchcodec's pybind11 module is
# per-ABI, so there is no single abi3 wheel even though it targets torch's stable ABI.
# See https://pypi.riseproject.dev/python-wheels/development.html#target-python-versions
python-version: [312, 313, 314, 314t]

env:
TORCHCODEC_VERSION: ${{ matrix.version }}

steps:
- name: Checkout torchcodec v${{ env.TORCHCODEC_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: pytorch/torchcodec
ref: v${{ env.TORCHCODEC_VERSION }}
persist-credentials: false

- name: Checkout python-wheels
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: python-wheels
persist-credentials: false

- name: Patch torchcodec source
run: git apply python-wheels/patches/torchcodec/${{ env.TORCHCODEC_VERSION }}/00*.patch

- uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0
env:
CIBW_BUILD: "cp${{ matrix.python-version }}-manylinux_riscv64"
# setup is scikit-build-core driving CMake, which resolves Torch_DIR by running
# `import torch` in the build interpreter; torch only exists on our registry, so it
# cannot be declared in build-system.requires and isolation has to be off.
CIBW_BUILD_FRONTEND: "pip; args: --no-build-isolation"
# auditwheel grafts libjpeg-turbo/libpng/libwebp/zlib into the wheel, so their
# licence texts have to travel with it (the rpm that owns each bundled .so is
# resolved rather than hardcoded; %license files are dropped by the image's
# tsflags=nodocs and come back with an explicit reinstall). LICENSE.* is what the
# patched pyproject.toml globs into .dist-info/licenses/.
CIBW_BEFORE_ALL: |
set -ex
dnf install -y --setopt=tsflags= pkgconfig libjpeg-turbo-devel libpng-devel libwebp-devel zlib-devel
devel=$(rpm -q --whatprovides --qf '%{NAME}\n' libjpeg-turbo-devel libpng-devel libwebp-devel zlib-devel | sort -u)
libs=$(rpm -ql $devel | grep -E '\.so$' | xargs -r readlink -f)
pkgs=$(rpm -qf --qf '%{NAME}\n' $libs | grep -E '^[A-Za-z0-9._+-]+$' | sort -u)
dnf reinstall -y --setopt=tsflags= $pkgs || true
for p in $pkgs; do
for f in /usr/share/licenses/$p/*; do
[ -f "$f" ] || continue
cp "$f" "{project}/LICENSE.$p.${f##*/}"
done
done
ls {project}/LICENSE.*
mkdir -p /tmp/ffmpeg
curl -fsSL "https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n${FFMPEG_VERSION}.tar.gz" | tar xz --strip-components=1 -C /tmp/ffmpeg
cd /tmp/ffmpeg
./configure --prefix=/usr/local --disable-static --enable-shared --enable-pic --disable-doc
make -j"$(nproc)" install
ldconfig
CIBW_BEFORE_BUILD: >-
pip install --only-binary=:all:
torch>=2.11 "scikit-build-core>=0.10" pybind11 ninja setuptools wheel
# I_CONFIRM_THIS_IS_NOT_A_LICENSE_VIOLATION is upstream's own escape hatch for
# wheels built against an FFmpeg that is not their S3 one; the guard exists to stop
# a GPL FFmpeg being linked, and the build above is LGPL.
# AVIF and HEIC are off because neither can be had on riscv64: libavif comes from
# the same S3 bucket and libheif is packaged in neither Rocky 10 nor a riscv64 EPEL.
# decode_avif/decode_heic then raise an actionable error instead of failing the build.
CIBW_ENVIRONMENT: >-
FFMPEG_VERSION=${{ env.FFMPEG_VERSION }}
BUILD_VERSION=${{ env.TORCHCODEC_VERSION }}
I_CONFIRM_THIS_IS_NOT_A_LICENSE_VIOLATION=1
TORCHCODEC_BUILD_AVIF=0
TORCHCODEC_BUILD_HEIC=0
TORCHCODEC_DISABLE_COMPILE_WARNING_AS_ERROR=1
PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/
# Same exclude list as upstream's packaging/repair_wheel.py: FFmpeg is a runtime
# dependency the user supplies, and libtorch/libc10/libgomp come from the torch
# wheel, which is imported (RTLD_GLOBAL) before torchcodec loads its own libraries.
CIBW_REPAIR_WHEEL_COMMAND: >-
auditwheel repair -w {dest_dir} {wheel}
--exclude 'libavcodec*'
--exclude 'libavdevice*'
--exclude 'libavfilter*'
--exclude 'libavformat*'
--exclude 'libavutil*'
--exclude 'libswresample*'
--exclude 'libswscale*'
--exclude 'libpostproc*'
--exclude 'libtorch*'
--exclude 'libc10*'
--exclude libgomp.so.1
CIBW_TEST_REQUIRES: torch>=2.11 numpy pytest pillow
# pyproject.toml is staged alongside test/ purely for its [tool.pytest.ini_options]
# (the needs_* markers and the 'not slow' default); src/ deliberately is not, which
# is what upstream's packaging/remove_src.sh achieves before its own test jobs.
CIBW_TEST_SOURCES: test pyproject.toml
# FAIL_WITHOUT_<CODEC> turns a missing image codec from a silent skip into a
# failure, for the three we do build. test_transform_ops.py imports torchvision at
# module scope and pypi.riseproject.dev has no riscv64 torchvision yet.
# Upstream tests against conda-forge's GPL FFmpeg; the LGPL build above has only
# FFmpeg's built-in codecs - no H.264/HEVC/VP9/AV1/MP3 encoder at all (libx264 and
# libx265 need --enable-gpl) and an AV1 decoder that only works through a hwaccel -
# so the tests that mux a real video, shell out to the ffmpeg CLI or ask for MP3 are
# dropped, smoke_test.py included: it encodes H.264 yuv444p to build every one of its
# fixtures. test_logging.py asserts an empty subprocess stderr, which torch's
# libcpuinfo pollutes on this runner by reading core_id -1 from /sys.
CIBW_TEST_COMMAND: >-
FAIL_WITHOUT_JPEG=1 FAIL_WITHOUT_PNG=1 FAIL_WITHOUT_WEBP=1 pytest
--ignore=test/test_transform_ops.py
--ignore=test/smoke_test.py
--ignore=test/test_logging.py
-k "not test_get_frame_at_av1
and not ((test_audio_against_cli or test_multiple_audio_formats
or test_audio_to_file_vs_to_file_like) and mp3)
and not (test_add_video_and_encode_frames or test_add_audio_and_video_and_encode
or test_multiple_video_streams_and_audio or test_video_round_trip
or test_video_to_file_vs_to_file_like or test_video_contiguity
or test_video_against_ffmpeg_cli or test_fragmented_mp4
or test_codec_parameter_utilized or test_codec_spec_vs_impl_equivalence
or test_extra_options_errors or test_extra_options_utilized
or test_crf_out_of_range_errors or test_invalid_preset_errors
or test_to_file_like_real_file_video
or (test_to_file_like_custom_file_object and not audio)
or test_add_audio_unsupported_num_channels_errors
or test_add_audio_invalid_out_sample_rate_errors)"
test

- name: Check the wheel bundles the image codecs and no FFmpeg
run: |
python3 - wheelhouse/*.whl <<'EOF'
import sys, zipfile

names = zipfile.ZipFile(sys.argv[1]).namelist()
for want in ("torchcodec/libtorchcodec_core6.so", "torchcodec/libtorchcodec_image.so",
"libjpeg", "libpng", "libwebp", "licenses/LICENSE"):
if not any(want in n for n in names):
raise SystemExit(f"error: {want} missing from {sys.argv[1]}")
for unwanted in ("libavcodec", "libavutil", "libtorch.so", "libc10.so"):
if any(unwanted in n for n in names):
raise SystemExit(f"error: {unwanted} must not be bundled in {sys.argv[1]}")
print("\n".join(n for n in names if ".so" in n or "licenses/" in n))
EOF

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: torchcodec-${{ env.TORCHCODEC_VERSION }}-cp${{ matrix.python-version }}-manylinux_riscv64
path: ./wheelhouse/*.whl
if-no-files-found: error

publish:
name: Publish torchcodec ${{ 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: torchcodec-${{ matrix.version }}-*-manylinux_riscv64
5 changes: 5 additions & 0 deletions docs/packages/torchcodec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package-name: torchcodec
source-code: https://github.com/pytorch/torchcodec
license: BSD-3-Clause AND MIT AND Zlib AND libpng-2.0 AND IJG
versions:
- version: 0.16.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Fri, 19 Sep 2026 12:00:00 +0200
Subject: [PATCH] pyproject: glob the licence texts of the bundled image codecs

Upstream-Status: Inappropriate [our wheels are repaired by cibuildwheel's auditwheel, not by upstream's packaging/repair_wheel.py]

torchcodec redistributes libjpeg-turbo, libpng, zlib and libwebp as
binaries inside the wheel, and their (permissive) licences require the
copyright notice to travel with the binary. Upstream satisfies that in
packaging/repair_wheel.py's bundle_third_party_licenses(), which unpacks
each freshly built wheel, copies the licence texts out of the conda
packages the libraries came from into .dist-info/licenses/third_party/,
and repacks. That path is unusable here for two independent reasons: it
resolves the texts through CONDA_PREFIX/conda-meta (there is no conda
environment in the manylinux image, the libraries come from Rocky rpms),
and it hardcodes dist/ -> dist_repaired/ plus a mandatory S3 libavif
directory, neither of which exists under cibuildwheel's own auditwheel
repair step.

So the texts are staged next to the project's own LICENSE instead, as
LICENSE.<rpm name>.<file name>, and picked up by widening the PEP 639
license-files list. An explicit license-files list has no default glob
behind it, so without this the added files are silently dropped and the
wheel ships only torchcodec's own BSD text.

Not riscv64-specific in substance, but it only makes sense for a build
that repairs wheels with plain auditwheel, so there is nothing to send
upstream.
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyproject.toml b/pyproject.toml
index 4a52430..fde71b9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,7 +3,7 @@ name = "torchcodec"
description = "A video decoder for PyTorch"
readme = "README.md"
requires-python = ">=3.10"
-license-files = ["LICENSE"]
+license-files = ["LICENSE", "LICENSE.*"]
authors = [
{ name = "PyTorch Team", email = "packages@pytorch.org" },
]
--
2.51.0

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Sat, 19 Sep 2026 19:40:00 +0200
Subject: [PATCH] DecodeAvif: give the no-libavif stub the declared signature

Upstream-Status: To upstream [not yet submitted; upstream never builds this path - their wheels always link the libavif they fetch from S3, and no CI job sets TORCHCODEC_BUILD_AVIF=0]

decode_avif() is declared in DecodeAvif.h - and defined by the
TORCHCODEC_ENABLE_AVIF branch of DecodeAvif.cpp - with four parameters
(input, mode, output_dtype, num_threads), and image_custom_ops.cpp takes
its address with TORCH_BOX(&decode_avif) to register the
"decode_avif(Tensor input, int mode, int output_dtype=0, int
num_threads=1) -> Tensor" op. The !TORCHCODEC_ENABLE_AVIF stub, however,
still has the three-parameter signature from before num_threads was
added, so it defines a *different* overload and the four-parameter one is
never defined anywhere.

An ELF shared object may carry undefined symbols, so nothing complains:
libtorchcodec_image.so links, the wheel builds, auditwheel repairs it,
and the first torch.ops.load_library() of it fails with

OSError: .../torchcodec/libtorchcodec_image.so: undefined symbol:
_ZN8facebook10torchcodec11decode_avifERKN5torch6stable6TensorElll

i.e. facebook::torchcodec::decode_avif(torch::stable::Tensor const&,
long, long, long). Because load_image_library() runs at
`import torchcodec`, every entry point of the package is dead, not just
decode_avif - which defeats the documented purpose of the build flag
("A codec set to OFF is not built; its decode_*() then raises an
actionable error at call time [...] while importing torchcodec still
works").

Add the missing num_threads parameter to the stub. Nothing else changes:
the stub still just raises the actionable "not compiled with libavif
support" error, which is what test/utils.py's avif_is_available() looks
for to skip the AVIF tests.

This is not riscv64-specific - any build with TORCHCODEC_BUILD_AVIF=0 or
TORCHCODEC_BUILD_IMAGE=0 produces an unloadable image library - but it is
what our wheels hit, since libavif comes from upstream's S3 bucket, which
has no riscv64 build, and neither Rocky 10 nor a riscv64 EPEL packages it.

Signed-off-by: Ludovic Henry <git@ludovic.dev>
---
src/torchcodec/_core/DecodeAvif.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/torchcodec/_core/DecodeAvif.cpp b/src/torchcodec/_core/DecodeAvif.cpp
index 17fd03a..b40b9b9 100644
--- a/src/torchcodec/_core/DecodeAvif.cpp
+++ b/src/torchcodec/_core/DecodeAvif.cpp
@@ -19,7 +19,8 @@ namespace facebook::torchcodec {
torch::stable::Tensor decode_avif(
[[maybe_unused]] const torch::stable::Tensor& input,
[[maybe_unused]] int64_t mode,
- [[maybe_unused]] int64_t output_dtype) {
+ [[maybe_unused]] int64_t output_dtype,
+ [[maybe_unused]] int64_t num_threads) {
STD_TORCH_CHECK(
false,
"decode_avif: torchcodec was not compiled with libavif support. Rebuild "
--
2.51.0
Loading