cmake-pip: pip packages built by CMake
cmake-pip lets pip build and distribute python packages whose extensions
are produced by a CMake project. You declare one thing in setup.py – an
ExtensionCMake pointing at a
CMakeLists.txt and describing it – and pip install / pip wheel does the rest:
configure, build, stage the declared artefacts into the package, and make the
result relocatable (rpaths, dependency collection, linux soname mangling).
It is designed to be non-invasive on the cmake side: an existing project
keeps building exactly as before, with plain cmake; the packaging is a
handful of declarations on top. Wheels, source distributions and
PEP 660 editable installs all work; dependencies can be provided by
conan, entirely driven by cmake-pip.
The shortest possible tour:
# setup.py -- everything else lives in pyproject.toml
from setuptools import setup
from cmake_pip.cmake_extension import ExtensionCMake
setup(ext_modules=[
ExtensionCMake("my_package.extension1",
"my_cmake_project/CMakeLists.txt",
cmake_install_component="python_module"),
])
# in my_cmake_project/CMakeLists.txt -- tag what belongs to the package
install(TARGETS extension1
DESTINATION my_package
COMPONENT python_module)
then pip install .. The worked examples – each one a
fixture of the test suite, built on every CI run – are the recommended
starting point.
Getting started
Guides
Reference
Getting started
Suppose our repository has the following layout:
.
├── setup.py
├── my_cmake_project
| ├── CMakeLists.txt
| └── source_code
| └── python_extension.cpp
└── python_src
├── __init__.py
└── my_pure_python.py
We want a package my_package containing the pure-python module
my_pure_python and one extension, extension1, built by the cmake
project under my_cmake_project/ (whose internal layout is entirely up to
it).
The ExtensionCMake declaration carries exactly three facts:
the extension being produced:
my_package.extension1;where its cmake project lives:
my_cmake_project/CMakeLists.txt;what that project contributes to the package, named through
cmake_install_component.
What ends up in the distribution has to be declared: the cmake project tags the
install rules of the artefacts belonging to the python package with a COMPONENT, and
the extension names that component. After building, cmake-pip stages exactly that
component into the distribution with cmake --install --component; the DESTINATION
of the install rules is relative to the root of the staged distribution, ie. it is the
python package directory:
# in my_cmake_project/CMakeLists.txt
install(TARGETS extension1
DESTINATION my_package
COMPONENT python_module)
Declaring a component is barely intrusive for the cmake project: every install() rule
already belongs to a component (Unspecified when none is given), a plain
cmake --install of the project still installs everything, and tagging many rules at
once can be done with a single set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME ...).
This gives, with the static metadata (name, version, packages, authors, …)
living in pyproject.toml as usual:
# setup.py -- the plain setuptools setup: cmake-pip installs its build
# commands through a setuptools entry point as soon as an ExtensionCMake is
# declared, no custom cmdclass needed
from setuptools import setup
from cmake_pip.cmake_extension import ExtensionCMake
# "python_module" is the COMPONENT declared on the install rules
ext1 = ExtensionCMake("my_package.extension1",
"my_cmake_project/CMakeLists.txt",
cmake_install_component="python_module")
setup(ext_modules=[ext1])
Remember to put cmake-pip in the [build-system] requires of
pyproject.toml – pip builds run isolated, the backend has to ask for its
dependencies. A complete, installable project of exactly this shape opens the
worked examples.
Declaring through the cmake-pip cmake module
Instead of writing the install rules by hand, the cmake project can declare its content with the helpers provided by cmake-pip:
find_package(cmakepip REQUIRED)
python_package_add_target(TARGET extension1
PYTHON_PACKAGE my_package
DESTINATION my_package)
python_package_add_target emits the component install rules (the component is derived
from PYTHON_PACKAGE: python_package_MY_PACKAGE) and records the declaration into a
small manifest written at the end of the configure phase –
cmake_pip_packages.txt in the build tree, plain text, one tab-separated record per
line. That manifest is the protocol between the cmake configuration and cmake-pip:
thanks to it, setup.py addresses the package simply by its PYTHON_PACKAGE name:
ext1 = ExtensionCMake('my_package.extension1',
'my_cmake_project/CMakeLists.txt',
cmake_install_component='my_package')
A name that does not appear in the manifest is taken verbatim, which is the plain
COMPONENT case of the previous section.
find_package(cmakepip) also declares an umbrella custom target,
cmake_pip_target, and every target declared through
python_package_add_target becomes a dependency of it. Passing that name as the
cmake_target of the extension (the constant
CMAKE_PIP_UMBRELLA_TARGET avoids retyping it)
restricts the build to exactly the declared extensions – the test executables and
benchmarks of the project, although part of all, are not compiled for the
wheel:
ext1 = ExtensionCMake('my_package.extension1',
'my_cmake_project/CMakeLists.txt',
cmake_install_component='my_package',
cmake_target=CMAKE_PIP_UMBRELLA_TARGET)
Anything else the package needs built – generated data files, plugins – is attached by the project itself, from any directory, with the regular command:
add_dependencies(cmake_pip_target my_generated_data)
Detecting the cmake-pip modules
cmake-pip ships cmake modules providing the python_package_* helpers.
When the ExtensionCMake declares a
cmake_install_component, their location is injected into the configuration
(cmakepip_DIR) and the regular find_package(cmakepip) resolves them.
A project that must also configure without cmake-pip (a plain C++ build of the same tree) can make the helpers optional by declaring no-op fallbacks:
find_package(cmakepip)
if(NOT cmakepip_FOUND)
message(STATUS "[CMAKE-PIP] not driven by cmake-pip, python packaging disabled")
function(python_package_add_target)
endfunction()
function(python_package_add_file)
endfunction()
function(python_package_create)
endfunction()
endif()
A project using these fallbacks must also guard its own attachments to the umbrella, which exists only when the module was found:
if(TARGET cmake_pip_target)
add_dependencies(cmake_pip_target my_generated_data)
endif()