diff --git a/noxfile.py b/noxfile.py index 56abafc..a009b9b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,7 +1,12 @@ +import sys + import nox -hello_list = "hello-pure", "hello-cpp", "hello-pybind11", "hello-cython" -long_hello_list = hello_list + ("pen2-cython",) + +hello_list = ["hello-pure", "hello-cpp", "hello-pybind11", "hello-cython"] +if not sys.platform.startswith("win"): + hello_list.append("hello-cmake-package") +long_hello_list = hello_list + ["pen2-cython"] @nox.session diff --git a/projects/hello-cmake-package/CMakeLists.txt b/projects/hello-cmake-package/CMakeLists.txt new file mode 100644 index 0000000..76673d9 --- /dev/null +++ b/projects/hello-cmake-package/CMakeLists.txt @@ -0,0 +1,109 @@ +cmake_minimum_required(VERSION 3.14...3.19) + +project( + hello + VERSION "0.1.0" + LANGUAGES CXX) + +include(GNUInstallDirs) + +# define the C++ library "hello" +add_library(hello SHARED "${PROJECT_SOURCE_DIR}/src/hello.cpp") + +target_include_directories( + hello PUBLIC $ + $) + +install(DIRECTORY "${PROJECT_SOURCE_DIR}/include/" + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + +# Standard installation subdirs for the C++ library are used. The files will end +# up in the specified subdirectories under the Python package root. For example, +# "/hello/lib/" if the destination is "lib/". +# +# Installing the objects in the package provides encapsulation and will become +# important later for binary redistribution reasons. +install( + TARGETS hello + EXPORT helloTargets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +# The CMake package config and target files are installed under the Python +# package root. This is necessary to ensure that all the relative paths in the +# helloTargets.cmake resolve correctly. It also provides encapsulation. +# +# The actual path used must be selected so that consuming projects can locate it +# via `find_package`. To support finding CMake packages in the Python package +# prefix, using `find_package`s default search path of +# `//share/*/cmake/` is reasonable. Adding the Python +# package installation prefix to CMAKE_PREFIX_PATH in combination with this path +# will allow `find_package` to find this package and any other package installed +# via a Python package if the CMake and Python packages are named the same. +set(HELLO_CMAKE_PACKAGE_INSTALL_SUBDIR "share/hello/cmake") + +install( + EXPORT helloTargets + NAMESPACE hello:: + DESTINATION ${HELLO_CMAKE_PACKAGE_INSTALL_SUBDIR}) + +include(CMakePackageConfigHelpers) + +write_basic_package_version_file( + helloConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMinorVersion) + +configure_package_config_file( + "${PROJECT_SOURCE_DIR}/cmake/helloConfig.cmake.in" helloConfig.cmake + INSTALL_DESTINATION ${HELLO_CMAKE_PACKAGE_INSTALL_SUBDIR}) + +install(FILES "${PROJECT_BINARY_DIR}/helloConfig.cmake" + "${PROJECT_BINARY_DIR}/helloConfigVersion.cmake" + DESTINATION ${HELLO_CMAKE_PACKAGE_INSTALL_SUBDIR}) + +# We are using the SKBUILD variable, which is defined when scikit-build is +# running the CMake build, to control building the Python wrapper. This allows +# the C++ project to be installed, standalone, when using the standard CMake +# build flow. +if(DEFINED SKBUILD) + + # prevent an unused variable warning + set(ignoreMe "${SKBUILD}") + + # call pybind11-config to obtain the root of the cmake package + execute_process(COMMAND ${PYTHON_EXECUTABLE} -m pybind11 --cmakedir + OUTPUT_VARIABLE pybind11_ROOT_RAW) + string(STRIP ${pybind11_ROOT_RAW} pybind11_ROOT) + find_package(pybind11) + + pybind11_add_module(_hello MODULE + "${PROJECT_SOURCE_DIR}/src/hello/hello_py.cpp") + + target_link_libraries(_hello PRIVATE hello) + + # Installing the extension module to the root of the package + install(TARGETS _hello DESTINATION .) + + configure_file("${PROJECT_SOURCE_DIR}/src/hello/__main__.py.in" + "${PROJECT_BINARY_DIR}/src/hello/__main__.py") + + install(FILES "${PROJECT_BINARY_DIR}/src/hello/__main__.py" DESTINATION .) + + # The extension module must load the hello library as a dependency when the + # extension module is loaded. The easiest way to locate the hello library is + # via RPATH. Absolute RPATHs are possible, but they make the resulting + # binaries not redistributable to other Python installations (conda is broke, + # wheel reuse is broke, and more!). + # + # Placing the hello library in the package and using relative RPATHs that + # doesn't point outside of the package means that the built package is + # relocatable. This allows for safe binary redistribution. + if(APPLE) + set_target_properties( + _hello PROPERTIES INSTALL_RPATH "@loader_path/${CMAKE_INSTALL_LIBDIR}") + else() + set_target_properties(_hello PROPERTIES INSTALL_RPATH + "$ORIGIN/${CMAKE_INSTALL_LIBDIR}") + endif() + +endif() diff --git a/projects/hello-cmake-package/README.md b/projects/hello-cmake-package/README.md new file mode 100644 index 0000000..d023b2a --- /dev/null +++ b/projects/hello-cmake-package/README.md @@ -0,0 +1,33 @@ +# Hello + +This is an example project demonstrating the use of scikit-build for distributing a standalone C library, *hello*; +a CMake package for that library; and a Python wrapper implemented in pybind11. + +The example assume some familiarity with CMake and pybind11, only really going into detail on the scikit-build parts. +pybind11 is used to implement the biding, but anything is possible: swig, C API library, etc. + +To install the package run in the project directory + +```bash +pip install . +``` + +To run the Python tests, first install the package then in the project directory run + +```bash +pytest +``` + +To run the C++ test, first install the package, then configure and build the project + +```bash +cmake -S test/cpp -B build/ -Dhello_ROOT=$(python -m hello --cmakefiles) +cmake --build build/ +``` + +Then run ctest in the build dir + +```bash +cd build/ +ctest +``` diff --git a/projects/hello-cmake-package/cmake/helloConfig.cmake.in b/projects/hello-cmake-package/cmake/helloConfig.cmake.in new file mode 100644 index 0000000..7b21d03 --- /dev/null +++ b/projects/hello-cmake-package/cmake/helloConfig.cmake.in @@ -0,0 +1,3 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/helloTargets.cmake") diff --git a/projects/hello-cmake-package/include/hello.hpp b/projects/hello-cmake-package/include/hello.hpp new file mode 100644 index 0000000..bb96d65 --- /dev/null +++ b/projects/hello-cmake-package/include/hello.hpp @@ -0,0 +1,14 @@ +#ifndef HELLO_HPP +#define HELLO_HPP + +#include + +namespace hello { + +void hello(); + +int return_two(); + +} // namespace hello + +#endif diff --git a/projects/hello-cmake-package/pyproject.toml b/projects/hello-cmake-package/pyproject.toml new file mode 100644 index 0000000..54ec47b --- /dev/null +++ b/projects/hello-cmake-package/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel", + "scikit-build", + "cmake>=3.14", + "ninja", + "pybind11>=2.6" +] +build-backend = "setuptools.build_meta" diff --git a/projects/hello-cmake-package/setup.py b/projects/hello-cmake-package/setup.py new file mode 100644 index 0000000..805d0cd --- /dev/null +++ b/projects/hello-cmake-package/setup.py @@ -0,0 +1,14 @@ +from skbuild import setup + + +setup( + name="hello-cmake-package", + version="0.1.0", + packages=['hello'], + package_dir={'': 'src'}, + cmake_install_dir='src/hello') + +# When building extension modules `cmake_install_dir` should always be set to the +# location of the package you are building extension modules for. +# Specifying the installation directory in the CMakeLists subtley breaks the relative +# paths in the helloTargets.cmake file to all of the library components. diff --git a/projects/hello-cmake-package/src/hello.cpp b/projects/hello-cmake-package/src/hello.cpp new file mode 100644 index 0000000..c4f7462 --- /dev/null +++ b/projects/hello-cmake-package/src/hello.cpp @@ -0,0 +1,9 @@ +#include "hello.hpp" + +namespace hello { + +void hello() { std::cout << "Hello, World!" << std::endl; } + +int return_two() { return 2; } + +} // namespace hello diff --git a/projects/hello-cmake-package/src/hello/__init__.py b/projects/hello-cmake-package/src/hello/__init__.py new file mode 100644 index 0000000..9218bdc --- /dev/null +++ b/projects/hello-cmake-package/src/hello/__init__.py @@ -0,0 +1,3 @@ +from ._hello import hello, return_two + +__all__ = ("hello", "return_two") diff --git a/projects/hello-cmake-package/src/hello/__main__.py.in b/projects/hello-cmake-package/src/hello/__main__.py.in new file mode 100644 index 0000000..85ab513 --- /dev/null +++ b/projects/hello-cmake-package/src/hello/__main__.py.in @@ -0,0 +1,30 @@ +import argparse +import sys +from typing import List, Any +from pathlib import Path +import hello + + +def main(argv: List[Any]) -> int: + + parser = argparse.ArgumentParser() + parser.add_argument("--cmakefiles", action='store_true', help="Print hello project CMake module directory. Useful for setting hello_ROOT in CMake.") + parser.add_argument("--prefix", action='store_true', help="Print hello package installation prefix. Useful for setting CMAKE_PREFIX_PATH in CMake.") + + args = parser.parse_args(args=argv[1:]) + + if not argv[1:]: + parser.print_help() + return + + prefix = Path(hello.__file__).parent + + if args.cmakefiles: + print(prefix / "@HELLO_CMAKE_PACKAGE_INSTALL_SUBDIR@") + + if args.prefix: + print(prefix) + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/projects/hello-cmake-package/src/hello/hello_py.cpp b/projects/hello-cmake-package/src/hello/hello_py.cpp new file mode 100644 index 0000000..8bfce75 --- /dev/null +++ b/projects/hello-cmake-package/src/hello/hello_py.cpp @@ -0,0 +1,9 @@ +#include "hello.hpp" +#include + + +PYBIND11_MODULE(_hello, m) { + m.doc() = "Example module"; + m.def("hello", &hello::hello, "Prints \"Hello, World!\""); + m.def("return_two", &hello::return_two, "Returns 2"); +} diff --git a/projects/hello-cmake-package/tests/cpp/CMakeLists.txt b/projects/hello-cmake-package/tests/cpp/CMakeLists.txt new file mode 100644 index 0000000..5837a40 --- /dev/null +++ b/projects/hello-cmake-package/tests/cpp/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.14...3.19) + +project(hello-user VERSION 0.1.0) + +find_package(hello REQUIRED) + +add_executable(hello_user "${PROJECT_SOURCE_DIR}/test.cpp") +target_link_libraries(hello_user PRIVATE hello::hello) + +include(CTest) + +add_test(NAME hello_test COMMAND hello_user) diff --git a/projects/hello-cmake-package/tests/cpp/test.cpp b/projects/hello-cmake-package/tests/cpp/test.cpp new file mode 100644 index 0000000..86af5ea --- /dev/null +++ b/projects/hello-cmake-package/tests/cpp/test.cpp @@ -0,0 +1,8 @@ +#include "hello.hpp" + + +int main() +{ + hello::hello(); + return (hello::return_two() != 2); +} diff --git a/projects/hello-cmake-package/tests/test_hello.py b/projects/hello-cmake-package/tests/test_hello.py new file mode 100644 index 0000000..16ebe9b --- /dev/null +++ b/projects/hello-cmake-package/tests/test_hello.py @@ -0,0 +1,9 @@ +import hello + + +def test_hello(): + hello.hello() + + +def test_return_two(): + assert hello.return_two() == 2