Index

Cross-compilation

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).

Table of contents
Terminology
Simple example
Caveats
Complete cross-compilation workflow
Set up the environment
Download a cross-compilation toolchain for your host system
Download Python for your host system
Inspect and customize the toolchain files and py-build-cmake configuration
Cross-compile the pybind11-project example package using py-build-cmake
Automated Bash scripts
A closer look at the CMake toolchain files
Cross-compilation of dependencies
Automatic cross-compilation
Windows
macOS
Linux

Terminology

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:

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:

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.3.3
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 https://github.com/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 https://github.com/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 https://github.com/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.3.3-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.3.3-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.3.3-cp37-cp37m-linux_armv6l.whl
├── pybind11_project-0.3.3-cp37-cp37m-manylinux_2_27_aarch64.whl
├── pybind11_project-0.3.3-cp37-cp37m-manylinux_2_27_armv7l.whl
├── pybind11_project-0.3.3-cp37-cp37m-manylinux_2_27_x86_64.whl
├── pybind11_project-0.3.3-cp38-cp38-linux_armv6l.whl
├── pybind11_project-0.3.3-cp38-cp38-manylinux_2_27_aarch64.whl
├── pybind11_project-0.3.3-cp38-cp38-manylinux_2_27_armv7l.whl
├── pybind11_project-0.3.3-cp38-cp38-manylinux_2_27_x86_64.whl
├── pybind11_project-0.3.3-cp39-cp39-linux_armv6l.whl
├── pybind11_project-0.3.3-cp39-cp39-manylinux_2_27_aarch64.whl
├── pybind11_project-0.3.3-cp39-cp39-manylinux_2_27_armv7l.whl
├── pybind11_project-0.3.3-cp39-cp39-manylinux_2_27_x86_64.whl
├── pybind11_project-0.3.3-cp310-cp310-linux_armv6l.whl
├── pybind11_project-0.3.3-cp310-cp310-manylinux_2_27_aarch64.whl
├── pybind11_project-0.3.3-cp310-cp310-manylinux_2_27_armv7l.whl
├── pybind11_project-0.3.3-cp310-cp310-manylinux_2_27_x86_64.whl
├── pybind11_project-0.3.3-cp311-cp311-linux_armv6l.whl
├── pybind11_project-0.3.3-cp311-cp311-manylinux_2_27_aarch64.whl
├── pybind11_project-0.3.3-cp311-cp311-manylinux_2_27_armv7l.whl
├── pybind11_project-0.3.3-cp311-cp311-manylinux_2_27_x86_64.whl
├── pybind11_project-0.3.3-cp312-cp312-linux_armv6l.whl
├── pybind11_project-0.3.3-cp312-cp312-manylinux_2_27_aarch64.whl
├── pybind11_project-0.3.3-cp312-cp312-manylinux_2_27_armv7l.whl
├── pybind11_project-0.3.3-cp312-cp312-manylinux_2_27_x86_64.whl
├── pybind11_project-0.3.3-cp313-cp313-linux_armv6l.whl
├── pybind11_project-0.3.3-cp313-cp313-manylinux_2_27_aarch64.whl
├── pybind11_project-0.3.3-cp313-cp313-manylinux_2_27_armv7l.whl
├── pybind11_project-0.3.3-cp313-cp313-manylinux_2_27_x86_64.whl
├── pybind11_project-0.3.3-pp37-pypy37_pp73-manylinux_2_27_aarch64.whl
├── pybind11_project-0.3.3-pp37-pypy37_pp73-manylinux_2_27_x86_64.whl
├── pybind11_project-0.3.3-pp38-pypy38_pp73-manylinux_2_27_aarch64.whl
├── pybind11_project-0.3.3-pp38-pypy38_pp73-manylinux_2_27_x86_64.whl
├── pybind11_project-0.3.3-pp39-pypy39_pp73-manylinux_2_27_aarch64.whl
├── pybind11_project-0.3.3-pp39-pypy39_pp73-manylinux_2_27_x86_64.whl
├── pybind11_project-0.3.3-pp310-pypy310_pp73-manylinux_2_27_aarch64.whl
└── pybind11_project-0.3.3-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:

  1. The configuration does not yet contain a [tool.py-build-cmake.cross] entry,
  2. and the DIST_EXTRA_CONFIG environment variable is set,
  3. and that variable points to a configuration file that specifies build_ext.plat_name,
  4. and that value differs from the current platform.

The supported values for build_ext.plat_name are:

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:

  1. The configuration does not yet contain a [tool.py-build-cmake.cross] entry,
  2. and the ARCHFLAGS environment variable is set,
  3. and its value contains one or more flags of the form -arch XXX, where XXX is either x86_64 or arm64,
  4. and the current platform's architecture is not included in this list.

The supported values for ARCHFLAGS are:

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.