Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bindings: python: add a script to generate armv7l wheels #93

Closed
wants to merge 1 commit into from

Conversation

pdcastro
Copy link

@pdcastro pdcastro commented Sep 2, 2024

Add a generate_armv7l_wheels.sh shell script that makes armv7l Python wheels for
combinations of libc (glibc, musl) and selected CPython versions.

Currently, Python wheels are generated for the x86_64 and aarch64 CPU architectures only, using the cibuildwheel tool invoked by the existing generate_pypi_artifacts.sh script. The cibuildwheel tool, in turn, relies on the PyPA manylinux and musllinux Docker images that, sadly, are not available for the armv7l architecture that is still common in edge devices.

It was previously suggested that Raspberry Pi users could rely on the piwheels.org repository for armv7l gpiod wheels. Alas, that repository lags significantly behind the latest CPython releases. It currently provides gpiod wheels for CPython versions 3.9 and 3.11 only, while CPython 3.12 was released a year ago and CPython 3.13 is about to be released.

It turns out that armv7l Python wheels can be relatively easily generated without cibuildwheel, in about 160 lines of structured shell script that introduces no new dependencies (the script depends on Docker and binfmt_misc only). Instead of manylinux and musllinux Docker images, the script uses the official Python image on Docker Hub, which is available for armv7l in Debian and Alpine flavours (for glibc and musl libc respectively).

This PR introduces such a shell script.

Sample usage:

$ LIBGPIOD_VERSION=2.1.3 ./generate_armv7l_wheels.sh

... ('docker build' and 'docker run' output...)

86K  gpiod-2.2.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
87K  gpiod-2.2.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
89K  gpiod-2.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
88K  gpiod-2.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
87K  gpiod-2.2.0-cp39-cp39-musllinux_1_2_armv7l.whl
88K  gpiod-2.2.0-cp310-cp310-musllinux_1_2_armv7l.whl
90K  gpiod-2.2.0-cp311-cp311-musllinux_1_2_armv7l.whl
90K  gpiod-2.2.0-cp312-cp312-musllinux_1_2_armv7l.whl

All done! Hint: Use 'docker system prune' to free disk space.

(The LIBGPIOD_VERSION env var is only needed if the SDist file hasn’t been created yet.)

@pdcastro
Copy link
Author

pdcastro commented Sep 2, 2024

Hey @brgl, here's a script that makes armv7l (32-bit ARM) Python wheels, which I have been using in my own projects. The script passes shellcheck checks without any warnings or errors.

The first question is whether you think that it makes sense to add it to the libgpiod repo in order for you to produce armv7l wheels and upload them to PyPI when you create new releases. If so, my next question will be about testing. I have only tested the wheels on my own projects that make very basic use of libgpiod (reading and writing GPIO pins), but I see that the Python bindings have unit tests that can use a chip simulator. Anyway, one question at a time. :)

@brgl
Copy link
Owner

brgl commented Sep 3, 2024

Cc'ing @vfazio

@pdcastro We already have a script for generating wheels at bindings/python/generate_pypi_artifacts.sh, can you extend it?

@vfazio
Copy link
Contributor

vfazio commented Sep 3, 2024

I'm not opposed to this in principle, however I would like to reiterate the concerns from:

#62 (comment)

#62 (comment)

The purpose of publishing wheels on PyPi is to provide wheels that are compatible for all boards for the architecture target. 32bit ARM wheels are not prolific in the ecosystem after all of these years probably because ensuring compatibility has been difficult.

pypa/manylinux#84 (comment)

pypa/manylinux#84 (comment)

pypa/manylinux#84 (comment)

If your target is RPi, the wheels published on piwheels should generally be sufficient. They are purpose built for that platform and should be as easy as pointing to the index either via pip or your package manager. Wheels on piwheels are not cross platform compatible per pypa/manylinux#1405 (comment)

Also, armv7l is one subset of the 32bit ARM architecture family. This does not account for armv6l or armv8l and does not account for any optionally licensed features such as NEON or VFP. armv6l with no optional extensions enabled should technically be compatible through armv8l. I'm not sure whether the python package resolvers would understand that or not.

pypa/manylinux#1405 (comment)

I believe most of the variance exists in how floating point is handled, so gpiod may not necessarily be affected, but it's important because building wheels has the chance of packing in linked libraries from the build host that are then referenced via -rpath. If those libraries are built with unsupported instructions and executed, it could crash... a problem i've definitely had with PPC and their 32bit architectures.

You'd have to ensure that gpiod and dependent host libraries are not compiled with VFP, NEON/SIMD, etc since these are all optional addons to the core ARMv7 instruction set. An x86_64 analog would be things like AVX/AVX2/AVX512/SSE/SSE2 support

This is not an issue with 64bit ARM because aarch64 is a common subset for all armv8 chips. It's akin to having specified one of the x86_64 psABIs where you can guarantee a subset of instructions are supported. The 32bit ARM descriptors do not encompass this.

If there is still a taste for this, then I would suggest providing evidence that these wheels work across a suite of armv7 chips. What i don't want to have happen is we upload these and then have a support nightmare because they don't work on someone's esoteric chipset. I'm also not confident how Python's packaging utilities handle installing packages on 32bit arm and how well tested that is. it looks like packaging has a quirk to yield armv7l on armv8l, so maybe it works fine.

Anyway, I think these are some of the things that need to be considered before accepting this PR.

Add a 'generate_armv7l_wheels.sh' shell script that makes armv7l Python
wheels for combinations of libc (glibc, musl) and selected CPython
versions.

Currently, Python wheels are generated for the x86_64 and aarch64 CPU
architectures only, using the 'cibuildwheel' tool invoked by the existing
'generate_pypi_artifacts.sh' script. The 'cibuildwheel' tool, in turn,
relies on the PyPA manylinux and musllinux Docker images that, sadly, are
not available for the armv7l architecture that is still common in edge
devices.

