How to force a platform wheel using build and pyproject.toml?

Question:

I am trying to force a Python3 non-universal wheel I’m building to be a platform wheel, despite not having any native build steps that happen during the distribution-packaging process.

The wheel will include an OS-specific shared library, but that library is built and copied into my package directory by a larger build system that my package knows nothing about. By the time my Python3 package is ready to be built into a wheel, my build system has already built the native shared library and copied it into the package directory.

This SO post details a solution that works for the now-deprecated setup.py approach, but I’m unsure how to accomplish the same result using the new and now-standard build / pyproject.toml system:

mypackage/
  mypackage.py  # Uses platform.system and importlib to load the local OS-specific library
  pyproject.toml
  mysharedlib.so  # Or .dylib on macOS, or .dll on Windows

Based on the host OS performing the build, I would like the resulting wheel to be manylinux, macos, or windows.

I build with python3 -m build --wheel, and that always emits mypackage-0.1-py3-none-any.whl.

What do I have to change to force the build to emit a platform wheel?

Asked By: Charles Nicholson

||

Answers:

OK, after some research and reading of code, I can present a bit of information and a few solutions that might meet other people’s needs, summarized here:

Firstly, pyproject.toml is not mutually exclusive from setup.py. setuptools will complain about deprecation if you create a distribution package via python3 setup.py ... and no pyproject.toml file is present.

However, setup.py is still around and available, but it’s a mistake to duplicate project configuration values (name, version, etc). So, put as much as your package will allow inside your pyproject.toml file, and use setup.py for things like overriding the Distribution class, or overriding the bdist_wheel module, etc.

As far as creating platform wheels, there are a few approaches that work, with pros and cons:

  1. Override the bdist_wheel command class in setup.py as described here and set self.root_is_pure to False in the finalize_options override. This forces the python tag (e.g. cp39) to be set, along with the platform tag.
  2. Override the Distribution class in setup.py as described here and override has_ext_modules() to simply return True. This also forces the python and platform tags to be set.
  3. Add an unused minimal extension module to your packaging definition, as described here and here. This lengthens the build process and adds a useless "dummy" shared library to be dragged along wherever your wheel goes.
  4. Add the argument -C=--build-option=--plat {your-platform-tag} to the build invocation (for my case that’s python -m build -w -n, for example). This leaves the Python tag untouched but you have to supply your own tag; there’s no way to say "use whatever the native platform is". You can discover the exact platform tag with the command wheel.bdist_wheel.get_platform(pathlib.Path('.')) after importing the pathlib and wheel.bdist_wheel packages, but that can be cumbersome because wheel isn’t a standard library package.
  5. Simply rename your wheel from mypkg-py3-none-any.whl to mypkg-py3-none-macosx_13_0_x86_64.whl– it appears that the platform tag is only encoded into the filename, and not any of the package metadata that’s generated during the distribution-package process.

In the end I chose option #4 because it required the least amount of work- no setup.py files need to be introduced solely to accomplish this, and the build logs make it clear that a platform wheel (not a pure wheel) is being created.

Answered By: Charles Nicholson