A utility for building wheels in reproducible environments

Created On:

PEP 517 and PEP 518 improved Python packaging by allowing projects to specify which dependencies are needed to build a wheel. They specify a build-system.requires section in a pyproject.toml file which acts as a requirements.txt for build time dependencies. The benefit of specifying build time dependencies is projects other than setuptools can be used to build wheels.

Unfortunately there is no lock file equivalent of the dependencies in pyproject.toml meaning tools like pip need to re-resolve the dependencies every time a wheel needs to be built. As I have written before not only does this slow building the wheel, it also introduces non determinism. For example the following pyproject.toml section results in non determinism.

[build-system]
requires = ["setuptools>48", "wheel"]

Since these requirements don’t specify an exact version nor do they pin transitive dependencies, the versions used in the build will be the latest versions published on PyPI. It’s even possible to fail to build entirely because of backwards incompatible changes.

Introducing reproducible-wheel-builder

To fix the above issue I wrote reproducible-wheel-builder1 a utility which combines pex and build to create reproducible environments for building wheels. Instead of relying on pip to re-resolve the build-system.requires for every build, pex and a Pex lockfile are used to create a reproducible virtual environment for the build. Then build uses that virtual environment to build the wheel. Since a lockfile is used, the build environment is reproducible.

It is distributed as a pex and can be downloaded from GitHub. Running it is as simple as executing main.pex.

$ ./main.pex --help
usage: __pex_executable__.py [-h] --lock LOCK --src SRC [--dist {wheel,sdist,editable}] --out OUT [--quiet] [--requires_config REQUIRES_CONFIG] [--build_config BUILD_CONFIG]

options:
  -h, --help            show this help message and exit
  --lock LOCK           Path to the pex lock file
  --src SRC             Path to the source directory to build
  --dist {wheel,sdist,editable}
                        Type of distribution to build
  --out OUT             Path to the output directory
  --quiet               Supress the output of the build backend
  --requires_config REQUIRES_CONFIG
                        JSON file of config to pass to build backend when getting requires
  --build_config BUILD_CONFIG
                        JSON file of config to pass to build backend when building

This can be used instead of pip wheel to build a project.

Example

gevent 21.1.2 does not have wheels for Python 3.10. Using pip wheel on the sdist results in the following error.

$ pip --verbose wheel ./gevent-21.1.2
Processing ./gevent-21.1.2
  ...

  Error compiling Cython file:
  -----------------------------------------------------------
  ...
  cdef load_traceback
  cdef Waiter
  cdef wait
  cdef iwait
  cdef reraise
  cpdef GEVENT_CONFIG
        ^
  ------------------------------------------------------------

  src/gevent/_gevent_cgreenlet.pxd:182:6: Variables cannot be declared with 'cpdef'. Use 'cdef' instead.
  Compiling src/gevent/greenlet.py because it changed.

  ...

  × Getting requirements to build wheel did not run successfully.
  │ exit code: 1
  ╰─> See above for output.

Build failure output when trying to build a wheel for gevent 21.1.2

This is because the pyproject.toml has the following build-system.requires section.

requires = [
     "setuptools >= 40.8.0",
     "wheel",
     "Cython >= 3.0a6",
     "cffi >= 1.12.3 ; platform_python_implementation == 'CPython'",
     "greenlet >= 0.4.17, < 2.0 ; platform_python_implementation == 'CPython'",
]

The Cython >= 3.0a6 requirement is problematic because Cython 3.0a8 has the following change:

Variables can no longer be declared with cpdef.

This can be overcome by using reproducible-wheel-builder with a pex lockfile that pins Cython to 3.0a7. Generating a pex lockfile for the dependencies can be done with the pex3 command.

$ pex3 lock create --pip-version 22.3 --resolver-version pip-2020-resolver \
  --no-build --indent 2 -o pex.lock \
  'setuptools >= 40.8.0' 'wheel' 'Cython == 3.0a7' 'cffi >= 1.12.3' 'greenlet >= 0.4.17, < 2.0'

With the pex.lock file and the main.pex of reproducible-wheel-builder a wheel can be built now.

$ ./main.pex --lock pex.lock --src ./gevent-21.1.2 --out ./out --quiet

The above command successfully runs and outputs a wheel under ./out.

Conclusion

PEP 517 and PEP 518 don’t specify a lockfile mechanism for specifying the build environment. This leads to non determinism and possibly failed builds when using pip wheel. This can be overcome with a pex lockfile of the build environment and passing it to reproducible-wheel-builder to do the build.


  1. I am not good at naming things↩︎