It was previously suggested that Raspberry Pi users could rely on the
piwheels.org repository for armv7l gpiod wheels. Alas, that repository
lags significantly behind the latest CPython releases. It currently
provides gpiod wheels for CPython versions 3.9 and 3.11 only, while
CPython 3.12 was released a year ago and CPython 3.13 is about to be
released.

It turns out that armv7l Python wheels can be relatively easily generated
without 'cibuildwheel', in about 160 lines of structured shell script that
introduces no new dependencies (the script depends on Docker and
binfmt_misc only). Instead of manylinux and musllinux Docker images, the
script uses the official Python image on Docker Hub, which is available
for armv7l in Debian and Alpine flavours (for glibc and musl libc
respectively).

Signed-off-by: Paulo Ferreira de Castro <[email protected]>
@pdcastro pdcastro changed the title bindings: python: add a script to generate 32-bit ARM (armv7l) wheels bindings: python: add a script to generate armv7l wheels Oct 6, 2024
@pdcastro
Copy link
Author

pdcastro commented Oct 6, 2024

@vfazio, @brgl, thank you very much for your informative comments. I read all the links and learned several things.

Refined goal
I am changing the title of this PR and the commit message to remove references to “32-bit ARM” and leave only references to armv7l (the ‘l’ stands for little endian). The goal is to provide wheels that work with ARM v7 hardware and Linux distros (shared library ABI), with manylinux and musllinux tags. As expanded below, this also means targeting only hard-float systems — ARM v7 CPUs with the Vector Floating Point (VFP) instruction set extension, and Linux distros based on a libc compiled for the VFP extension. I have also drafted a note to be added to the bindings/Python/README.md file regarding armv7l wheels.

PiWheels
Regarding piwheels.org, while my present target is indeed the Raspberry Pi, PiWheels is lagging significantly behind the latest Python releases: Python 3.12 was released a year ago, Python 3.13 is about to be released, but PiWheels still only provides wheels for Python 3.11 and older. Many developers like me would like to take advantage of the latest Python release in their projects, and gpiod wheels need to be compiled for Python’s minor version (the ‘x’ in ‘3.x.y’).

By the way, in some future issue / PR, we could investigate the possibility of gpiod wheels making use of Python’s Limited C API only, so that a single wheel tagged abi3 would work across all 3.x minor versions, avoiding the need for separate wheels tagged cp39, cp310, cp311... If this were the case, presumably PiWheels could publish the abi3 tagged wheel that would automatically be compatible with the latest 3.x Python releases — like perhaps the scyllaft wheels did it. Currently, however, the libgpiod Python binding C source code appears to make use of API items outside the Limited C API, for example PyObject_HEAD, PyUnicode_AsUTF8 and PyObject_CallFinalizerFromDealloc.

How pip selects a wheel
I have learned that pip install parses a wheel’s file name in order to decide whether wheels published to PyPi (or PiWheels) are compatible with the local system, while the auditwheel repair command (used by cibuildwheel and by the script introduced in this PR) renames a wheel’s file name in accordance with the manylinux and musllinux standards.

For example, given a wheel file named:
gpiod-2.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl

... pip install will match the armv7l tag in the file name against the local system using Python’s sysconfig.get_platform() (pip source: tags.py L487), which in turn is compatible with os.uname(), which appears to be compatible with the Linux uname -m command output.

Of interest, as expected, this means that a wheel tagged armv7l will be ignored on an ARM v6 (or lower) system where uname -m reports for example armv6l.

Soft vs. hard float
In the case of a manylinux armv7l wheel, pip’s source has a hardcoded requirement of a Python interpreter compiled for hard float, and consequently typically a Linux distro’s glibc also compiled for hard float:

pip source: _manylinux.py L56

def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
    if "armv7l" in archs:
        return _is_linux_armhf(executable)

Note the 'hf' (hard float) in 'armhf' in the call to _is_linux_armhf().

This means that pip install prevents the installation of a manylinux armv7l tagged wheel on a soft-float distro, as shown below:

$ docker run -it --pull always --platform linux/arm/v5 -v "${PWD}:/dist" \
  --hostname bullseye-armv5 python:3.12.5-slim-bullseye bash

root@bullseye-armv5:/# uname -m
armv7l

root@bullseye-armv5:/# python -c 'import sysconfig; print(sysconfig.get_platform())'
linux-armv7l

# Below, the absence of 'hf' in 'gnueabi' indicates a soft-float distro/libc.
root@bullseye-armv5:/# ls -ld /lib/arm-linux-*
drwxr-xr-x 1 root root 4096 Sep  5 09:39 /lib/arm-linux-gnueabi

root@bullseye-armv5:/# pip install /dist/gpiod-2.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
ERROR: gpiod-2.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl is not a supported wheel on this platform.

Effectively, manylinux armv7l implies hard float. This means that there is no risk of a gpiod manylinux armv7l wheel compiled for hard float being selected on a soft float system.

A soft-float wheel cannot be used on a hard-float system
At first I thought that a gpiod soft-float wheel (whose binary ‘.so’ file did not make use of VFP instructions) might be a more “universal” wheel that could be used in both soft-float and hard-float systems. Unfortunately this is not the case, because:

  • Even after the auditwheel repair command, a gpiod wheel still has dependencies on fundamental shared libraries, namely libc.so.6 and libpthread.so.0. This is by design: The manylinux 2014 policy defines an exclusion list of system-provided shared libraries that the auditwheel repair command will not incorporate in the Python wheels.

  • An executable (including the .so file in a Python wheel) compiled for soft float will only work with shared libraries also compiled for soft float. I gather this is because the shared library ABI (Application Binary Interface) uses different function calling conventions: the “hard-float calling convention” makes use of VFP registers, while the “soft-float calling convention” uses the core CPU registers only.

Given that pip install treats manylinux armv7l wheels as hard float (as discussed above) and given that wheel file names cannot be tagged to distinguish between soft and hard float “variants”, it seems there is no use in trying to produce a manylinux armv7l soft float wheel.

