|
| 1 | +# tests/CMakeLists.txt — build + test the numpycpp Python extension. |
| 2 | +# |
| 3 | +# Build (out-of-source, from project root): |
| 4 | +# cmake -S tests -B tests/build |
| 5 | +# cmake --build tests/build |
| 6 | +# cmake --build tests/build --target pytest |
| 7 | +# |
| 8 | +# Precision strategy (no dlopen / no /proc tricks): |
| 9 | +# float32 → npy_*f scalar from numpy's _multiarray_umath.so → ≤1 ULP |
| 10 | +# float64 → __svml_*8 SVML from numpy's _multiarray_umath.so → 0 ULP |
| 11 | +# Both symbol sets are resolved at link-time via -l:<umath.so>. |
| 12 | + |
| 13 | +cmake_minimum_required(VERSION 3.18) |
| 14 | +project(numpycpp_tests LANGUAGES CXX) |
| 15 | + |
| 16 | +set(CMAKE_CXX_STANDARD 17) |
| 17 | +set(CMAKE_CXX_STANDARD_REQUIRED ON) |
| 18 | +set(CMAKE_CXX_EXTENSIONS OFF) |
| 19 | + |
| 20 | +# --------------------------------------------------------------------------- |
| 21 | +# Dependencies |
| 22 | +# --------------------------------------------------------------------------- |
| 23 | +find_package(Python3 REQUIRED COMPONENTS Interpreter Development) |
| 24 | + |
| 25 | +# Locate pybind11 — prefer the pip-installed copy, fall back to system paths |
| 26 | +execute_process( |
| 27 | + COMMAND "${Python3_EXECUTABLE}" -c |
| 28 | + "import pybind11; print(pybind11.get_cmake_dir())" |
| 29 | + OUTPUT_VARIABLE _PYBIND11_CMAKE_DIR |
| 30 | + OUTPUT_STRIP_TRAILING_WHITESPACE |
| 31 | + ERROR_QUIET |
| 32 | +) |
| 33 | +find_package(pybind11 REQUIRED CONFIG HINTS "${_PYBIND11_CMAKE_DIR}") |
| 34 | + |
| 35 | +find_package(Eigen3 REQUIRED NO_MODULE) |
| 36 | +find_package(OpenMP) |
| 37 | + |
| 38 | +# --------------------------------------------------------------------------- |
| 39 | +# Locate numpy's _multiarray_umath.so |
| 40 | +# This .so bundles Intel SVML (__svml_exp8 / __svml_sin8 / …) and exports |
| 41 | +# numpy's scalar polynomial helpers (npy_expf / npy_sinf / …). |
| 42 | +# We link against it directly so core.h's extern "C" declarations resolve at |
| 43 | +# link time — no dlopen, no runtime symbol lookup. |
| 44 | +# --------------------------------------------------------------------------- |
| 45 | +execute_process( |
| 46 | + COMMAND "${Python3_EXECUTABLE}" -c |
| 47 | + "import os, numpy.core; print(os.path.dirname(numpy.core.__file__))" |
| 48 | + OUTPUT_VARIABLE NUMPY_CORE_DIR |
| 49 | + OUTPUT_STRIP_TRAILING_WHITESPACE |
| 50 | + COMMAND_ERROR_IS_FATAL ANY |
| 51 | +) |
| 52 | +execute_process( |
| 53 | + COMMAND "${Python3_EXECUTABLE}" -c |
| 54 | + "import os, glob, numpy.core |
| 55 | +d = os.path.dirname(numpy.core.__file__) |
| 56 | +print(os.path.basename(glob.glob(os.path.join(d, '_multiarray_umath*.so'))[0]))" |
| 57 | + OUTPUT_VARIABLE NUMPY_UMATH_FILENAME |
| 58 | + OUTPUT_STRIP_TRAILING_WHITESPACE |
| 59 | + COMMAND_ERROR_IS_FATAL ANY |
| 60 | +) |
| 61 | +message(STATUS "NumPy core : ${NUMPY_CORE_DIR}") |
| 62 | +message(STATUS "NumPy umath : ${NUMPY_UMATH_FILENAME}") |
| 63 | + |
| 64 | +# --------------------------------------------------------------------------- |
| 65 | +# Python extension module: numpycpp |
| 66 | +# --------------------------------------------------------------------------- |
| 67 | +pybind11_add_module(numpycpp MODULE |
| 68 | + "${CMAKE_CURRENT_SOURCE_DIR}/module.cpp" |
| 69 | +) |
| 70 | + |
| 71 | +target_include_directories(numpycpp PRIVATE |
| 72 | + "${CMAKE_CURRENT_SOURCE_DIR}/.." # numpy/core.h, numpy/einsum.h, … |
| 73 | + "${CMAKE_CURRENT_SOURCE_DIR}/../pycpp" # core_py.h, linalg_py.h, … |
| 74 | +) |
| 75 | + |
| 76 | +target_compile_options(numpycpp PRIVATE |
| 77 | + -O3 |
| 78 | + -march=native # enables __AVX512F__ on this machine |
| 79 | + -fno-math-errno |
| 80 | + -fno-trapping-math |
| 81 | + -ffp-contract=off # no FMA contraction (keeps results reproducible) |
| 82 | + -DNUMPYCPP_USE_NUMPY_MATH # activates npy_* / SVML specialisations in core.h |
| 83 | +) |
| 84 | + |
| 85 | +if(Eigen3_FOUND) |
| 86 | + target_link_libraries(numpycpp PRIVATE Eigen3::Eigen) |
| 87 | +endif() |
| 88 | + |
| 89 | +if(OpenMP_CXX_FOUND) |
| 90 | + target_link_libraries(numpycpp PRIVATE OpenMP::OpenMP_CXX) |
| 91 | +endif() |
| 92 | + |
| 93 | +# Link against numpy's umath .so to resolve __svml_* and npy_* symbols. |
| 94 | +# |
| 95 | +# Naming constraints: |
| 96 | +# • The file is _multiarray_umath*.so — no "lib" prefix. |
| 97 | +# • Passing the absolute path makes CMake generate "-l_multiarray_umath…" |
| 98 | +# (strips dir + .so) which ld cannot find. |
| 99 | +# • LINK_FLAGS / target_link_options put flags BEFORE object files; |
| 100 | +# with GNU ld --as-needed (Ubuntu default) the library is then skipped |
| 101 | +# because no symbols are referenced yet → runtime undefined symbols. |
| 102 | +# |
| 103 | +# Solution: items starting with "-l" inside target_link_libraries() are: |
| 104 | +# (a) passed verbatim (no CMake name mangling), and |
| 105 | +# (b) placed AFTER object files in the linker command (the link-libraries |
| 106 | +# slot, not the link-flags slot) — satisfying --as-needed ordering. |
| 107 | +# target_link_directories() adds -L<dir> (safe before objects). |
| 108 | +target_link_directories(numpycpp PRIVATE "${NUMPY_CORE_DIR}") |
| 109 | +target_link_libraries(numpycpp PRIVATE "-l:${NUMPY_UMATH_FILENAME}") |
| 110 | + |
| 111 | +# Embed the numpy core directory as RPATH so the loader finds the .so at runtime. |
| 112 | +set_target_properties(numpycpp PROPERTIES |
| 113 | + BUILD_RPATH "${NUMPY_CORE_DIR}" |
| 114 | + INSTALL_RPATH "${NUMPY_CORE_DIR}" |
| 115 | +) |
| 116 | + |
| 117 | +# --------------------------------------------------------------------------- |
| 118 | +# Test targets |
| 119 | +# --------------------------------------------------------------------------- |
| 120 | +# PYTHONPATH must include the build dir so `import numpycpp` resolves. |
| 121 | +set(PYTEST_ENV "PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}:$ENV{PYTHONPATH}") |
| 122 | + |
| 123 | +# `cmake --build . --target pytest` — verbose, shows per-test output |
| 124 | +add_custom_target(pytest |
| 125 | + COMMAND ${CMAKE_COMMAND} -E env "${PYTEST_ENV}" |
| 126 | + "${Python3_EXECUTABLE}" -m pytest |
| 127 | + "${CMAKE_CURRENT_SOURCE_DIR}/test_all.py" |
| 128 | + -v --tb=short --no-header |
| 129 | + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/.." |
| 130 | + DEPENDS numpycpp |
| 131 | + COMMENT "pytest (PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR})" |
| 132 | + VERBATIM |
| 133 | +) |
| 134 | + |
| 135 | +# CTest integration: `ctest` or `cmake --build . --target test` |
| 136 | +enable_testing() |
| 137 | +add_test( |
| 138 | + NAME pytest |
| 139 | + COMMAND "${Python3_EXECUTABLE}" -m pytest |
| 140 | + "${CMAKE_CURRENT_SOURCE_DIR}/test_all.py" |
| 141 | + -q --tb=short --no-header |
| 142 | + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/.." |
| 143 | +) |
| 144 | +set_tests_properties(pytest PROPERTIES |
| 145 | + ENVIRONMENT "${PYTEST_ENV}" |
| 146 | +) |
0 commit comments