Skip to content
Merged
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
156 changes: 156 additions & 0 deletions .github/workflows/build-pillow-jxl-plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# This workflow is based on the `linux` job of
# https://github.com/Isotr0py/pillow-jpegxl-plugin/blob/v1.3.8/.github/workflows/release.yml
name: Build pillow-jxl-plugin wheels (riscv64)

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

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: pillow-jxl-plugin
version: ${{ inputs.version }}

build_wheels:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
name: Build pillow-jxl-plugin ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
# Cargo.toml has `pyo3 = { version="0.29.0", features=["extension-module"] }`
# with no abi3 feature, matching upstream's own per-interpreter release
# matrix (MATURIN_INTERPRETERS: "3.10 3.11 3.12 3.13 3.14 pypy3.11").
# pp311 is dropped: PyPy has no riscv64 port.
python: ["cp310", "cp311", "cp312", "cp313", "cp314"]

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

steps:
- name: Checkout pillow-jxl-plugin v${{ env.PILLOW_JXL_PLUGIN_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Isotr0py/pillow-jpegxl-plugin
ref: v${{ env.PILLOW_JXL_PLUGIN_VERSION }}
persist-credentials: false

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

- name: Patch pillow-jxl-plugin source
run: git apply python-wheels/patches/pillow-jxl-plugin/${{ env.PILLOW_JXL_PLUGIN_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 }}
# [tool.maturin] already has `features = [..., "vendored"]`, which
# builds libjxl/highway/brotli from source via jpegxl-src's CMake
# invocation - no MATURIN_PEP517_ARGS needed. `cargo metadata
# --features vendored` unpacks jpegxl-src (an optional dependency
# gated by that feature) so its bundled licences can be staged here
# for the widened `license-files` glob to pick up.
CIBW_BEFORE_ALL_LINUX: >-
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y &&
export PATH="$PATH:$HOME/.cargo/bin" &&
cargo metadata --manifest-path {package}/Cargo.toml --features vendored --format-version=1 >/dev/null &&
JXL_SRC="$(find "$HOME/.cargo/registry/src" -maxdepth 2 -type d -name 'jpegxl-src-*' | head -1)" &&
cp "$JXL_SRC/libjxl/LICENSE" {package}/LICENSE.libjxl &&
cp "$JXL_SRC/libjxl/PATENTS" {package}/PATENTS.libjxl &&
cp "$JXL_SRC/libjxl/third_party/highway/LICENSE" {package}/LICENSE.highway &&
cp "$JXL_SRC/libjxl/third_party/brotli/LICENSE" {package}/LICENSE.brotli &&
cp "$JXL_SRC/libjxl/third_party/skcms/LICENSE" {package}/LICENSE.skcms
# PIP_ONLY_BINARY=pillow: cp310/cp311 only have an 11.3.0/12.2.0
# riscv64 Pillow wheel on our registry (Pillow 12.0+ dropped
# cp310, 12.3.0 dropped cp311); without this, pip's resolver
# prefers the newest Pillow sdist visible on the combined
# index over the older prebuilt wheel, and that sdist then
# fails to build for lack of libjpeg headers in the test venv.
CIBW_ENVIRONMENT_LINUX: >-
PATH="$PATH:$HOME/.cargo/bin"
PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/
PIP_ONLY_BINARY=pillow
CIBW_TEST_REQUIRES: pytest numpy
CIBW_TEST_SOURCES: test
# test/test_plugin.py's OpenEXR/pyexiv2-gated tests are patched to
# pytest.importorskip: pyexiv2 has no riscv64 wheel on our registry
# at all, and OpenEXR has none for cp310/cp311.
CIBW_TEST_COMMAND: >-
python -c "import pillow_jxl; from pillow_jxl import JpegXLImagePlugin" &&
python -m pytest test/test_plugin.py -v

- name: Check the wheel contents
run: |
python3 - wheelhouse/*.whl <<'EOF'
import sys, zipfile
for whl in sys.argv[1:]:
names = zipfile.ZipFile(whl).namelist()
assert any(n.startswith("pillow_jxl/pillow_jxl") and n.endswith(".so") for n in names), whl
licenses = sorted(n.rsplit("/", 1)[1] for n in names if ".dist-info/licenses/" in n)
print(whl, "licenses:", licenses)
for expect in ("LICENSE.libjxl", "PATENTS.libjxl", "LICENSE.highway", "LICENSE.brotli", "LICENSE.skcms"):
assert expect in licenses, (whl, expect, licenses)
print(whl, "ok")
EOF

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

publish:
name: Publish pillow-jxl-plugin ${{ 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: pillow-jxl-plugin-${{ matrix.version }}-*-manylinux_riscv64
5 changes: 5 additions & 0 deletions docs/packages/pillow-jxl-plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package-name: pillow-jxl-plugin
source-code: https://github.com/Isotr0py/pillow-jpegxl-plugin
license: GPL-3.0-or-later
versions:
- version: 1.3.8
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
From 59744a7bd9210d6c7099d5bbbbecdeb0e21914a6 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Mon, 5 Oct 2026 00:00:00 +0000
Subject: [PATCH] test: make OpenEXR and pyexiv2 imports lazy

OpenEXR (test_decode_F16) and pyexiv2 (the metadata-encode tests) are
unconditional module-level imports, so the whole test file fails to
collect wherever either package is absent. On riscv64, pyexiv2 has no
wheel on pypi.riseproject.dev at all, and OpenEXR has none for the
cp310/cp311 interpreters this port tests, so collection failed before
a single test ran. Move both imports into the functions that use them
and use pytest.importorskip, so unrelated tests still collect and run
and only the dependency-gated ones are skipped.

Upstream-Status: Inappropriate [riscv64 test environment has no pyexiv2 wheel at all, and no OpenEXR wheel for some of the interpreters this port tests]
---
test/test_plugin.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/test/test_plugin.py b/test/test_plugin.py
index 38594ad..b65ad98 100644
--- a/test/test_plugin.py
+++ b/test/test_plugin.py
@@ -2,8 +2,6 @@ import os
import tempfile

import numpy as np
-import OpenEXR
-import pyexiv2
import pytest
from PIL import Image

@@ -50,6 +48,7 @@ def test_decode_F():


def test_decode_F16():
+ OpenEXR = pytest.importorskip("OpenEXR")
img_jxl = Image.open("test/images/random_image_f16.jxl")
assert img_jxl.mode == "RGB"

@@ -116,6 +115,7 @@ def test_metadata_decode_from_direct_decoding():


def test_metadata_encode_from_jpg():
+ pyexiv2 = pytest.importorskip("pyexiv2")
# Load a JPEG image
ref_img_path = "test/images/metadata/1x1_exif_xmp.jpg"
temp = tempfile.mktemp(suffix=".jxl")
@@ -130,6 +130,7 @@ def test_metadata_encode_from_jpg():


def test_metadata_encode_from_raw_exif():
+ pyexiv2 = pytest.importorskip("pyexiv2")
with open("test/images/metadata/sample.exif", "rb") as f:
ref_exif = f.read()
img_ori = Image.open("test/images/sample.png")
@@ -142,6 +143,7 @@ def test_metadata_encode_from_raw_exif():


def test_metadata_encode_from_pil_exif():
+ pyexiv2 = pytest.importorskip("pyexiv2")
exif_img_path = "test/images/metadata/1x1_exif_xmp.jpg"
dummy_img = Image.open("test/images/sample.png")
exif_img = Image.open(exif_img_path)
--
2.43.0

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
From f15842a37a57fbb3d7abe8cdd0c21ebe9625bf44 Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Mon, 5 Oct 2026 00:00:00 +0000
Subject: [PATCH] pyproject: widen license-files to cover vendored libraries

license-files = ["LICENSE"] is an explicit PEP 639 list with no glob
default behind it, so dropping a LICENSE.<lib> file at the project root
for a vendored C/C++ library compiled into the extension is silently
ignored. The vendored feature statically links libjxl (BSD-3-Clause,
with its own PATENTS grant), highway (Apache-2.0 / BSD-3-Clause dual
licence), brotli (MIT) and skcms (BSD-3-Clause), yet only the plugin's
own GPL-3.0-or-later LICENSE lands in dist-info/licenses. Widen the
glob so the build can stage those files under matching names.

Upstream-Status: Inappropriate [this build vendors libjxl/highway/brotli/skcms from source via the vendored cargo feature; upstream's own release wheels either dynamically link a system libjxl or statically link it without staging the dependencies' licence files either, so this is specific to how this port assembles the wheel]
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyproject.toml b/pyproject.toml
index c9393dd..21e6580 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ authors = [
description = "Pillow plugin for JPEG-XL, using Rust for bindings."
readme = "README.md"
license = "GPL-3.0-or-later"
-license-files = ["LICENSE"]
+license-files = ["LICENSE", "LICENSE.*", "PATENTS.*"]
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
--
2.43.0

Loading