The script provided in this PR, which uses the official Python image in Docker Hub for the linux/arm/v7 platform, builds “hard-float wheels” that make use of the VFP extension registers — even if the wheel does not actually make any floating point calculations. Indeed, running objdump on the ‘.so’ file of the gpiod armv7l wheel, I found a couple of VFP instructions: VLDR (extension register load) and VSTR (extension register store).

musllinux
Unlike the manylinux case, I have not come across pip install logic or guarantees that armv7l implies soft or hard float. This means that we could not rely on pip install preventing the selection of a hard-float wheel on a soft-float system, or vice-versa.

However, this may not be much of an issue in practice. Despite a fair amount of searching, I could not find a single Linux distro pre-built for musl libc and soft-float ARM v7 or below (v4, v5, v6, v7). I found a few musl-based Linux distros or distro “stages” pre-built for ARM v7 with hard float (VFP required), for example Alpine Linux and Gentoo’s Stage 3 Musl, but none for soft float. (The Alpine Linux Downloads page uses the term ‘armhf’ to refer to ARMv6 + VFP, and ‘armv7’ to refer to ARMv7 + VFP.)

It was a similar story for Linux distro images on Docker Hub. The official linux/arm/v6 and linux/arm/v7 Python images for Alpine Linux on Docker Hub (e.g. python:3.12-alpine3.20) were built for hard float (VFP required).

The fact that I did not find any musl-based, soft-float ARM v7-or-below pre-built Linux distro only potentially proves that my search was not thorough. (By the way, I also went through the musl-libc.org list of projects using musl.) But if indeed such pre-built distros are so hard to find, I am tempted to conclude that:

  • Any users of a musl-based, soft-float ARM v7 Linux distro will have built the distro themselves, for example using Yocto / OpenEmbedded or such.
  • ARMv7-A without VFP is rare, indeed as stated in ARM’s documentation: “VFP is an optional but rarely omitted extension to the instruction sets in the ARMv7-A architecture.” ARMv7-A is the architecture profile designed to run operating systems like Linux and Windows. The other two v7 architecture profiles are ARMv7-R for Real-Time operating systems and ARMv7-M for low-cost microcontrollers.
  • An ARMv7-A system without VFP, with a custom-built musl Linux distro, is more likely to be a resource-constrained embedded device that is unlikely to make use of CPython, in which case the gpiod Python wheels are not relevant.

These tentative points are also supported by the fact that most of the glibc-based, soft-float Linux distros that I came across targeted ARM v4 or v5 rather than v6 or v7.

Consequences of publishing armv7l hard-float wheels only
If we published musllinux and manylinux armv7l hard-float wheels and a user tried pip install gpiod on a soft-float armv7l system (hardware and/or distro), the result would be:

  • On a glibc-based soft-float Linux distro, pip install would ignore the published armv7l hard-float wheel, and locally build a soft-float wheel from source. This is what we want.

  • On a musl-based soft-float Linux distro, I could not test because I could not find any such distro. However, I believe pip install would install the published hard-float wheel. An error would then happen at the import gpiod line in Python source code, with a Python ImportError exception — because this is what I observed when I manually swapped soft/hard float gpiod .so files in the site-packages directory on a glibc-based distro. Hopefully the user would then raise an issue in this repo or through a mail list, in which case I would ask the user for details of their musl-based soft-float Linux distro, and whether they could share that distro with us. I might then be able to use that distro for further testing and a workaround. In the worst case, we could choose to unpublish the musllinux wheels from PyPi.org.

Conclusion
I am inclined to say that we could publish manylinux and musllinux armv7l wheels built for hard float only (VFP required). I know that such wheels are useful for the Raspberry Pi, as I have a use case myself (Docker containers running python:3.12-alpine3.20 and python:3.12-bookworm images).

@vfazio, @brgl, what do you think?

If we agree to proceed, I will then look at Bartosz’s earlier request of extending the existing script. I might propose having the existing script automatically call the new script but still having separate files, because the logic intersection is minimal and each script’s responsibilities distinct. The original script sits at a higher level of abstraction and does not deal with Docker directly; it orchestrates the call to cibuildwheel and the call to the new script. The new script, more similarly to cibuildwheel, creates and deletes Docker containers and images in order to build armv7l wheels.

@vfazio
Copy link
Contributor

vfazio commented Oct 7, 2024

Caveat lector: I'm not the maintainer of this repo so my thoughts/opinions/rationale should be discounted accordingly.

I think there are a number of things that warrant discussion here should we go this direction, but I think the first step is identifying the use cases to justify the work and continued maintenance.

Piwheels is purpose built for RPi wheels for those using RPi OS. It does have wheels for gpiod==2.2.1 https://www.piwheels.org/project/gpiod/

The reason Piwheels does not support newer CPython releases is because the Debian distro used by RPi OS only comes with a fixed version. For Bookworm based builds, it's 3.11. Debian Trixie is going to target 3.12 at which point I would expect 32bit wheels to be available there (assuming there is still a 32bit distro available).

For RPi, the use cases where piwheels aren't supported/guaranteed to work:

  • You're not using RPi OS
  • You're using a non-distribution Python on the OS

For a subset of RPis, migrating to a 64bit OS and using the AArch64 wheels should work. I and members of my team have tested these wheels on:

Testbed RPi 3B+ RPi CM3 NXP i.MX 8QuadMax
Debian w/ CPython 3.11 ✔️ ✔️ ✔️
Debian w/ CPython 3.12 ✔️ ✔️ ✔️
Alpine Linux ✔️ ✔️ not tested

The scenarios not covered by piwheels + AArch64 wheels are:

  • RPi on 32bit RPi OS with non-standard python deployment
  • RPi on 32bit non-RPi OS
  • other ARM core on 32bit OS

Note also from piwheels' own FAQ, emphasis mine:

I'm a package maintainer. Can I upload an Arm wheel to PyPI?

PyPI now allows uploading Armv6 and Armv7 wheels, but we recommend that you don't upload Arm wheels to PyPI for incompatibility reasons and other complications.

