From f6b759e052cfad77f63815d947c431b3f8a542b0 Mon Sep 17 00:00:00 2001 From: Trevor Gamblin Date: Tue, 30 Jun 2026 14:06:35 -0400 Subject: [PATCH 1/2] matplotlib: add build, test workflows for riscv64 Signed-off-by: Trevor Gamblin --- .github/workflows/build-matplotlib.yml | 123 +++++++++++++ .github/workflows/test-matplotlib.yml | 242 +++++++++++++++++++++++++ 2 files changed, 365 insertions(+) create mode 100644 .github/workflows/build-matplotlib.yml create mode 100644 .github/workflows/test-matplotlib.yml diff --git a/.github/workflows/build-matplotlib.yml b/.github/workflows/build-matplotlib.yml new file mode 100644 index 0000000..bb4393d --- /dev/null +++ b/.github/workflows/build-matplotlib.yml @@ -0,0 +1,123 @@ +--- +name: Build matplotlib wheels (riscv64) + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + +permissions: {} + +jobs: + build_sdist: + name: Build sdist + runs-on: ubuntu-24.04-riscv + permissions: + contents: read + outputs: + SDIST_NAME: ${{ steps.sdist.outputs.SDIST_NAME }} + + steps: + - name: Checkout python-wheels + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Checkout matplotlib + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: matplotlib/matplotlib + path: upstream + fetch-depth: 0 + persist-credentials: false + + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + name: Install Python + with: + python-version: '3.12' + activate-environment: true + enable-cache: false + + # Something changed somewhere that prevents the downloaded-at-build-time + # licenses from being included in built wheels, so pre-download them so + # that they exist before the build and are included. + - name: Pre-download bundled licenses + working-directory: upstream + run: > + curl -Lo LICENSE/LICENSE_QHULL + https://github.com/qhull/qhull/raw/2020.2/COPYING.txt + + - name: Install dependencies + run: python -m pip install build twine + + - name: Build sdist + id: sdist + working-directory: upstream + run: | + python -m build --sdist + python ci/export_sdist_name.py + + - name: Check README rendering for PyPI + working-directory: upstream + run: twine check dist/* + + - name: Upload sdist result + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cibw-sdist + path: upstream/dist/*.tar.gz + if-no-files-found: error + + build_wheels: + needs: build_sdist + name: Build wheels on ubuntu-24.04-riscv for riscv64 + permissions: + contents: read + runs-on: ubuntu-24.04-riscv + + steps: + - name: Checkout python-wheels + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Download sdist + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cibw-sdist + path: dist/ + + - name: Build wheels for CPython 3.14 + uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 + with: + package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} + env: + CIBW_BUILD: "cp314-* cp314t-*" + CIBW_ARCHS: "riscv64" + CIBW_ENVIRONMENT: 'PIP_PREFER_BINARY=1 PIP_EXTRA_INDEX_URL=https://gitlab.com/api/v4/projects/riseproject%2Fpython%2Fwheel_builder/packages/pypi/simple' + + - name: Build wheels for CPython 3.13 + uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 + with: + package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} + env: + CIBW_BUILD: "cp313-*" + CIBW_ARCHS: "riscv64" + CIBW_ENVIRONMENT: 'PIP_PREFER_BINARY=1 PIP_EXTRA_INDEX_URL=https://gitlab.com/api/v4/projects/riseproject%2Fpython%2Fwheel_builder/packages/pypi/simple' + + - name: Build wheels for CPython 3.12 + uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 + with: + package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} + env: + CIBW_BUILD: "cp312-*" + CIBW_ARCHS: "riscv64" + CIBW_ENVIRONMENT: 'PIP_PREFER_BINARY=1 PIP_EXTRA_INDEX_URL=https://gitlab.com/api/v4/projects/riseproject%2Fpython%2Fwheel_builder/packages/pypi/simple' + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cibw-wheels-${{ runner.os }}-riscv64 + path: ./wheelhouse/*.whl + if-no-files-found: error diff --git a/.github/workflows/test-matplotlib.yml b/.github/workflows/test-matplotlib.yml new file mode 100644 index 0000000..3871f58 --- /dev/null +++ b/.github/workflows/test-matplotlib.yml @@ -0,0 +1,242 @@ +--- +name: Test matplotlib (riscv64) +concurrency: + group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.ref }} + cancel-in-progress: true + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + +permissions: {} + +env: + NO_AT_BRIDGE: 1 # Necessary for GTK3 interactive test. + OPENBLAS_NUM_THREADS: 1 + PYTHONFAULTHANDLER: 1 + +jobs: + test: + permissions: + contents: read + name: "Python ${{ matrix.python-version }} on ${{ matrix.os }}" + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: upstream + + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04-riscv + python-version: '3.12' + + steps: + - name: Checkout python-wheels + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Checkout matplotlib + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: matplotlib/matplotlib + path: upstream + fetch-depth: 0 + persist-credentials: false + + - name: Set up Python ${{ matrix.python-version }} + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + activate-environment: true + enable-cache: false + + - name: Install OS dependencies + run: | + echo 'Acquire::Retries "3";' | sudo tee /etc/apt/apt.conf.d/80-retries + sudo apt-get update -yy + sudo apt-get install -yy --no-install-recommends \ + ccache \ + cm-super \ + dvipng \ + fonts-freefont-otf \ + fonts-noto-cjk \ + fonts-wqy-zenhei \ + gdb \ + gir1.2-gtk-3.0 \ + gir1.2-gtk-4.0 \ + graphviz \ + inkscape \ + language-pack-de \ + lcov \ + libcairo2 \ + libcairo2-dev \ + libffi-dev \ + libgeos-dev \ + libnotify4 \ + libsdl2-2.0-0 \ + libxkbcommon-x11-0 \ + libxcb-cursor0 \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-randr0 \ + libxcb-render-util0 \ + libxcb-xinerama0 \ + lmodern \ + ninja-build \ + pkg-config \ + qtbase5-dev \ + texlive-fonts-recommended \ + texlive-latex-base \ + texlive-latex-extra \ + texlive-latex-recommended \ + texlive-luatex \ + texlive-pictures \ + texlive-xetex + sudo apt-get install -yy --no-install-recommends ffmpeg poppler-utils + sudo apt-get install -yy --no-install-recommends libgirepository-2.0-dev + + - name: Cache pip + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.cache/pip + key: | + ${{ matrix.os }}-py${{ matrix.python-version }}-pip-${{ + hashFiles('upstream/pyproject.toml', 'upstream/ci/minver-requirements.txt') }} + restore-keys: | + ${{ matrix.os }}-py${{ matrix.python-version }}-pip- + - name: Cache ccache + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + ~/.ccache + key: ${{ matrix.os }}-py${{ matrix.python-version }}-ccache-${{ hashFiles('upstream/src/*') }} + restore-keys: | + ${{ matrix.os }}-py${{ matrix.python-version }}-ccache- + - name: Cache Matplotlib + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + ~/.cache/matplotlib + !~/.cache/matplotlib/tex.cache + !~/.cache/matplotlib/test_cache + key: 6-${{ matrix.os }}-py${{ matrix.python-version }}-mpl-${{ github.ref }}-${{ github.sha }} + restore-keys: | + 6-${{ matrix.os }}-py${{ matrix.python-version }}-mpl-${{ github.ref }}- + 6-${{ matrix.os }}-py${{ matrix.python-version }}-mpl- + + - name: Install Python dependencies + run: | + # Upgrade pip and setuptools and wheel to get as clean an install as + # possible. + python -m pip install --upgrade pip setuptools wheel + + # Install dependencies from PyPI. + # Preinstall build requirements to enable no-build-isolation builds. + python -m pip install --upgrade --prefer-binary \ + --group build --group test + + # Install optional dependencies from PyPI. + # Sphinx is needed to run sphinxext tests + python -m pip install --upgrade sphinx!=6.1.2 + + # GUI toolkits are pip-installable only for some versions of Python + # so don't fail if we can't install them. Make it easier to check + # whether the install was successful by trying to import the toolkit + # (sometimes, the install appears to be successful but shared + # libraries cannot be loaded at runtime, so an actual import is a + # better check). + python -m pip install --upgrade pycairo 'cairocffi>=0.8' 'PyGObject' && + ( + python -c 'import gi; gi.require_version("Gtk", "4.0"); from gi.repository import Gtk' && + echo 'PyGObject 4 is available' || echo 'PyGObject 4 is not available' + ) && ( + python -c 'import gi; gi.require_version("Gtk", "3.0"); from gi.repository import Gtk' && + echo 'PyGObject 3 is available' || echo 'PyGObject 3 is not available' + ) + + # PyQt5 has no riscv64 wheels; skip. + python -mpip install --upgrade --only-binary :all: pyqt6 && + python -c 'import PyQt6.QtCore' && + echo 'PyQt6 is available' || + echo 'PyQt6 is not available' + python -mpip install --upgrade --only-binary :all: pyside6 && + python -c 'import PySide6.QtCore' && + echo 'PySide6 is available' || + echo 'PySide6 is not available' + + - name: Install Matplotlib + run: | + ccache -s + git describe + + export CPPFLAGS='--coverage -fprofile-abs-path' + + python -m pip install --no-deps --no-build-isolation --verbose \ + --config-settings=setup-args="-DrcParams-backend=Agg" \ + --editable .[dev] + + unset CPPFLAGS + + - name: Run pytest + run: | + pytest -rfEsXR -n auto \ + --maxfail=50 --timeout=300 --durations=25 \ + --cov-report=xml --cov=lib --log-level=DEBUG --color=yes + + - name: Cleanup non-failed image files + if: failure() + run: | + find ./result_images -name "*-expected*.png" | while read file; do + if [[ $file == *-expected_???.png ]]; then + extension=${file: -7:3} + base=${file%*-expected_$extension.png}_$extension + else + extension="png" + base=${file%-expected.png} + fi + if [[ ! -e ${base}-failed-diff.png ]]; then + indent="" + list=($file $base.png) + if [[ $extension != "png" ]]; then + list+=(${base%_$extension}-expected.$extension ${base%_$extension}.$extension) + fi + for to_remove in "${list[@]}"; do + if [[ -e $to_remove ]]; then + rm $to_remove + echo "${indent}Removed $to_remove" + fi + indent+=" " + done + fi + done + + if [ "$(find ./result_images -mindepth 1 -type d)" ]; then + find ./result_images/* -type d -empty -delete + fi + + - name: Filter C coverage + if: ${{ !cancelled() }} + run: | + LCOV_IGNORE_ERRORS='mismatch' + lcov --rc lcov_branch_coverage=1 --ignore-errors $LCOV_IGNORE_ERRORS \ + --capture --directory . --exclude $PWD/subprojects --exclude $PWD/build \ + --output-file coverage.info + lcov --rc lcov_branch_coverage=1 --ignore-errors $LCOV_IGNORE_ERRORS \ + --output-file coverage.info --extract coverage.info $PWD/src/'*' + lcov --rc lcov_branch_coverage=1 --ignore-errors $LCOV_IGNORE_ERRORS \ + --list coverage.info + find . -name '*.gc*' -delete + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: failure() + with: + name: "${{ matrix.python-version }} ${{ matrix.os }} result images" + path: upstream/result_images From 5816bb0b10374c7e2e56cb732f92218375cc7b8c Mon Sep 17 00:00:00 2001 From: Trevor Gamblin Date: Tue, 30 Jun 2026 14:13:50 -0400 Subject: [PATCH 2/2] matplotlib: use uv - replace 'python -m pip' with 'uv pip' - set cache to match uv usage - use uv for CIBW_BUILD_FRONTEND - set UV_EXTRA_INDEX_URL to our GitLab registry, with UV_INDEX_STRATEGY=unsafe-best-match and UV_ONLY_BINARY=:all: so we pick whichever source has a binary wheel - drop 'allow-prereleases' from setup-uv since this only applies to the setup-python action Signed-off-by: Trevor Gamblin --- .github/workflows/build-matplotlib.yml | 30 ++++++++++++++++++++---- .github/workflows/test-matplotlib.yml | 32 ++++++++++++++++---------- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-matplotlib.yml b/.github/workflows/build-matplotlib.yml index bb4393d..6bb7df1 100644 --- a/.github/workflows/build-matplotlib.yml +++ b/.github/workflows/build-matplotlib.yml @@ -10,6 +10,16 @@ on: permissions: {} +env: + # Query PyPI + gitlab riscv64 mirror; pick best compatible wheel + # (`unsafe-best-match` = pip-like). `first-index` stops at PyPI for sdist + # packages like cffi, never consulting gitlab for the riscv64 wheel. + UV_EXTRA_INDEX_URL: https://gitlab.com/api/v4/projects/riseproject%2Fpython%2Fwheel_builder/packages/pypi/simple + UV_INDEX_STRATEGY: unsafe-best-match + # Refuse sdist for dependencies — wheels only. Forces uv to pick the gitlab + # riscv64 wheel instead of falling back to building cffi/cryptography/etc. + UV_ONLY_BINARY: ':all:' + jobs: build_sdist: name: Build sdist @@ -50,7 +60,7 @@ jobs: https://github.com/qhull/qhull/raw/2020.2/COPYING.txt - name: Install dependencies - run: python -m pip install build twine + run: uv pip install build twine - name: Build sdist id: sdist @@ -96,7 +106,11 @@ jobs: env: CIBW_BUILD: "cp314-* cp314t-*" CIBW_ARCHS: "riscv64" - CIBW_ENVIRONMENT: 'PIP_PREFER_BINARY=1 PIP_EXTRA_INDEX_URL=https://gitlab.com/api/v4/projects/riseproject%2Fpython%2Fwheel_builder/packages/pypi/simple' + CIBW_BUILD_FRONTEND: "build[uv]" + CIBW_ENVIRONMENT: >- + UV_EXTRA_INDEX_URL=https://gitlab.com/api/v4/projects/riseproject%2Fpython%2Fwheel_builder/packages/pypi/simple + UV_INDEX_STRATEGY=unsafe-best-match + UV_ONLY_BINARY=:all: - name: Build wheels for CPython 3.13 uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 @@ -105,7 +119,11 @@ jobs: env: CIBW_BUILD: "cp313-*" CIBW_ARCHS: "riscv64" - CIBW_ENVIRONMENT: 'PIP_PREFER_BINARY=1 PIP_EXTRA_INDEX_URL=https://gitlab.com/api/v4/projects/riseproject%2Fpython%2Fwheel_builder/packages/pypi/simple' + CIBW_BUILD_FRONTEND: "build[uv]" + CIBW_ENVIRONMENT: >- + UV_EXTRA_INDEX_URL=https://gitlab.com/api/v4/projects/riseproject%2Fpython%2Fwheel_builder/packages/pypi/simple + UV_INDEX_STRATEGY=unsafe-best-match + UV_ONLY_BINARY=:all: - name: Build wheels for CPython 3.12 uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1 @@ -114,7 +132,11 @@ jobs: env: CIBW_BUILD: "cp312-*" CIBW_ARCHS: "riscv64" - CIBW_ENVIRONMENT: 'PIP_PREFER_BINARY=1 PIP_EXTRA_INDEX_URL=https://gitlab.com/api/v4/projects/riseproject%2Fpython%2Fwheel_builder/packages/pypi/simple' + CIBW_BUILD_FRONTEND: "build[uv]" + CIBW_ENVIRONMENT: >- + UV_EXTRA_INDEX_URL=https://gitlab.com/api/v4/projects/riseproject%2Fpython%2Fwheel_builder/packages/pypi/simple + UV_INDEX_STRATEGY=unsafe-best-match + UV_ONLY_BINARY=:all: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/.github/workflows/test-matplotlib.yml b/.github/workflows/test-matplotlib.yml index 3871f58..cdd0811 100644 --- a/.github/workflows/test-matplotlib.yml +++ b/.github/workflows/test-matplotlib.yml @@ -17,6 +17,15 @@ env: NO_AT_BRIDGE: 1 # Necessary for GTK3 interactive test. OPENBLAS_NUM_THREADS: 1 PYTHONFAULTHANDLER: 1 + # Query PyPI + gitlab riscv64 mirror; pick best compatible wheel. + # `first-index` is wrong here — PyPI has cffi sdist so uv would never check + # gitlab for the riscv64 wheel. `unsafe-best-match` is pip-like and selects + # the riscv64 wheel from gitlab when PyPI lacks one. + UV_EXTRA_INDEX_URL: https://gitlab.com/api/v4/projects/riseproject%2Fpython%2Fwheel_builder/packages/pypi/simple + UV_INDEX_STRATEGY: unsafe-best-match + # Refuse sdist for dependencies — wheels only. Forces uv to pick the gitlab + # riscv64 wheel instead of falling back to building cffi/cryptography/etc. + UV_ONLY_BINARY: ':all:' jobs: test: @@ -53,7 +62,6 @@ jobs: uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: python-version: ${{ matrix.python-version }} - allow-prereleases: true activate-environment: true enable-cache: false @@ -103,15 +111,15 @@ jobs: sudo apt-get install -yy --no-install-recommends ffmpeg poppler-utils sudo apt-get install -yy --no-install-recommends libgirepository-2.0-dev - - name: Cache pip + - name: Cache uv uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: - path: ~/.cache/pip + path: ~/.cache/uv key: | - ${{ matrix.os }}-py${{ matrix.python-version }}-pip-${{ + ${{ matrix.os }}-py${{ matrix.python-version }}-uv-${{ hashFiles('upstream/pyproject.toml', 'upstream/ci/minver-requirements.txt') }} restore-keys: | - ${{ matrix.os }}-py${{ matrix.python-version }}-pip- + ${{ matrix.os }}-py${{ matrix.python-version }}-uv- - name: Cache ccache uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: @@ -136,16 +144,16 @@ jobs: run: | # Upgrade pip and setuptools and wheel to get as clean an install as # possible. - python -m pip install --upgrade pip setuptools wheel + uv pip install --upgrade pip setuptools wheel # Install dependencies from PyPI. # Preinstall build requirements to enable no-build-isolation builds. - python -m pip install --upgrade --prefer-binary \ + uv pip install --upgrade --prefer-binary \ --group build --group test # Install optional dependencies from PyPI. # Sphinx is needed to run sphinxext tests - python -m pip install --upgrade sphinx!=6.1.2 + uv pip install --upgrade sphinx!=6.1.2 # GUI toolkits are pip-installable only for some versions of Python # so don't fail if we can't install them. Make it easier to check @@ -153,7 +161,7 @@ jobs: # (sometimes, the install appears to be successful but shared # libraries cannot be loaded at runtime, so an actual import is a # better check). - python -m pip install --upgrade pycairo 'cairocffi>=0.8' 'PyGObject' && + uv pip install --upgrade pycairo 'cairocffi>=0.8' 'PyGObject' && ( python -c 'import gi; gi.require_version("Gtk", "4.0"); from gi.repository import Gtk' && echo 'PyGObject 4 is available' || echo 'PyGObject 4 is not available' @@ -163,11 +171,11 @@ jobs: ) # PyQt5 has no riscv64 wheels; skip. - python -mpip install --upgrade --only-binary :all: pyqt6 && + uv pip install --upgrade --only-binary :all: pyqt6 && python -c 'import PyQt6.QtCore' && echo 'PyQt6 is available' || echo 'PyQt6 is not available' - python -mpip install --upgrade --only-binary :all: pyside6 && + uv pip install --upgrade --only-binary :all: pyside6 && python -c 'import PySide6.QtCore' && echo 'PySide6 is available' || echo 'PySide6 is not available' @@ -179,7 +187,7 @@ jobs: export CPPFLAGS='--coverage -fprofile-abs-path' - python -m pip install --no-deps --no-build-isolation --verbose \ + uv pip install --no-deps --no-build-isolation --verbose \ --config-settings=setup-args="-DrcParams-backend=Agg" \ --editable .[dev]