Cross-compilation#
Cross-compilation is useful for building packages for platforms such as Raspberry Pi or Windows on ARM. You may also want to cross-compile your packages by default to ensure compatibility with a wide range of systems and to avoid accidental dependencies on libraries from your build environment.
When using py-build-cmake, cross-compiling Python extension modules is supported
out-of-the-box through CMake toolchain files.
When building packages using a tool like cibuildwheel
,
cross-compilation will also be enabled automatically whenever appropriate
(e.g. when building ARM64 packages on an Intel Mac).
Terminology#
Build system: the system used for building, on which py-build-cmake is invoked.
Host system: the system that the compiled binaries and Python modules will be used on.
Cross-build: building on the build system, for the host system.
Native build: building on build system, for the build system (this is the usual scenario).
In cross-compilation, the build and host systems are often different systems with different architectures.
Simple example#
If your CMake project supports cross-compilation, cross-compiling a Python
package can be achieved by simply adding a py-build-cmake.cross.toml
file
to the same directory as pyproject.toml
. This file should contain the
necessary information about the host system, such as the Python version,
implementation and ABI, the host operating system and architecture, and the
relative path of the CMake toolchain file to use.
For example:
implementation = 'cp' # CPython
version = '312' # 3.12
abi = 'cp312' # (default ABI for CPython 3.12)
arch = 'linux_aarch64'
toolchain_file = 'aarch64-linux-gnu.cmake'
library = "/path-to-sysroot/usr/lib/aarch64-linux-gnu/libpython3.12.so"
include_dir = "/path-to-sysroot/usr/include/python3.12"
This will generate a package for CPython 3.12 on Linux for 64-bit ARM, using the
compilers defined by the toolchain file aarch64-linux-gnu.cmake
(which you
should provide as well).
The format for the values in this file is the same as the format used for the
tags in wheel filenames, for example pkg-1.2.3-cp312-cp312-linux_aarch64.whl
.
For details about platform compatibility tags, see the PyPA specification:
https://packaging.python.org/en/latest/specifications/platform-compatibility-tags.
Caveats#
Note that py-build-cmake
does not check the Python version when
cross-compiling, so make sure that your CMakeLists.txt scripts find the correct
Python installation (one that matches the version, implementation, ABI,
operating system and architecture specified in the py-build-cmake.cross.toml
file), e.g. by setting the appropriate hints and artifacts variables:
https://cmake.org/cmake/help/latest/module/FindPython3.html#hints
https://cmake.org/cmake/help/latest/module/FindPython3.html#artifacts-specification
You can either specify these in your py-build-cmake.cross.toml
configuration
as shown above, or in your CMake toolchain file.
Complete cross-compilation workflow#
This section will guide you through the full process of cross-compiling your Python package.
You’ll need the following:
A cross-compilation toolchain that provides compilers for your host system (while running on your build system).
A (pre-compiled) Python installation for the host system.
A py-build-cmake configuration file and a CMake toolchain file with the appropriate settings for your use case.
Let’s go over these requirements step by step:
Set up the environment#
We’ll first clone py-build-cmake
and its example projects:
git clone https://github.com/tttapa/py-build-cmake --branch=0.4.2
cd py-build-cmake
Download a cross-compilation toolchain for your host system#
It is important that the toolchain has an older version of glibc and the linux headers, so that the resulting package is compatible with a wide range of Linux distributions. The toolchains in your system’s package manager are usually not compatible with older systems.
You can find ready-to-use toolchains with good compatibility at tttapa/toolchains.
# Create a directory to save the cross-compilation toolchains into
mkdir -p toolchains
# Download and extract the toolchain for AArch64 (~121 MiB)
url="https://github.com/tttapa/toolchains/releases/download/1.0.1"
wget "$url/x-tools-aarch64-rpi3-linux-gnu-gcc14.tar.xz" -O- | tar xJ -C toolchains
# Verify that the toolchain works
./toolchains/x-tools/aarch64-rpi3-linux-gnu/bin/aarch64-rpi3-linux-gnu-gcc --version
Download Python for your host system#
CMake needs to be able to locate the Python header files, and in some cases the Python shared library before you can build your package.
You can download these from tttapa/python-dev.
# The toolchain is read-only by default, make it writable to add Python to it
chmod u+w "toolchains/x-tools/aarch64-rpi3-linux-gnu"
# Download and extract the Python binaries for AArch64 (~124 MiB)
url="https://github.com/tttapa/python-dev/releases/download/0.0.7"
wget "$url/python-dev-aarch64-rpi3-linux-gnu.tar.xz" -O- | tar xJ -C toolchains
Inspect and customize the toolchain files and py-build-cmake configuration#
The Python installations from tttapa/python-dev already
include the necessary CMake toolchain files and py-build-cmake
configuration
files. Inspect them and customize to your specific setup if necessary.
(No changes necessary when just following this guide using the example projects).
# List the available CMake toolchain files.
ls toolchains/x-tools/*.cmake
# List the available py-build-cmake cross-compilation configuration files.
ls toolchains/x-tools/*.py-build-cmake.cross.toml
We’ll have a quick look at the toolchains/x-tools/aarch64-rpi3-linux-gnu.python3.11.py-build-cmake.cross.toml
as an example:
implementation = 'cp'
version = '311'
abi = 'cp311'
arch = 'manylinux_2_27_aarch64'
toolchain_file = 'aarch64-rpi3-linux-gnu.python.toolchain.cmake'
[cmake.options]
TOOLCHAIN_PYTHON_VERSION = '3.11'
The Python implementation, version, ABI and architecture were already discussed
in a previous section. The CMake toolchain file simply points to a file in the
same directory as the configuration file. We’ll have a look at what it does
shortly. Finally, the TOOLCHAIN_PYTHON_VERSION
variable tells the toolchain
file which version of Python to add to the CMake search paths.
Cross-compile the pybind11-project example package using py-build-cmake#
We now have our toolchain, the Python installation, and a configuration file
that selects the correct toolchain and Python version, so we are ready to
cross-compile our first package. We’ll use the pybind11-project
example that’s
included with py-build-cmake
.
# Install PyPA build as a build front-end
python3 -m pip install -U build
# Cross-compile the example package using our cross-compilation configuration
python3 -m build -w examples/pybind11-project \
-C cross="$PWD/toolchains/x-tools/aarch64-rpi3-linux-gnu.python3.11.py-build-cmake.cross.toml"
If everything worked as expected, you should see output similar to the following.
-- The C compiler identification is GNU 14.2.0
-- The CXX compiler identification is GNU 14.2.0
-- Check for working C compiler: py-build-cmake/toolchains/x-tools/aarch64-rpi3-linux-gnu/bin/aarch64-rpi3-linux-gnu-gcc - skipped
[...]
-- Found Python3: py-build-cmake/toolchains/x-tools/aarch64-rpi3-linux-gnu/python3.11/usr/local/include/python3.11 (found version "3.11.10") found components: Development.Module
-- Detecting pybind11 CMake location
-- pybind11 CMake location: /tmp/build-env-xxxxx/lib/python3.9/site-packages/pybind11/share/cmake/pybind11
-- Performing Test HAS_FLTO
-- Performing Test HAS_FLTO - Success
-- Found pybind11: /tmp/build-env-xxxxx/lib/python3.9/site-packages/pybind11/include (found version "2.13.6")
-- Configuring done (1.4s)
-- Generating done (0.0s)
-- Build files have been written to: py-build-cmake/examples/pybind11-project/.py-build-cmake_cache/cp311-cp311-manylinux_2_27_aarch64
[ 50%] Building CXX object CMakeFiles/_add_module.dir/src/add_module.cpp.o
[100%] Linking CXX shared module _add_module.cpython-311-aarch64-linux-gnu.so
[100%] Built target _add_module
-- Installing: /tmp/xxxxx/staging/pybind11_project/_add_module.cpython-311-aarch64-linux-gnu.so
[...]
Successfully built pybind11_project-0.4.2-cp311-cp311-manylinux_2_27_aarch64.whl
You can see that CMake is using the cross-compiler we downloaded, and that it
managed to locate the version of Python we requested (CPython 3.11 for AArch64).
It is important to verify the module extension suffix
(.cpython-311-aarch64-linux-gnu.so
in this case) and the Wheel tags
(cp311-cp311-manylinux_2_27_aarch64
).
You can now copy the Wheel package in examples/pybind11-project/dist/pybind11_project-0.4.2-cp311-cp311-manylinux_2_27_aarch64.whl
to e.g. a Raspberry Pi and install it using pip install
.
Automated Bash scripts#
The included scripts/download-cross-toolchains-linux.sh
script downloads the
toolchains and Python installations for common architectures and current
versions of Python. You can then run the examples/pybind11-project/cross-compile-linux.sh
and examples/nanobind-project/cross-compile-linux.sh
scripts to cross-compile
these example packages for this wide range of configurations.
./scripts/download-cross-toolchains-linux.sh
./examples/pybind11-project/cross-compile-linux.sh
./examples/nanobind-project/cross-compile-linux.sh
You can find the resulting Wheel packages in the
examples/pybind11-project/dist
directory:
examples/pybind11-project/dist
├── pybind11_project-0.4.2-cp37-cp37m-linux_armv6l.whl
├── pybind11_project-0.4.2-cp37-cp37m-manylinux_2_27_aarch64.whl
├── pybind11_project-0.4.2-cp37-cp37m-manylinux_2_27_armv7l.whl
├── pybind11_project-0.4.2-cp37-cp37m-manylinux_2_27_x86_64.whl
├── pybind11_project-0.4.2-cp38-cp38-linux_armv6l.whl
├── pybind11_project-0.4.2-cp38-cp38-manylinux_2_27_aarch64.whl
├── pybind11_project-0.4.2-cp38-cp38-manylinux_2_27_armv7l.whl
├── pybind11_project-0.4.2-cp38-cp38-manylinux_2_27_x86_64.whl
├── pybind11_project-0.4.2-cp39-cp39-linux_armv6l.whl
├── pybind11_project-0.4.2-cp39-cp39-manylinux_2_27_aarch64.whl
├── pybind11_project-0.4.2-cp39-cp39-manylinux_2_27_armv7l.whl
├── pybind11_project-0.4.2-cp39-cp39-manylinux_2_27_x86_64.whl
├── pybind11_project-0.4.2-cp310-cp310-linux_armv6l.whl
├── pybind11_project-0.4.2-cp310-cp310-manylinux_2_27_aarch64.whl
├── pybind11_project-0.4.2-cp310-cp310-manylinux_2_27_armv7l.whl
├── pybind11_project-0.4.2-cp310-cp310-manylinux_2_27_x86_64.whl
├── pybind11_project-0.4.2-cp311-cp311-linux_armv6l.whl
├── pybind11_project-0.4.2-cp311-cp311-manylinux_2_27_aarch64.whl
├── pybind11_project-0.4.2-cp311-cp311-manylinux_2_27_armv7l.whl
├── pybind11_project-0.4.2-cp311-cp311-manylinux_2_27_x86_64.whl
├── pybind11_project-0.4.2-cp312-cp312-linux_armv6l.whl
├── pybind11_project-0.4.2-cp312-cp312-manylinux_2_27_aarch64.whl
├── pybind11_project-0.4.2-cp312-cp312-manylinux_2_27_armv7l.whl
├── pybind11_project-0.4.2-cp312-cp312-manylinux_2_27_x86_64.whl
├── pybind11_project-0.4.2-cp313-cp313-linux_armv6l.whl
├── pybind11_project-0.4.2-cp313-cp313-manylinux_2_27_aarch64.whl
├── pybind11_project-0.4.2-cp313-cp313-manylinux_2_27_armv7l.whl
├── pybind11_project-0.4.2-cp313-cp313-manylinux_2_27_x86_64.whl
├── pybind11_project-0.4.2-pp37-pypy37_pp73-manylinux_2_27_aarch64.whl
├── pybind11_project-0.4.2-pp37-pypy37_pp73-manylinux_2_27_x86_64.whl
├── pybind11_project-0.4.2-pp38-pypy38_pp73-manylinux_2_27_aarch64.whl
├── pybind11_project-0.4.2-pp38-pypy38_pp73-manylinux_2_27_x86_64.whl
├── pybind11_project-0.4.2-pp39-pypy39_pp73-manylinux_2_27_aarch64.whl
├── pybind11_project-0.4.2-pp39-pypy39_pp73-manylinux_2_27_x86_64.whl
├── pybind11_project-0.4.2-pp310-pypy310_pp73-manylinux_2_27_aarch64.whl
└── pybind11_project-0.4.2-pp310-pypy310_pp73-manylinux_2_27_x86_64.whl
A closer look at the CMake toolchain files#
Let us now look at toolchains/x-tools/aarch64-rpi3-linux-gnu.python.toolchain.cmake
more closely:
include("${CMAKE_CURRENT_LIST_DIR}/aarch64-rpi3-linux-gnu.toolchain.cmake")
# [...]
# User options
set(TOOLCHAIN_PYTHON_VERSION "3" CACHE STRING "Python version to locate")
option(TOOLCHAIN_NO_FIND_PYTHON "Do not set the FindPython hints" Off)
option(TOOLCHAIN_NO_FIND_PYTHON3 "Do not set the FindPython3 hints" Off)
# [...]
# Internal variables
set(TOOLCHAIN_PYTHON_ROOT "${CMAKE_CURRENT_LIST_DIR}/${CROSS_GNU_TRIPLE}/python${TOOLCHAIN_PYTHON_VERSION}")
list(APPEND CMAKE_FIND_ROOT_PATH "${TOOLCHAIN_PYTHON_ROOT}")
# Determine the paths and other properties of the Python installation
function(toolchain_locate_python)
# [...]
endfunction()
if (NOT TOOLCHAIN_NO_FIND_PYTHON OR NOT TOOLCHAIN_NO_FIND_PYTHON3)
# [...]
# Set FindPython hints and artifacts
if (NOT TOOLCHAIN_NO_FIND_PYTHON)
set(Python_ROOT_DIR ${TOOLCHAIN_PYTHON_ROOT_DIR} CACHE PATH "" FORCE)
set(Python_LIBRARY ${TOOLCHAIN_PYTHON_LIBRARY} CACHE FILEPATH "" FORCE)
set(Python_INCLUDE_DIR ${TOOLCHAIN_PYTHON_INCLUDE_DIR} CACHE PATH "" FORCE)
set(Python_INTERPRETER_ID "Python" CACHE STRING "" FORCE)
endif()
# Set FindPytho3 hints and artifacts
if (NOT TOOLCHAIN_NO_FIND_PYTHON3)
set(Python3_ROOT_DIR ${TOOLCHAIN_PYTHON_ROOT_DIR} CACHE PATH "" FORCE)
set(Python3_LIBRARY ${TOOLCHAIN_PYTHON_LIBRARY} CACHE FILEPATH "" FORCE)
set(Python3_INCLUDE_DIR ${TOOLCHAIN_PYTHON_INCLUDE_DIR} CACHE PATH "" FORCE)
set(Python3_INTERPRETER_ID "Python" CACHE STRING "" FORCE)
endif()
# Set pybind11 hints
# [...]
set(PYTHON_MODULE_DEBUG_POSTFIX ${PYTHON_MODULE_DEBUG_POSTFIX} CACHE INTERNAL "")
set(PYTHON_MODULE_EXTENSION ${PYTHON_MODULE_EXTENSION} CACHE INTERNAL "")
set(PYTHON_IS_DEBUG ${PYTHON_IS_DEBUG} CACHE INTERNAL "")
# Set nanobind hints
# [...]
set(NB_SUFFIX ${NB_SUFFIX} CACHE INTERNAL "")
set(NB_SUFFIX_S ${NB_SUFFIX_S} CACHE INTERNAL "")
endif()
First, it includes the standard toolchain file for the platform (the one that
sets the platform properties and compiler paths, see below).
Next, it declares the options that you might want to configure as a user, such
as the version of Python to make available, and whether to set CMake’s
FindPython and
FindPython3 hints.
Then it adds the selected Python installation to CMake’s search path, it
locates the Python library and include paths, its properties such as the
ABI and the extension suffix, and sets the FindPython hints.
Finally, it sets some specific cache variables that are needed by the
pybind11 and nanobind
frameworks.
The file toolchains/x-tools/aarch64-rpi3-linux-gnu.toolchain.cmake
contains:
# System information
set(CMAKE_SYSTEM_NAME "Linux")
set(CMAKE_SYSTEM_PROCESSOR "aarch64")
set(CROSS_GNU_TRIPLE "aarch64-rpi3-linux-gnu"
CACHE STRING "The GNU triple of the toolchain to use")
set(CMAKE_LIBRARY_ARCHITECTURE "aarch64-linux-gnu")
# Compiler flags
set(CMAKE_C_FLAGS_INIT "-mcpu=cortex-a53+crc+simd")
set(CMAKE_CXX_FLAGS_INIT "-mcpu=cortex-a53+crc+simd")
set(CMAKE_Fortran_FLAGS_INIT "-mcpu=cortex-a53+crc+simd")
# Search path configuration
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
# Packaging
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "arm64")
# Compiler binaries
set(TOOLCHAIN_DIR "${CMAKE_CURRENT_LIST_DIR}/${CROSS_GNU_TRIPLE}")
set(CMAKE_C_COMPILER "${TOOLCHAIN_DIR}/bin/${CROSS_GNU_TRIPLE}-gcc"
CACHE FILEPATH "C compiler")
set(CMAKE_CXX_COMPILER "${TOOLCHAIN_DIR}/bin/${CROSS_GNU_TRIPLE}-g++"
CACHE FILEPATH "C++ compiler")
set(CMAKE_Fortran_COMPILER "${TOOLCHAIN_DIR}/bin/${CROSS_GNU_TRIPLE}-gfortran"
CACHE FILEPATH "Fortran compiler")
Cross-compilation of dependencies#
If your Python package depends on native libraries, you’ll have to cross-compile
these dependencies as well. You can either compile them from source yourself,
using the CMake toolchain files included with the toolchain, or you can use
the Conan package manager to build these dependencies for you. A Conan profile
is included as well, see e.g. toolchains/x-tools/aarch64-rpi3-linux-gnu.profile.conan
.
Automatic cross-compilation#
In order to ensure compatibility with cibuildwheel
,
py-build-cmake
automatically enables cross-compilation when certain
environment variables are set.
Windows#
Cross-compilation on Windows is enabled if the following conditions are met:
The configuration does not yet contain a
[tool.py-build-cmake.cross]
entry,and the
DIST_EXTRA_CONFIG
environment variable is set,and that variable points to a configuration file that specifies
build_ext.plat_name
,and that value differs from the current platform.
The supported values for build_ext.plat_name
are:
win32
win-amd64
win-arm32
win-arm64
As a result, py-build-cmake
sets the CMAKE_SYSTEM_NAME
,
CMAKE_SYSTEM_PROCESSOR
and CMAKE_GENERATOR_PLATFORM
CMake options to the
appropriate values for the given plat_name
.
If build_ext.library_dirs
is set in the configuration file as well, those
directories are searched for the Python library, and if found, the
CMake {Python,Python3}_LIBRARY
hints are specified. If the Python library is
part of a Python installation hierarchy that also contains an include
directory, this is specified using the {Python,Python3}_INCLUDE_DIR
hints.
Other CMake options are inherited from the [tool.py-build-cmake.windows.cmake]
configuration in pyproject.toml
.
macOS#
Cross-compilation on macOS is enabled if the following conditions are met:
The configuration does not yet contain a
[tool.py-build-cmake.cross]
entry,and the
ARCHFLAGS
environment variable is set,and its value contains one or more flags of the form
-arch XXX
, whereXXX
is eitherx86_64
orarm64
,and the current platform’s architecture is not included in this list.
The supported values for ARCHFLAGS
are:
-arch x86_64
(Intel)-arch arm64
(Apple silicon)-arch x86_64 -arch arm64
(Universal 2 fat binaries, both Intel and Apple silicon)
As a result, py-build-cmake
sets the CMAKE_SYSTEM_NAME
and
CMAKE_OSX_ARCHITECTURES
CMake options to the appropriate values.
If the current interpreter is CPython, the SETUPTOOLS_EXT_SUFFIX
environment
variable is set as well.
Other CMake options are inherited from the [tool.py-build-cmake.mac.cmake]
configuration in pyproject.toml
.
If the current platform’s architecture is included in the ARCHFLAGS
(violating condition 3), cross-compilation will not be enabled, but the
CMAKE_OSX_ARCHITECTURES
CMake option will still be set. (This is the case for
universal wheels.)
Linux#
Since cibuildwheel
does not support cross-compilation on Linux,
py-build-cmake
does not enable automatic cross-compilation for this platform.
By default, cibuildwheel
will try to build your package in an emulated ARM64
container. This can be very slow, so it is recommended to use explicit
cross-compilation as described above.