Wheels built on a Raspberry Pi 3, although tagged armv7l are not truly Armv7, and may be incompatible with other Arm platforms. If your package is made exclusively for the Raspberry Pi platform, there is no harm in uploading Arm wheels to PyPI, and this will speed up the installation for users who do not have pip configured to use piwheels. However, it is not possible to upload two identical wheel files with armv6l and armv7l platform tags, so the second one will be rejected by PyPI.

If you intend for your package to be installed by users in multiple Arm platforms, you may be best not to upload Arm wheels to PyPI and instead leave Raspberry Pi users to get a wheel from piwheels, and leave other Arm platform users to build from source.

I do not have hard numbers (I'm not sure how I'd even get them) but I do not envision the above edge cases account for a significant percentage of deployment scenarios... at least, IMO, not a significant enough percentage that requires this repo to shoulder the responsibility of building compliant wheels and demonstrating a cross-architecture support matrix for RPI and other ARM cores. Wheels can still be compiled on-the-fly via sdist. If the use case is significant enough, i imagine people deploying in these edge cases have other packages for which they may need wheels and may already have a private package index for this case.

I actively uses the 64bit wheels in a project and have a vested interest in seeing that those continue to work, however, I do not have the time to help test/debug/maintain the armv7l builds.

Given the blurb from the piwheels team and the testing/maintenance burden, I'm hesitant to pursue these without further evidence of need and someone dedicated to helping maintain them.

@brgl
Copy link
Owner

brgl commented Oct 7, 2024

@vfazio, @brgl, what do you think?

That this is for sure an awesome investigation but this is way tl;dr for this week that's super loaded for me. I already promised @vfazio to review his patches on linux-gpio and am not any closer to doing it.

Could I get a short summary of your results?

By the way, in some future issue / PR, we could investigate the possibility of gpiod wheels making use of Python’s Limited C API only, so that a single wheel tagged abi3 would work across all 3.x minor versions, avoiding the need for separate wheels tagged cp39, cp310, cp311... If this were the case, presumably PiWheels could publish the abi3 tagged wheel that would automatically be compatible with the latest 3.x Python releases — like perhaps the scyllaft wheels did it. Currently, however, the libgpiod Python binding C source code appears to make use of API items outside the Limited C API, for example PyObject_HEAD, PyUnicode_AsUTF8 and PyObject_CallFinalizerFromDealloc.

TBH I don't care about binary distributions enough to impose on myself strict limits on which C APIs to use. Comfort of coding will always take precedence over ABI compatibility issues for me.

@pdcastro
Copy link
Author

pdcastro commented Oct 9, 2024

The use case
I think the scenarios not covered by PiWheels + aarch64 are still significant.

My own current project environment is just one data point, but for what it is worth:

  • Hardware: Raspberry Pi 2 B with ARM Cortex-A7 processor (ARM v7 with VFP). The Pi 2 B is no longer a flagship device of course, however:

    • The Pi 2 B is still in production at least until Jan 2026 (obsolescence statement).
    • It can still be bought from distributors like Digikey — model SC1029 (Cortex-A7).
    • Millions of Pi 2 B units were sold according to some forum posts, and users like me are for sure still putting it to good use in various projects. 😀
  • OS: balenaOS: “Run Docker containers on embedded devices”. balenaOS has a read-only root filesystem and is architected around running user applications in Docker containers. (Btw, containers can access GPIO via the docker run --device /dev/gpiochip0 option.)

  • Python interpreter: Docker images such as python:3.12-alpine3.20 or python:3.12-slim-bookworm, i.e. Python 3.12. I will switch to Python 3.13 soon.

Generalizing the previous points:

  • In the Raspberry Pi ecosystem, the Pi 2 B remains a significant, widespread armv7l specimen — even if one of declining market share.

    • I am less familiar with armv7l hardware other than the Raspberry Pi, but I understand it is significant too.
    • An armv7l wheel is also needed on a 32-bit OS running on 64-bit hardware (because for example the libc shared libraries will be 32 bits).
  • Several Linux distros currently offer pre-built armv7l images for download, among them:
    Debian, Alpine Linux, Arch Linux, balenaOS, FreeBSD, Raspberry Pi OS, Gentoo Linux, Void Linux, Armbian, DietPi, Kali Linux, OpenWrt, etc.

    • If armv7l wasn’t still widespread, I’d expect to find fewer such pre-built Linux distros.
    • Docker containers are also a popular way of running Python apps, including apps that use GPIO, even on RPi OS.
  • Developers use Python interpreter versions other than 3.9 and 3.11, in which case PiWheels’ wheels cannot be used.

Let me emphasize the last point. PiWheels’ choice of producing wheels for a couple of Python minor versions only (3.9, 3.11) really diminishes its reliability, even within the Pi ecosystem. Python versions other than 3.9 or 3.11 were described as “non-standard Python deployments”, but this merely means Python versions other than the one that ships with a particular OS release. When a developer or team makes a choice of programming language version, like Python 3.10, in my experience it is rarely based on the version that comes with an OS release. Instead, the choice is usually based on compatibility with existing source code or libraries required by the project. In the absence of source code or library restrictions, the latest stable Python version would be the preferred version, in order to make the most of the programming language’s new features and improvements. There are popular tools like pyenv that are dedicated to making it easy to install multiple versions of Python, and then there are Docker containers too!

Re “wheels can still be compiled on-the-fly via sdist”, in a way it could be argued that gpiod wheels for armv7l are “more needed” than for aarch64 because armv7l devices are generally slower (e.g. Pi 2 vs. Pi 3/4/5), so the developer experience of building from source is poorer on armv7l.

