From 589efb6a10cccf82fa1f296eb5693cb7c12fc588 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Thu, 27 Aug 2026 14:24:49 +0200 Subject: [PATCH 1/3] pyinstaller: add build-pyinstaller.yml for riscv64 wheels PyInstaller publishes py3-none- wheels whose per-platform content is the compiled bootloader (PyInstaller/bootloader//{run,run_d}), built from the C sources in bootloader/src via waf. There is no riscv64 wheel on PyPI, so riscv64 users fall back to the sdist and have to compile the bootloader themselves. The workflow mirrors upstream's own release path: release/linux-bootloaders compiles the bootloader inside a per-architecture container, and release/build-wheels then runs hatchling with PYI_PLATFORM/PYI_WHEEL_TAG set for that architecture. PyInstaller already normalises riscv64 to the "riscv" bootloader directory (PyInstaller/_shared_with_waf.py), so no patch is needed. manylinux2014 is not defined for riscv64, hence the manylinux_2_39_riscv64 tag from the image's glibc. The wheel is interpreter-independent, so it is built once; the test job mirrors upstream's ubuntu-24.04 CI job (locale-gen plus tests/unit and tests/functional/test_basic.py, which freezes and runs 114 real applications through the new bootloader) across cp312/cp313/cp314. Co-Authored-By: Claude Opus 5 --- .github/workflows/build-pyinstaller.yml | 192 ++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 .github/workflows/build-pyinstaller.yml diff --git a/.github/workflows/build-pyinstaller.yml b/.github/workflows/build-pyinstaller.yml new file mode 100644 index 00000000..50f9d150 --- /dev/null +++ b/.github/workflows/build-pyinstaller.yml @@ -0,0 +1,192 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +# +# This workflow is based on: https://github.com/pyinstaller/pyinstaller/blob/develop/release/linux-bootloaders, +# https://github.com/pyinstaller/pyinstaller/blob/develop/release/build-wheels and +# https://github.com/pyinstaller/pyinstaller/blob/develop/.github/workflows/ci.yml +name: Build PyInstaller wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'PyInstaller version to build (git tag without leading v, e.g. 6.22.2)' + required: true + default: '6.22.2' + pull_request: + paths: + - '.github/workflows/build-pyinstaller.yml' + +concurrency: + group: ${{ github.workflow }}-${{ inputs.version || '6.22.2' }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +env: + PYINSTALLER_VERSION: ${{ inputs.version || '6.22.2' }} + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + # PyInstaller's own name for the bootloader directory on 64-bit RISC-V glibc + # (PyInstaller/_shared_with_waf.py maps machine "riscv*" to "riscv"). + PYI_PLATFORM: Linux-64bit-riscv + # Upstream ships manylinux2014_ wheels, but manylinux2014 is not defined + # for riscv64; the bootloader is built against the image's glibc 2.39 instead. + PYI_WHEEL_TAG: manylinux_2_39_riscv64 + +jobs: + build_wheel: + name: Build PyInstaller ${{ inputs.version || '6.22.2' }} py3-none-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + + steps: + - name: Checkout PyInstaller v${{ env.PYINSTALLER_VERSION }} + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: pyinstaller/pyinstaller + ref: v${{ env.PYINSTALLER_VERSION }} + persist-credentials: false + + - name: Compile bootloader and build wheel + shell: bash + run: | + podman run -t \ + --log-driver=none \ + --network=host \ + -v "$(pwd)":/workspace \ + --workdir /workspace \ + -e PYI_PLATFORM="${PYI_PLATFORM}" \ + -e PYI_WHEEL_TAG="${PYI_WHEEL_TAG}" \ + --pull=newer \ + "${MANYLINUX_RISCV64_IMAGE}" \ + bash -euxo pipefail -c ' + # libcmocka lives in Rocky 10 CRB, which manylinux enables. + dnf -y install libcmocka-devel + export PATH="/opt/python/cp312-cp312/bin:$PATH" + python -m pip install --upgrade hatchling + (cd bootloader && python ./waf --tests all) + python -m hatchling build -t wheel + ' + + - name: Verify wheel contents + shell: bash + run: | + python3 - dist/*.whl <<'EOF' + import struct + import sys + import zipfile + + wheel = sys.argv[1] + assert wheel.endswith("-py3-none-manylinux_2_39_riscv64.whl"), wheel + + archive = zipfile.ZipFile(wheel) + names = set(archive.namelist()) + + bootloaders = {name for name in names if name.startswith("PyInstaller/bootloader/Linux")} + expected = { + "PyInstaller/bootloader/Linux-64bit-riscv/run", + "PyInstaller/bootloader/Linux-64bit-riscv/run_d", + } + assert bootloaders == expected, bootloaders + + for name in sorted(bootloaders): + data = archive.read(name) + assert data[:4] == b"\x7fELF", name + e_machine, = struct.unpack_from(" Date: Thu, 27 Aug 2026 17:41:34 +0200 Subject: [PATCH 2/3] pyinstaller: deselect three interpreter-build assertions on 3.13/3.14 python-build-standalone links _ctypes into the interpreter from 3.13 on, so test_extension asserts a BuiltinModule is an Extension, and its Linux builds ship no libtcl/libtk shared object, so PyInstaller's splash builder cannot find one. Both reproduce on x86_64 with the same interpreters; 3.12 is unaffected because its PBS build has _ctypes as a shared module and no _tkinter at all, so it keeps running all three. --- .github/workflows/build-pyinstaller.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-pyinstaller.yml b/.github/workflows/build-pyinstaller.yml index 50f9d150..fd9bfda2 100644 --- a/.github/workflows/build-pyinstaller.yml +++ b/.github/workflows/build-pyinstaller.yml @@ -119,6 +119,20 @@ jobs: # Upstream's CI matrix has no free-threaded entry, and neither does the # set of interpreters it publishes wheels for, so cp314t is left out. python-version: ['3.12', '3.13', '3.14'] + include: + # python-build-standalone links _ctypes and Tcl/Tk statically into the + # 3.13+ interpreters setup-uv provisions, so these three assert on the + # interpreter build rather than on the wheel. Identical on x86_64. + - python-version: '3.13' + deselect: >- + --deselect tests/unit/test_modulegraph_more.py::test_extension + --deselect "tests/functional/test_basic.py::test_symlink_basename_is_kept[onedir-splash]" + --deselect "tests/functional/test_basic.py::test_symlink_basename_is_kept[onefile-splash]" + - python-version: '3.14' + deselect: >- + --deselect tests/unit/test_modulegraph_more.py::test_extension + --deselect "tests/functional/test_basic.py::test_symlink_basename_is_kept[onedir-splash]" + --deselect "tests/functional/test_basic.py::test_symlink_basename_is_kept[onefile-splash]" steps: - name: Checkout PyInstaller v${{ env.PYINSTALLER_VERSION }} @@ -171,7 +185,7 @@ jobs: PYTEST_ADDOPTS: --timeout=1800 run: | python -c "import PyInstaller; assert 'site-packages' in PyInstaller.__file__, PyInstaller.__file__; print(PyInstaller.__file__)" - python -m pytest -n 4 --durations 10 tests/unit tests/functional/test_basic.py + python -m pytest -n 4 --durations 10 ${{ matrix.deselect }} tests/unit tests/functional/test_basic.py publish: name: Publish PyInstaller ${{ inputs.version || '6.22.2' }} to GitLab From f4462b3891340fb993d9ec55bb12bc471b55829a Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Thu, 27 Aug 2026 18:57:22 +0200 Subject: [PATCH 3/3] pyinstaller: run the whole functional suite, not just test_basic Upstream's ci.yml runs `pytest tests/unit tests/functional`; the port was narrowed to tests/functional/test_basic.py. The library-dependent modules (Qt, numpy, scipy, PIL, gi, django, pywin32) all guard themselves with importorskip and skip cleanly with only tests/requirements-base.txt installed, so the rest of the suite runs unmodified. Four more deselects on the 3.13/3.14 entries, all of them properties of the python-build-standalone interpreter setup-uv provisions rather than of the wheel, and all reproducing on aarch64: test_binary_vs_data_reclassification.py::test_automatic_reclassification_binary copies _ctypes.__file__, which PBS builds into the interpreter test_multipackage.py::test_spec_with_multipackage its TOC expects a shared libssl beside the frozen app; PBS links OpenSSL statically into the interpreter test_libraries.py::test_idlelib test_libraries.py::test_tkinter_tcl_library_dir guarded on can_import_module("tkinter"), which succeeds under PBS while the Tcl/Tk shared libraries are absent, so they fail where test_splash's stricter tkinter_fully_usable guard skips Rehearsed in an arm64 ubuntu:24.04 container against a locally built wheel: 773 passed, 421 skipped, 20 xfailed, 1 xpassed, no failures. Co-Authored-By: Claude Opus 5 --- .github/workflows/build-pyinstaller.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-pyinstaller.yml b/.github/workflows/build-pyinstaller.yml index fd9bfda2..c426074d 100644 --- a/.github/workflows/build-pyinstaller.yml +++ b/.github/workflows/build-pyinstaller.yml @@ -120,19 +120,28 @@ jobs: # set of interpreters it publishes wheels for, so cp314t is left out. python-version: ['3.12', '3.13', '3.14'] include: - # python-build-standalone links _ctypes and Tcl/Tk statically into the - # 3.13+ interpreters setup-uv provisions, so these three assert on the - # interpreter build rather than on the wheel. Identical on x86_64. + # python-build-standalone, which setup-uv provisions for 3.13+, builds + # _ctypes and OpenSSL into the interpreter and ships no Tcl/Tk, so these + # assert on the interpreter build rather than on the wheel. Identical on + # x86_64; the 3.12 entry gets the runner's own interpreter and keeps them. - python-version: '3.13' deselect: >- --deselect tests/unit/test_modulegraph_more.py::test_extension --deselect "tests/functional/test_basic.py::test_symlink_basename_is_kept[onedir-splash]" --deselect "tests/functional/test_basic.py::test_symlink_basename_is_kept[onefile-splash]" + --deselect tests/functional/test_binary_vs_data_reclassification.py::test_automatic_reclassification_binary + --deselect tests/functional/test_multipackage.py::test_spec_with_multipackage + --deselect tests/functional/test_libraries.py::test_idlelib + --deselect tests/functional/test_libraries.py::test_tkinter_tcl_library_dir - python-version: '3.14' deselect: >- --deselect tests/unit/test_modulegraph_more.py::test_extension --deselect "tests/functional/test_basic.py::test_symlink_basename_is_kept[onedir-splash]" --deselect "tests/functional/test_basic.py::test_symlink_basename_is_kept[onefile-splash]" + --deselect tests/functional/test_binary_vs_data_reclassification.py::test_automatic_reclassification_binary + --deselect tests/functional/test_multipackage.py::test_spec_with_multipackage + --deselect tests/functional/test_libraries.py::test_idlelib + --deselect tests/functional/test_libraries.py::test_tkinter_tcl_library_dir steps: - name: Checkout PyInstaller v${{ env.PYINSTALLER_VERSION }} @@ -185,7 +194,7 @@ jobs: PYTEST_ADDOPTS: --timeout=1800 run: | python -c "import PyInstaller; assert 'site-packages' in PyInstaller.__file__, PyInstaller.__file__; print(PyInstaller.__file__)" - python -m pytest -n 4 --durations 10 ${{ matrix.deselect }} tests/unit tests/functional/test_basic.py + python -m pytest -n 4 --durations 10 ${{ matrix.deselect }} tests/unit tests/functional publish: name: Publish PyInstaller ${{ inputs.version || '6.22.2' }} to GitLab