Skip to content

Commit 8af3bb1

Browse files
ktbarretthenryiii
andauthored
Add advanced example that installs a CMake package (#8)
* Add advanced example that installs a CMake package This advanced example, unlike the other examples, builds a separate shared library and installs a CMake package to use that shared library with the Python package. The example uses pybind11 to implement the C++-Python binding. The example has Python tests, but is missing C++ tests. * Change example to use packaged pybind11 cmakefiles * Apply suggestions from code review Co-authored-by: Henry Schreiner <[email protected]> * Run cmake-format * tests: run tests on CMake package * ci: drop Windows Co-authored-by: Henry Schreiner <[email protected]>
1 parent 9844fd9 commit 8af3bb1

File tree

14 files changed

+270
-2
lines changed

14 files changed

+270
-2
lines changed

noxfile.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import sys
2+
13
import nox
24

3-
hello_list = "hello-pure", "hello-cpp", "hello-pybind11", "hello-cython"
4-
long_hello_list = hello_list + ("pen2-cython",)
5+
6+
hello_list = ["hello-pure", "hello-cpp", "hello-pybind11", "hello-cython"]
7+
if not sys.platform.startswith("win"):
8+
hello_list.append("hello-cmake-package")
9+
long_hello_list = hello_list + ["pen2-cython"]
510

611

712
@nox.session
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
cmake_minimum_required(VERSION 3.14...3.19)
2+
3+
project(
4+
hello
5+
VERSION "0.1.0"
6+
LANGUAGES CXX)
7+
8+
include(GNUInstallDirs)
9+
10+
# define the C++ library "hello"
11+
add_library(hello SHARED "${PROJECT_SOURCE_DIR}/src/hello.cpp")
12+
13+
target_include_directories(
14+
hello PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
15+
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
16+
17+
install(DIRECTORY "${PROJECT_SOURCE_DIR}/include/"
18+
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
19+
20+
# Standard installation subdirs for the C++ library are used. The files will end
21+
# up in the specified subdirectories under the Python package root. For example,
22+
# "<python package prefix>/hello/lib/" if the destination is "lib/".
23+
#
24+
# Installing the objects in the package provides encapsulation and will become
25+
# important later for binary redistribution reasons.
26+
install(
27+
TARGETS hello
28+
EXPORT helloTargets
29+
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
30+
31+
# The CMake package config and target files are installed under the Python
32+
# package root. This is necessary to ensure that all the relative paths in the
33+
# helloTargets.cmake resolve correctly. It also provides encapsulation.
34+
#
35+
# The actual path used must be selected so that consuming projects can locate it
36+
# via `find_package`. To support finding CMake packages in the Python package
37+
# prefix, using `find_package`s default search path of
38+
# `<prefix>/<name>/share/<name>*/cmake/` is reasonable. Adding the Python
39+
# package installation prefix to CMAKE_PREFIX_PATH in combination with this path
40+
# will allow `find_package` to find this package and any other package installed
41+
# via a Python package if the CMake and Python packages are named the same.
42+
set(HELLO_CMAKE_PACKAGE_INSTALL_SUBDIR "share/hello/cmake")
43+
44+
install(
45+
EXPORT helloTargets
46+
NAMESPACE hello::
47+
DESTINATION ${HELLO_CMAKE_PACKAGE_INSTALL_SUBDIR})
48+
49+
include(CMakePackageConfigHelpers)
50+
51+
write_basic_package_version_file(
52+
helloConfigVersion.cmake
53+
VERSION ${PROJECT_VERSION}
54+
COMPATIBILITY SameMinorVersion)
55+
56+
configure_package_config_file(
57+
"${PROJECT_SOURCE_DIR}/cmake/helloConfig.cmake.in" helloConfig.cmake
58+
INSTALL_DESTINATION ${HELLO_CMAKE_PACKAGE_INSTALL_SUBDIR})
59+
60+
install(FILES "${PROJECT_BINARY_DIR}/helloConfig.cmake"
61+
"${PROJECT_BINARY_DIR}/helloConfigVersion.cmake"
62+
DESTINATION ${HELLO_CMAKE_PACKAGE_INSTALL_SUBDIR})
63+
64+
# We are using the SKBUILD variable, which is defined when scikit-build is
65+
# running the CMake build, to control building the Python wrapper. This allows
66+
# the C++ project to be installed, standalone, when using the standard CMake
67+
# build flow.
68+
if(DEFINED SKBUILD)
69+
70+
# prevent an unused variable warning
71+
set(ignoreMe "${SKBUILD}")
72+
73+
# call pybind11-config to obtain the root of the cmake package
74+
execute_process(COMMAND ${PYTHON_EXECUTABLE} -m pybind11 --cmakedir
75+
OUTPUT_VARIABLE pybind11_ROOT_RAW)
76+
string(STRIP ${pybind11_ROOT_RAW} pybind11_ROOT)
77+
find_package(pybind11)
78+
79+
pybind11_add_module(_hello MODULE
80+
"${PROJECT_SOURCE_DIR}/src/hello/hello_py.cpp")
81+
82+
target_link_libraries(_hello PRIVATE hello)
83+
84+
# Installing the extension module to the root of the package
85+
install(TARGETS _hello DESTINATION .)
86+
87+
configure_file("${PROJECT_SOURCE_DIR}/src/hello/__main__.py.in"
88+
"${PROJECT_BINARY_DIR}/src/hello/__main__.py")
89+
90+
install(FILES "${PROJECT_BINARY_DIR}/src/hello/__main__.py" DESTINATION .)
91+
92+
# The extension module must load the hello library as a dependency when the
93+
# extension module is loaded. The easiest way to locate the hello library is
94+
# via RPATH. Absolute RPATHs are possible, but they make the resulting
95+
# binaries not redistributable to other Python installations (conda is broke,
96+
# wheel reuse is broke, and more!).
97+
#
98+
# Placing the hello library in the package and using relative RPATHs that
99+
# doesn't point outside of the package means that the built package is
100+
# relocatable. This allows for safe binary redistribution.
101+
if(APPLE)
102+
set_target_properties(
103+
_hello PROPERTIES INSTALL_RPATH "@loader_path/${CMAKE_INSTALL_LIBDIR}")
104+
else()
105+
set_target_properties(_hello PROPERTIES INSTALL_RPATH
106+
"$ORIGIN/${CMAKE_INSTALL_LIBDIR}")
107+
endif()
108+
109+
endif()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Hello
2+
3+
This is an example project demonstrating the use of scikit-build for distributing a standalone C library, *hello*;
4+
a CMake package for that library; and a Python wrapper implemented in pybind11.
5+
6+
The example assume some familiarity with CMake and pybind11, only really going into detail on the scikit-build parts.
7+
pybind11 is used to implement the biding, but anything is possible: swig, C API library, etc.
8+
9+
To install the package run in the project directory
10+
11+
```bash
12+
pip install .
13+
```
14+
15+
To run the Python tests, first install the package then in the project directory run
16+
17+
```bash
18+
pytest
19+
```
20+
21+
To run the C++ test, first install the package, then configure and build the project
22+
23+
```bash
24+
cmake -S test/cpp -B build/ -Dhello_ROOT=$(python -m hello --cmakefiles)
25+
cmake --build build/
26+
```
27+
28+
Then run ctest in the build dir
29+
30+
```bash
31+
cd build/
32+
ctest
33+
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@PACKAGE_INIT@
2+
3+
include("${CMAKE_CURRENT_LIST_DIR}/helloTargets.cmake")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#ifndef HELLO_HPP
2+
#define HELLO_HPP
3+
4+
#include <iostream>
5+
6+
namespace hello {
7+
8+
void hello();
9+
10+
int return_two();
11+
12+
} // namespace hello
13+
14+
#endif
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[build-system]
2+
requires = [
3+
"setuptools>=42",
4+
"wheel",
5+
"scikit-build",
6+
"cmake>=3.14",
7+
"ninja",
8+
"pybind11>=2.6"
9+
]
10+
build-backend = "setuptools.build_meta"

projects/hello-cmake-package/setup.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from skbuild import setup
2+
3+
4+
setup(
5+
name="hello-cmake-package",
6+
version="0.1.0",
7+
packages=['hello'],
8+
package_dir={'': 'src'},
9+
cmake_install_dir='src/hello')
10+
11+
# When building extension modules `cmake_install_dir` should always be set to the
12+
# location of the package you are building extension modules for.
13+
# Specifying the installation directory in the CMakeLists subtley breaks the relative
14+
# paths in the helloTargets.cmake file to all of the library components.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#include "hello.hpp"
2+
3+
namespace hello {
4+
5+
void hello() { std::cout << "Hello, World!" << std::endl; }
6+
7+
int return_two() { return 2; }
8+
9+
} // namespace hello
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ._hello import hello, return_two
2+
3+
__all__ = ("hello", "return_two")
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import argparse
2+
import sys
3+
from typing import List, Any
4+
from pathlib import Path
5+
import hello
6+
7+
8+
def main(argv: List[Any]) -> int:
9+
10+
parser = argparse.ArgumentParser()
11+
parser.add_argument("--cmakefiles", action='store_true', help="Print hello project CMake module directory. Useful for setting hello_ROOT in CMake.")
12+
parser.add_argument("--prefix", action='store_true', help="Print hello package installation prefix. Useful for setting CMAKE_PREFIX_PATH in CMake.")
13+
14+
args = parser.parse_args(args=argv[1:])
15+
16+
if not argv[1:]:
17+
parser.print_help()
18+
return
19+
20+
prefix = Path(hello.__file__).parent
21+
22+
if args.cmakefiles:
23+
print(prefix / "@HELLO_CMAKE_PACKAGE_INSTALL_SUBDIR@")
24+
25+
if args.prefix:
26+
print(prefix)
27+
28+
29+
if __name__ == "__main__":
30+
sys.exit(main(sys.argv))

0 commit comments

Comments
 (0)