Promoting libgpiod in the Pi world
Here's another use case example. I am working on a open-source side project that I will publish in the near future. It involves Python, MQTT, Home Assistant, Docker containers on balenaOS and the Raspberry Pi. For GPIO, I chose to use libgpiod’s Python binding. I want to make the dev experience simple and fast so that more developers are attracted to my project. During my own development with a Pi 2 B (armv7l), I was cross-building the gpiod wheel on my workstation and uploading it to the Pi, because this is faster and it allows my application Docker containers to be smaller, without build toolchains. However I did not want to document this wheel cross-build process as part of that project because it is relatively complex and becomes a distraction. Simpler options include:

  • Changing my app’s base image from python:3.12-slim-bookworm to python:3.12-bookworm, which increases the image size 8x from ~38MB to ~315MB, but includes all toolchain required to build gpiod wheels from source. But on my Pi 2 B, just docker pull of this image takes 11 minutes! 😱 (The bottleneck seems to be writing to the SD card, and/or Docker (BalenaEngine) not doing it efficiently.)
  • Having a multi-stage Dockerfile where an earlier stage builds the gpiod wheel, and a later stage reverts back to python:3.12-slim-bookworm. This is an improvement, but complexity creeps up again.
  • Having the libgpiod project publish pre-built armv7l wheels to PyPI.org, so that the end users of my project don’t need to build anything from source in the first place.

I thought that the last option would be the best, by far. After all, I had already written the script that builds the armv7l wheels, and I could save other people the trouble by contributing the work to the libgpiod repo. Then every user of the gpiod Python binding on armv7l would have a simpler developer experience and faster app deployment.

The Raspberry Pi is the poster child of GPIO programming with Python — with literally a bunch of GPIO pins sticking prominently out. It would not be too much of a stretch to say that the Pi’s mission is “to make GPIO accessible to all”! 😀 Meanwhile, the libgpiod project is a great GPIO programming solution and the official (Linux) one too. Yet when googling “raspberry pi gpio library” or “raspberry pi gpio programming”, few results even mention gpiod or libgpiod. There are historical reasons for this, but removing any remaining rough edges from the gpiod dev experience can only help widen its adoption.

PiWheels’ FAQ
My impression of the PiWheels FAQ, when it advises against uploading ARM wheels to PyPI, is that they mean ARM wheels built by PiWheels. As in: “don’t download a wheel from PiWheels, which may be tagged armv7l, and upload it to PyPI” because PiWheels sort of cheats on the tags. A wheel tagged armv7l by PiWheels is actually built with a 32-bit armv6l OS/libc on 64-bit “armv8l” hardware (Pi 3 and Pi 4):

“While the hardware in Raspberry Pi 1 and Zero is Armv6, Pi 2 is Armv7, Pi 3, 4, 400 and Zero 2 are Armv8, the (32-bit) operating system images provided on raspberrypi.com are made to support all three architectures (and are compatible with all Raspberry Pi products to date) by operating in Armv6 userland.”
“However, wheels built on a Raspberry Pi 2/3/4 running the 32-bit OS are tagged armv7l. Since wheels built on a Raspberry Pi 3/4 will work on a Pi 5, 4, 3, 2, 1, Zero or Zero 2, we simply provide Pi 3/4-built wheels renamed armv6l, with a few exceptions (some packages, like opencv and tensorflow, are built with optimisations available on Pi 3/4).”

Indeed, here’s the output of readelf -A for the gpiod armv7l wheel built by PiWheels:

$ docker run --rm -it --platform linux/arm/v7 -v "${PWD}:/dist" -w /dist python:3.13-bookworm bash
...
root@4ed72415f483:/dist/piwheels# readelf -A _ext.cpython-311-arm-linux-gnueabihf.so
Attribute Section: aeabi
File Attributes
  Tag_CPU_name: "6"
  Tag_CPU_arch: v6
  Tag_ARM_ISA_use: Yes
  Tag_THUMB_ISA_use: Thumb-1
  Tag_FP_arch: VFPv2
  Tag_ABI_PCS_wchar_t: 4
  Tag_ABI_FP_denormal: Needed
  Tag_ABI_FP_exceptions: Needed
  Tag_ABI_FP_number_model: IEEE 754
  Tag_ABI_align_needed: 8-byte
  Tag_ABI_align_preserved: 8-byte, except leaf SP
  Tag_ABI_enum_size: int
  Tag_ABI_VFP_args: VFP registers
  Tag_CPU_unaligned_access: v6

With this context, it’s understandable that PiWheels gets defensive.

However, wheels built with the script provided in this PR contain actual ARM v7 binaries built with armv7l OS/libc and Docker / qemu / binfmt_misc emulation:

root@4ed72415f483:/dist# readelf -A _ext.cpython-312-arm-linux-gnueabihf.so
Attribute Section: aeabi
File Attributes
  Tag_CPU_name: "7-A"
  Tag_CPU_arch: v7
  Tag_CPU_arch_profile: Application
  Tag_ARM_ISA_use: Yes
  Tag_THUMB_ISA_use: Thumb-2
  Tag_FP_arch: VFPv3-D16
  Tag_ABI_PCS_wchar_t: 4
  Tag_ABI_FP_denormal: Needed
  Tag_ABI_FP_exceptions: Needed
  Tag_ABI_FP_number_model: IEEE 754
  Tag_ABI_align_needed: 8-byte
  Tag_ABI_align_preserved: 8-byte, except leaf SP
  Tag_ABI_enum_size: int
  Tag_ABI_VFP_args: VFP registers
  Tag_CPU_unaligned_access: v6

The burden of testing and maintaining armv7l wheels
Regarding the concern of having “someone dedicated to helping maintain” armv7l wheels, for sure I would be happy to do it. “You become responsible, forever, for what you have tamed.” 😀 Also, I really doubt that the maintenance will be nearly as hard as trying to convince you to accept this contribution! 😅

@pdcastro
Copy link
Author

pdcastro commented Oct 9, 2024

Could I get a short summary of your results?

Sure, here it is:

  • The refined goal is to build and publish armv7l wheels only (a subset of 32-bit ARM).
    • The Raspberry Pi 2 B (Cortex-A7) is a prominent example of armv7l hardware and it is the system I am currently developing with. The Pi 2 B is still in production at least until Jan 2026 and can be bought from distributors like Digikey (model SC1029 - Cortex-A7). Millions of Pi 2 B boards are believed to have been sold.
    • The armv7l wheels are expected to be compatible with other armv7l hardware as well.
    • The armv7l wheels also apply in the case of a 32-bit OS running on 64-bit hardware.
  • The wheels will be tagged manylinux and musllinux, for compatibility with Linux distros based on glibc and musl libc respectively.
  • The wheels will be produced for selected CPython versions, including the latest stable version, now 3.13 (e.g. 3.10, 3.11, 3.12, 3.13).
    • This contrasts with gpiod wheels made by PiWheels, which are currently only available for versions 3.9 and 3.11.
  • The armv7l wheels will have an understood dependency on the VFP instruction set extension, a.k.a. “hard float”. This PR includes a draft note to be added to bindings/Python/README.md.
  • pip install prevents the installation of a wheel tagged armv7l on other 32-bit ARM systems like armv6l or earlier. (This is good.)
    • A 32-bit Linux distro running on 64-bit ARM hardware may report itself as armv8l (rather than aarch64). In this case, pip install will also select the armv7l wheel and it should work too.
  • pip install prevents the installation of manylinux armv7l wheels on “soft float” systems. On such systems, pip install builds the gpiod extension from source. (This is good.)
  • pip install does not offer a similar hard/soft float compatibility check for musllinux wheels. However, this may not be a problem in practice because I could not find a single example of a soft-float, musl-based, pre-built armv7l Linux distro, despite significant searching.
    • If it proves to be an issue, we can choose to unpublish the musllinux wheels.
  • If my contribution is accepted and armv7l wheels are published, I can confirm that I will help with testing, debugging and fixing issues related to those wheels.

No worries if you need some time to review this PR. Thanks again for your attention. 👍

@brgl
Copy link
Owner

brgl commented Oct 10, 2024

Could I get a short summary of your results?

Sure, here it is:

  • The refined goal is to build and publish armv7l wheels only (a subset of 32-bit ARM).

    • The Raspberry Pi 2 B (Cortex-A7) is a prominent example of armv7l hardware and it is the system I am currently developing with. The Pi 2 B is still in production at least until Jan 2026 and can be bought from distributors like Digikey (model SC1029 - Cortex-A7). Millions of Pi 2 B boards are believed to have been sold.

Ok.

  • The armv7l wheels are expected to be compatible with other armv7l hardware as well.

Well, I've seen my share of arm7 shenanigans to not take anyone's word for it. :)

  • The armv7l wheels also apply in the case of a 32-bit OS running on 64-bit hardware.

  • The wheels will be tagged manylinux and musllinux, for compatibility with Linux distros based on glibc and musl libc respectively.

  • The wheels will be produced for selected CPython versions, including the latest stable version, now 3.13 (e.g. 3.10, 3.11, 3.12, 3.13).

    • This contrasts with gpiod wheels made by PiWheels, which are currently only available for versions 3.9 and 3.11.

I see the script generating wheels for up to v3.12. @vfazio are you going to update it for v3.13 as well? Or is it system-dependent (my system has python v3.12.3)?

  • The armv7l wheels will have an understood dependency on the VFP instruction set extension, a.k.a. “hard float”. This PR includes a draft note to be added to bindings/Python/README.md.
  • pip install prevents the installation of a wheel tagged armv7l on other 32-bit ARM systems like armv6l or earlier. (This is good.)

Ok.

  • A 32-bit Linux distro running on 64-bit ARM hardware may report itself as armv8l (rather than aarch64). In this case, pip install will also select the armv7l wheel and it should work too.

Yeah but then we're running in compatibility mode.

  • pip install prevents the installation of manylinux armv7l wheels on “soft float” systems. On such systems, pip install builds the gpiod extension from source. (This is good.)

  • pip install does not offer a similar hard/soft float compatibility check for musllinux wheels. However, this may not be a problem in practice because I could not find a single example of a soft-float, musl-based, pre-built armv7l Linux distro, despite significant searching.

    • If it proves to be an issue, we can choose to unpublish the musllinux wheels.
  • If my contribution is accepted and armv7l wheels are published, I can confirm that I will help with testing, debugging and fixing issues related to those wheels.

I'm not saying no but I will defer to @vfazio who did all the work on publishing the wheels so far.

No worries if you need some time to review this PR. Thanks again for your attention. 👍

The patch should still go through the linux-gpio mailing list though.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it still a separate script? Can it not be integrated into the existing one for some reason?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s first make a decision on whether or not armv7l wheels will be published to PyPI.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not super straight forward unfortunately. cibuildwheel does have hooks for specifying the docker container used to generate a specific set of wheels, however, they didn't include armv7l until recently (and even then, only partially) so it's not as simple as specifying CIBW_{MANY,MUSL}LINUX_ARMV7L_IMAGE= and kicking off cibuildwheel.

pypa/cibuildwheel#2017

pypa/cibuildwheel#1421

I think they, too, are deferring to piwheels to provide those images for glibc environments because RPi OS is pretty much the defacto glibc based distro.

To add this support, a custom docker container needs to be generated with requisite dependencies, such as pip/build/etc and then invoked, preferably following the steps that cibuildwheel does it:

https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/linux.py#L161

So, stage one would generate standard wheels via cibuildwheel then a second stage would have to construct the container to generate armv7l wheels. I don't think there's any reason it couldn't be folded into the current script.

I'm not sure if we want to reinvent the wheel (pun partially intended). It looks like some people have build containers available: https://github.com/bjia56/armv7l-wheel-builder.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, so two weeks ago cibuildwheel added support for musllinux armv7l wheels, but not manylinux. I didn’t see that one coming! 🙂

Re https://github.com/bjia56/armv7l-wheel-builder, their Dockerfile is building binutils and glibc from source, and targeting armv6. It’s much simpler and faster to use the official Python base image on Docker Hub, as I’ve done in this PR.

If we are happy in principle to have armv7l wheels published to PyPI, I’d be happy to amend the existing script to use cibuildwheel for musllinux, and to add a few script functions to cover the manylinux case, along the lines of the work I have already done in this PR.

@vfazio
Copy link
Contributor

vfazio commented Oct 10, 2024

I see the script generating wheels for up to v3.12. @vfazio are you going to update it for v3.13 as well? Or is it system-dependent (my system has python v3.12.3)?

@brgl 3.13 was released just a few days ago. We can certainly add 3.13 wheels. I think we'd skip free-threaded wheels for now, however, as I'm not up-to-date on that feature and what changes may be required from the bindings to play nice in a GIL-less build.

Adding 3.13 wheels should be as simple as:

diff --git a/bindings/python/generate_pypi_artifacts.sh b/bindings/python/generate_pypi_artifacts.sh
index c2fb79f..6385a8a 100755
--- a/bindings/python/generate_pypi_artifacts.sh
+++ b/bindings/python/generate_pypi_artifacts.sh
@@ -108,8 +108,7 @@ python3 -m "${venv_module}" .venv
 venv_python="${temp_dir}/.venv/bin/python"
 
 # Install build dependencies
-# cibuildwheel 2.18.1 pins the toolchain containers to 2024-05-13-0983f6f
-${venv_python} -m pip install build==1.2.1 cibuildwheel==2.18.1
+${venv_python} -m pip install build==1.2.2 cibuildwheel==2.21.3
 
 LIBGPIOD_VERSION=${src_version} ${venv_python} -m build --sdist --outdir ./dist "${source_dir}"
 sdist=$(find ./dist -name '*.tar.gz')
  • If my contribution is accepted and armv7l wheels are published, I can confirm that I will help with testing, debugging and fixing issues related to those wheels.

I'm not saying no but I will defer to @vfazio who did all the work on publishing the wheels so far.

I don't necessarily want to be the decision maker here, I just want to make sure we're doing our due diligence and there's someone motivated and able to debug build/execution issues. As I mentioned before, I have a vested interest in the 64bit wheels as the company I work for is using these bindings to automate the testing of GPIO on boards we manufacture. Unfortunately, even things like #85, which I'm on the hook for, will be difficult for me to get around to due time and the foreign build environment (I haven't messed with Yocto in 5 years).

I would obviously strongly prefer that there be evidence that the armv7l wheels are tested on another ARM core variant besides RPi (mango Pi, NXP arm core, etc) and demonstrated to work, even if it's done via a 32bit container with /dev/gpio* mounted through.

Out of curiosity, i queried the platforms that are pulled gpiod 2.x in the past year. Note that this does not reflect scenarios where packages may be cached behind a pypi proxy like Sonatype Nexus:

version cpu num_downloads
2.2.1 x86_64 32802
2.2.1 aarch64 18972
2.2.1 5421
2.2.1 1022
2.2.1 armv7l 589
2.2.1 AMD64 496
2.2.1 arm64 92
2.2.1 riscv64 23
2.2.1 i686 10
2.2.1 armv6l 8
2.2.1 aarch64 2
2.2.1 armv8l 2
2.2.1 x86_64 1
2.2.1 armv5tel 1
2.2.1 mips 1
2.2.0 x86_64 11611
2.2.0 aarch64 10591
2.2.0 5262
2.2.0 AMD64 310
2.2.0 armv7l 263
2.2.0 228
2.2.0 arm64 73
2.2.0 riscv64 15
2.2.0 armv6l 10
2.2.0 i686 2
2.2.0 mips 1
2.1.3 x86_64 27407
2.1.3 aarch64 21793
2.1.3 13177
2.1.3 2407
2.1.3 armv7l 2169
2.1.3 AMD64 1104
2.1.3 arm64 140
2.1.3 armv6l 46
2.1.3 mips 31
2.1.3 riscv64 23
2.1.3 i686 13
2.1.3 armv8l 6
2.1.3 armv5tejl 5
2.1.3 ARM64 4
2.1.3 "iPhone8,1" 2
2.1.3 arm 2
2.1.3 aarch64 1
2.1.3 "iPhone14,5" 1
2.1.3 mips64 1
2.1.3 ppc 1
2.1.3 "iPad13,1" 1
2.1.2 455
2.1.2 aarch64 102
2.1.2 x86_64 23
2.1.2 AMD64 19
2.1.2 12
2.1.2 armv7l 9
2.1.2 arm64 9
2.1.1 x86_64 1071
2.1.1 518
2.1.1 aarch64 343
2.1.1 armv7l 77
2.1.1 AMD64 49
2.1.1 13
2.1.1 arm64 11
2.1.1 armv6l 9
2.1.1 i686 5
2.1.1 mips 4
2.1.0 428
2.1.0 aarch64 125
2.1.0 x86_64 63
2.1.0 AMD64 20
2.1.0 14
2.1.0 armv7l 13
2.1.0 arm64 8
2.1.0 i686 2
2.1.0 armv6l 2

@brgl
Copy link
Owner

brgl commented Oct 10, 2024

I see the script generating wheels for up to v3.12. @vfazio are you going to update it for v3.13 as well? Or is it system-dependent (my system has python v3.12.3)?

@brgl 3.13 was released just a few days ago. We can certainly add 3.13 wheels. I think we'd skip free-threaded wheels for now, however, as I'm not up-to-date on that feature and what changes may be required from the bindings to play nice in a GIL-less build.

Ha! Is there any writeup on this? I would definitely want to make sure the bindings to work fine without the GIL.

@vfazio
Copy link
Contributor

vfazio commented Oct 10, 2024

Ha! Is there any writeup on this? I would definitely want to make sure the bindings to work fine without the GIL.

https://peps.python.org/pep-0703/

https://docs.python.org/3/whatsnew/3.13.html#free-threaded-cpython

https://docs.python.org/3/c-api/module.html#c.Py_mod_gil

https://py-free-threading.github.io/porting/

@vfazio
Copy link
Contributor

vfazio commented Oct 10, 2024

The burden of testing and maintaining armv7l wheels
Regarding the concern of having “someone dedicated to helping maintain” armv7l wheels, for sure I would be happy to do it. “You become responsible, forever, for what you have tamed.” 😀 Also, I really doubt that the maintenance will be nearly as hard as trying to convince you to accept this contribution! 😅

@pdcastro Sorry if i'm coming off like an asshole, that's not my intent. I just want to make sure we're accounting for edge cases and not digging ourselves a maintenance nightmare.

@brgl
Copy link
Owner

brgl commented Oct 10, 2024

The burden of testing and maintaining armv7l wheels Regarding the concern of having “someone dedicated to helping maintain” armv7l wheels, for sure I would be happy to do it. “You become responsible, forever, for what you have tamed.” 😀 Also, I really doubt that the maintenance will be nearly as hard as trying to convince you to accept this contribution! 😅

I am willing to accept this but A 32-bit Linux distro running on 64-bit ARM hardware may report itself as armv8l (rather than aarch64). In this case, pip install will also select the armv7l wheel and it should work too. worries me and I'd like know when this can happen and if we can avoid running 32-bit binaries on 64- bit system without the user probably even realizing it.

@vfazio
Copy link
Contributor

vfazio commented Oct 10, 2024

The burden of testing and maintaining armv7l wheels Regarding the concern of having “someone dedicated to helping maintain” armv7l wheels, for sure I would be happy to do it. “You become responsible, forever, for what you have tamed.” 😀 Also, I really doubt that the maintenance will be nearly as hard as trying to convince you to accept this contribution! 😅

I am willing to accept this but A 32-bit Linux distro running on 64-bit ARM hardware may report itself as armv8l (rather than aarch64). In this case, pip install will also select the armv7l wheel and it should work too. worries me and I'd like know when this can happen and if we can avoid running 32-bit binaries on 64- bit system without the user probably even realizing it.

Isn't this expected? If you have a 64bit kernel, but a 32bit userspace, then we would expect pip to install 32bit wheels linked against 32bit libc? Or am i misunderstanding the situation?

@brgl
Copy link
Owner

brgl commented Oct 10, 2024

I am willing to accept this but A 32-bit Linux distro running on 64-bit ARM hardware may report itself as armv8l (rather than aarch64). In this case, pip install will also select the armv7l wheel and it should work too. worries me and I'd like know when this can happen and if we can avoid running 32-bit binaries on 64- bit system without the user probably even realizing it.

Is that when it happens? I'm not really sure what may report itself as armv8l means here? Who does the reporting? Where does this value come from? Can it only happen in the situation of a 64-bit kernel running 32-bit user-space?

@pdcastro
Copy link
Author

Out of curiosity, I queried the platforms that are pulled gpiod 2.x in the past year. [...]

Thanks for these numbers, @vfazio 👍 Wow, I expected that armv7l would be a minority but I am surprised at how small that minority is: For v2.2.1, armv7l is about 1% of total gpiod downloads, or 3% of ARM gpiod downloads. With this kind of number, you have convinced me that the benefits of publishing armv7l wheels do not outweigh the potential compatibility issues and ongoing maintenance effort. I am thus closing this PR.

Thank you both for the review.

@pdcastro pdcastro closed this Oct 10, 2024
@brgl
Copy link
Owner

brgl commented Oct 10, 2024

And I am honestly suprised by the tens of thousands of downloads in general!

@pdcastro
Copy link
Author

Is that when it happens? I'm not really sure what may report itself as armv8l means here? Who does the reporting? Where does this value come from? Can it only happen in the situation of a 64-bit kernel running 32-bit user-space?

I’ve already closed the PR as per previous comment but just for completeness: I meant a scenario where uname -m (and consequently Python’s sysconfig.get_platform()) reported armv8l instead of aarch64, indicating a 32-bit Linux distro (32-bit libc shared libraries) running on 64-bit hardware. I understand that in this case, the expected and desired behaviour is for pip install to select 32-bit wheels.

@vfazio
Copy link
Contributor

vfazio commented Oct 10, 2024

Not to necropost but i wanted to explain a bit. You can run your own queries off of

https://console.cloud.google.com/bigquery?p=bigquery-public-data&d=pypi&page=dataset&project=apt-rite-405917&ws=!1m4!1m3!3m2!1sbigquery-public-data!2spypi&inv=1&invt=Abearg

https://packaging.python.org/en/latest/guides/analyzing-pypi-package-downloads/#useful-queries

I think the reality is that for RPis, a lot of people use RPi.GPIO or pigpio which do not use the gpiochip character device and avoid the claim system altogether by simply memmapping the GPIO registers or link against c libs like libbcm2835 which essentially do the same.

This probably accounts for some of why 32bit arm downloads arent a high percentage of downloads.

It wasnt my intent to shut down this MR, I just wanted to provide some numbers. Maybe we can rerun the query in a few months to see if this has picked up.

I do think we could press on the cibuildwheel group about getting a stable manylinux image to build within now that there is one for musllinux.

@vfazio
Copy link
Contributor

vfazio commented Oct 11, 2024

Just so i don't have to remember how i wrote the queries:

SELECT details.cpu , COUNT(*) AS num_downloads FROM `bigquery-public-data.pypi.file_downloads` WHERE file.project = 'gpiod' AND 
  DATE(timestamp) BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 360 DAY) AND CURRENT_DATE() and
  file.version LIKE "2.%"
GROUP BY details.cpu
ORDER BY `num_downloads` DESC
SELECT file.version, details.cpu , COUNT(*) AS num_downloads FROM `bigquery-public-data.pypi.file_downloads` WHERE file.project = 'gpiod' AND 
  DATE(timestamp) BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 360 DAY) AND CURRENT_DATE() and
  file.version LIKE "2.%"
GROUP BY file.version, details.cpu
ORDER BY file.version DESC, `num_downloads` DESC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants