CMake: Ground the documented CMake behavior in tests

libeigen/eigen!2861

Co-authored-by: Rasmus Munk Larsen <rmlarsen@gmail.com>
This commit is contained in:
Rasmus Munk Larsen
2026-08-18 19:02:52 -07:00
co-authored by Rasmus Munk Larsen
parent cd5b44a5ad
commit 18f650509b
18 changed files with 819 additions and 6 deletions
+28
View File
@@ -110,6 +110,34 @@ that is impractical, that it reaches the new code by construction.
- Exercise the customization points users are documented to have (custom scalars, functors without declared traits),
not only the built-in specializations that happen to satisfy a new precondition.
## Build-System Tests
[`test/buildsystem`](../test/buildsystem) holds the coverage for Eigen's own CMake surface: what an install tree
contains, what `find_package(Eigen3)` and the version ranges in
[`cmake/Eigen3ConfigVersion.cmake.in`](../cmake/Eigen3ConfigVersion.cmake.in) accept, and how an embedding project
opts out of Eigen's install rules. They exist because those are claims
[`doc/TopicCMakeGuide.dox`](../doc/TopicCMakeGuide.dox) makes to users and nothing else checks; the blocking
documentation job only builds the docs, it does not run what they describe.
```bash
cmake -G Ninja -S . -B build -DEIGEN_BUILD_TESTING=ON
cmake -E chdir build ctest -L buildsystem --output-on-failure
```
`ctest --test-dir` would be the shorter spelling, but that option arrived in CMake 3.20; the 3.17 Eigen supports
accepts and ignores it, inspects the source directory instead, reports that no tests were found, and exits
successfully.
No target needs building first: each scenario runs its own nested configure, build, and install into the CTest
binary directory. Add a claim by dropping a scenario in `scenarios/` and naming it in the list in
`test/buildsystem/CMakeLists.txt`; the driver `run_scenario.cmake` supplies the assertion helpers.
Two hazards specific to these tests. Eigen calls `export(PACKAGE Eigen3)`, so CMake's user package registry names
every Eigen build tree on the machine — a `find_package` scenario must disable both registries and assert the package
came from the prefix it installed, or it passes without reading that prefix at all. And because CMake registers the
tests, a guard that stops matching yields an empty selection rather than a failure, so the CI job runs `ctest` with
`--no-tests=error`.
## Configurations The Test Suite Cannot See
- In the default host-test configuration, no test compiles an `EIGEN_NO_DEBUG` code path: `test/main.h` undefines
+2
View File
@@ -15,6 +15,8 @@ core.*
*.bak
*~
*.build*
# Keep the build-system test job's script, whose name matches the rule above.
!ci/scripts/test.buildsystem.script.sh
.tidy-build/
*.moc.*
*.moc
+4
View File
@@ -754,6 +754,10 @@ if (EIGEN_BUILD_TESTING)
add_subdirectory(failtest EXCLUDE_FROM_ALL)
endif()
# Build-system integration tests. These build nothing in this project, so
# they need no place in either target above.
add_subdirectory(test/buildsystem)
ei_testing_print_summary()
if (EIGEN_SPLIT_TESTSUITE)
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
# Configures Eigen and runs only the build-system integration tests under
# test/buildsystem.
#
# This job builds and consumes no artifact, unlike the paired build/test jobs:
# each scenario drives its own nested configure, build, and install, so the
# enclosing build directory holds nothing but the CTest definitions. For the
# same reason the job stays off the content-addressed pass cache, which keys on
# a test's executable; these tests have none, so a recorded pass would outlive
# any change to Eigen's CMake files.
set -ex
rootdir=$(pwd)
mkdir -p "${EIGEN_CI_BUILDDIR}"
cd "${EIGEN_CI_BUILDDIR}"
# The scenarios configure Eigen themselves with whatever options they need, so
# this configure only has to register the tests.
#
# EIGEN_CI_ADDITIONAL_ARGS carries several -D options in a single variable and
# has to word-split; ci/build.linux.gitlab-ci.yml documents that contract for
# the jobs that set it. Every other expansion here is a single word.
# shellcheck disable=SC2086
cmake -G Ninja \
"-DCMAKE_C_COMPILER=${EIGEN_CI_C_COMPILER}" \
"-DCMAKE_CXX_COMPILER=${EIGEN_CI_CXX_COMPILER}" \
-DEIGEN_BUILD_TESTING=ON \
-DEIGEN_BUILD_DOC=OFF \
-DEIGEN_BUILD_DEMOS=OFF \
-DEIGEN_BUILD_BLAS=OFF \
-DEIGEN_BUILD_LAPACK=OFF \
${EIGEN_CI_ADDITIONAL_ARGS} "${rootdir}"
# --no-tests=error matters more here than in the paired test jobs: everything
# that registers these tests is itself CMake, so a guard in
# test/buildsystem/CMakeLists.txt that stops matching would otherwise leave
# ctest reporting success over an empty selection.
ctest -L buildsystem --output-on-failure --no-tests=error "-j${NPROC}"
cd "${rootdir}"
set +x
+58
View File
@@ -731,3 +731,61 @@ test:linux:aarch64:clang-14:default:smoketest:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
tags:
- saas-linux-small-arm64
##### Build-System Integration #################################################
# Grounds the CMake behavior doc/TopicCMakeGuide.dox documents: find_package
# against an installed tree, the version-range contract of the hand-written
# Eigen3ConfigVersion.cmake, embedding via add_subdirectory, and the ways an
# embedding project opts out of Eigen's install rules.
#
# Self-contained, so it neither extends .test:linux nor needs a build job: the
# scenarios run their own nested configures against the checkout, and the
# enclosing build directory holds only the CTest definitions. Staying off
# .test:linux also keeps it off the pass cache, which keys on a test's
# executable -- these tests have none, so a recorded pass would outlive any
# change to Eigen's CMake files.
#
# Cheap -- a nested configure and a small compile per scenario, six seconds of
# script on the runner, inside a job that takes half a minute once the image
# pull and checkout are counted -- but still gated on the files that can make
# it fail rather than run on every merge request: Eigen's own CMake surface,
# the guide it grounds, and the job's own definition and script. A push to the
# default branch runs it unconditionally, as the backstop for whatever the path
# list does not anticipate.
#
# Not behind the all-tests label, unlike the documentation job it complements:
# that label is why an incorrect claim in the CMake guide reached a release
# unnoticed, and an MR that edits these files is exactly the MR that needs the
# answer before merge rather than after.
test:linux:buildsystem:
extends: .common:linux:cross
image: ${EIGEN_CI_IMAGE_LINUX_AMD64_SMOKETEST_BUILD}
stage: test
needs: []
variables:
EIGEN_CI_TARGET_ARCH: x86_64
EIGEN_CI_CROSS_TARGET_TRIPLE: x86_64-linux-gnu
EIGEN_CI_C_COMPILER: gcc-10
EIGEN_CI_CXX_COMPILER: g++-10
EIGEN_CI_SKIP_APT: "true"
script:
- . ci/scripts/test.buildsystem.script.sh
tags:
- saas-linux-medium-amd64
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- CMakeLists.txt
- cmake/**/*
- Eigen/Version
- blas/CMakeLists.txt
- lapack/CMakeLists.txt
- unsupported/**/CMakeLists.txt
- doc/TopicCMakeGuide.dox
- test/buildsystem/**/*
- ci/test.linux.gitlab-ci.yml
- ci/scripts/test.buildsystem.script.sh
- if: $CI_PIPELINE_SOURCE == "schedule"
- if: $CI_PIPELINE_SOURCE == "web"
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+31 -6
View File
@@ -36,12 +36,15 @@ instances of %Eigen installed, `find_package` will use the first one
encountered. To request a specific version of %Eigen, use the `<version>`
option in `find_package`:
```
find_package(Eigen3 3.4 REQUIRED NO_MODULE) # Restricts to 3.4.z
find_package(Eigen3 3.4 REQUIRED NO_MODULE) # Any version >=3.4 but <4.0.0.
```
Starting with Eigen 3.4.1, we also support a range spanning major versions:
```
find_package(Eigen3 3.4...5 REQUIRED NO_MODULE) # Any version >=3.4.1 but <6.0.0.
```
\note Version ranges are `find_package` syntax introduced in %CMake 3.19. Older %CMake
passes `3.4...5` through as a single version string, which `Eigen3ConfigVersion.cmake`
cannot parse and therefore rejects.
Do not forget to set the <a href="https://cmake.org/cmake/help/v3.7/variable/CMAKE_PREFIX_PATH.html">\c CMAKE_PREFIX_PATH </a> variable if Eigen is not installed in a default location or if you want to pick a specific version. For instance:
\code{.sh}
@@ -81,7 +84,6 @@ FetchContent_Declare(
GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
GIT_TAG master
GIT_SHALLOW TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(Eigen)
@@ -106,10 +108,33 @@ FetchContent_MakeAvailable(Eigen)
\subsection title_fetchcontent_exclude Using EXCLUDE_FROM_ALL
Projects that depend on %Eigen should typically exclude installing %Eigen by
default to avoid overwriting a previous installation. Pass
\c EXCLUDE_FROM_ALL in the \c FetchContent_Declare call (as shown above) or
the equivalent \c add_subdirectory option so that %Eigen's install rules are
not included in the default install target.
default to avoid overwriting a previous installation. Passing
\c EXCLUDE_FROM_ALL to \c add_subdirectory drops %Eigen's install rules, and in
addition keeps %Eigen's targets out of the default build. That form works on
every %CMake version %Eigen supports:
\code{.cmake}
add_subdirectory(path/to/eigen eigen-build EXCLUDE_FROM_ALL)
\endcode
\c FetchContent_Declare accepts the same keyword, but only from %CMake 3.28
onwards, so a project using it has to say so:
\code{.cmake}
cmake_minimum_required(VERSION 3.28)
include(FetchContent)
FetchContent_Declare(
Eigen
GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
GIT_TAG master
GIT_SHALLOW TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(Eigen)
\endcode
\warning %CMake 3.17 through 3.27 accept \c EXCLUDE_FROM_ALL in
\c FetchContent_Declare without complaint and ignore it, so %Eigen is installed
anyway.
\subsection title_fetchcontent_cmake330 Note for CMake 3.30 and later
+59
View File
@@ -0,0 +1,59 @@
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
# Build-system integration tests. Each scenario drives a real configure,
# build, and install of Eigen through CMake and asserts a claim that
# doc/TopicCMakeGuide.dox makes about consuming Eigen, so documented CMake
# behavior cannot drift from the tree without a test failing.
#
# Registered only for a top-level, non-cross-compiling build: a scenario
# configures and runs a nested project, which needs a usable host toolchain
# rather than the artifacts of the enclosing build.
if(NOT PROJECT_IS_TOP_LEVEL OR CMAKE_CROSSCOMPILING)
return()
endif()
set(EIGEN_BUILDSYSTEM_SCENARIOS
find_package
find_package_version_reject
add_subdirectory
install_default
exclude_from_all
)
# find_package version ranges are CMake 3.19 syntax. On the 3.17 Eigen still
# supports, "3.4...5" reaches Eigen3ConfigVersion.cmake as one unparsable
# version string and the consumer configure fails, so the claim this scenario
# tests cannot be stated there at all.
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.19)
list(APPEND EIGEN_BUILDSYSTEM_SCENARIOS find_package_version_range)
endif()
# EIGEN_BUILD_PKGCONFIG only exists off Windows, so the scenario asserting
# that it suppresses eigen3.pc has nothing to assert there.
if(NOT WIN32 OR NOT CMAKE_HOST_SYSTEM_NAME MATCHES Windows)
list(APPEND EIGEN_BUILDSYSTEM_SCENARIOS pkgconfig_off)
endif()
set(EIGEN_BUILDSYSTEM_CONFIG "${CMAKE_BUILD_TYPE}")
if(NOT EIGEN_BUILDSYSTEM_CONFIG)
set(EIGEN_BUILDSYSTEM_CONFIG "Release")
endif()
foreach(scenario IN LISTS EIGEN_BUILDSYSTEM_SCENARIOS)
add_test(NAME buildsystem_${scenario}
COMMAND ${CMAKE_COMMAND}
"-DEIGEN_SOURCE_DIR=${Eigen_SOURCE_DIR}"
"-DSCENARIO=${scenario}"
"-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/${scenario}"
"-DGENERATOR=${CMAKE_GENERATOR}"
"-DC_COMPILER=${CMAKE_C_COMPILER}"
"-DCXX_COMPILER=${CMAKE_CXX_COMPILER}"
"-DBUILD_CONFIG=${EIGEN_BUILDSYSTEM_CONFIG}"
-P "${CMAKE_CURRENT_SOURCE_DIR}/run_scenario.cmake")
# Each scenario runs several nested CMake invocations; the default 1500s
# ceiling is ample, but a hung nested build should fail rather than hang.
set_tests_properties(buildsystem_${scenario} PROPERTIES
LABELS "buildsystem" TIMEOUT 900)
endforeach()
@@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
# Consumes an installed Eigen exactly the way doc/TopicCMakeGuide.dox opens:
# find_package plus target_link_libraries against Eigen3::Eigen.
# EIGEN_VERSION_SPEC carries the optional version argument so one project can
# stand in for the plain, ranged, and rejected forms the guide documents.
cmake_minimum_required(VERSION 3.17)
project(eigen_installed_consumer CXX)
find_package(Eigen3 ${EIGEN_VERSION_SPEC} REQUIRED NO_MODULE)
# Prove the package came from the install tree the scenario built. Eigen calls
# export(PACKAGE Eigen3), so an unrelated build tree recorded in CMake's user
# package registry can satisfy find_package and leave the scenario asserting
# nothing about what it installed.
if(EIGEN_EXPECTED_PREFIX)
get_filename_component(eigen_found_dir "${Eigen3_DIR}" REALPATH)
get_filename_component(eigen_expected_prefix "${EIGEN_EXPECTED_PREFIX}" REALPATH)
string(FIND "${eigen_found_dir}" "${eigen_expected_prefix}/" eigen_prefix_position)
if(NOT eigen_prefix_position EQUAL 0)
message(FATAL_ERROR
"found Eigen3 at ${eigen_found_dir}, expected it under ${eigen_expected_prefix}")
endif()
endif()
add_executable(consumer ../main.cpp)
target_link_libraries(consumer Eigen3::Eigen)
+34
View File
@@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: The Eigen Authors
// SPDX-License-Identifier: MPL-2.0
// Consumer program shared by the build-system integration tests. It exists to
// prove that the Eigen a scenario assembled is usable, so it exercises a
// header, a template instantiation, and a computed result rather than only
// including <Eigen/Dense>. The marker on the last line is what the driver
// matches; a run that ends early fails the scenario.
#include <Eigen/Dense>
#include <cstdio>
int main() {
Eigen::Matrix3d m = Eigen::Matrix3d::Identity();
m(0, 1) = 2.0;
m(2, 0) = -1.0;
// Unit triangular up to the (2,0) entry, so the determinant is exactly one
// in binary floating point and needs no tolerance.
if (m.determinant() != 1.0) {
std::printf("determinant was %f, expected 1\n", m.determinant());
return 1;
}
Eigen::Vector3d v = m * Eigen::Vector3d(1.0, 1.0, 1.0);
if (v(0) != 3.0) {
std::printf("product gave %f, expected 3\n", v(0));
return 1;
}
std::printf("eigen-ok\n");
return 0;
}
@@ -0,0 +1,39 @@
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
# Embeds Eigen the way doc/TopicCMakeGuide.dox describes for FetchContent:
# the options are forced before Eigen is added, then Eigen3::Eigen is linked.
# FetchContent_MakeAvailable reduces to this add_subdirectory call, so driving
# add_subdirectory directly tests the same path without a network fetch.
#
# EIGEN_USE_EXCLUDE_FROM_ALL selects the variant the guide's EXCLUDE_FROM_ALL
# section describes.
cmake_minimum_required(VERSION 3.17)
project(eigen_subproject_consumer CXX)
# The consumer runs a test of its own, so the scenario can require that CTest
# in this build directory sees exactly that one test. Without a test to find,
# "Eigen contributed nothing" and "CTest read the wrong directory" produce the
# same empty listing. enable_testing() precedes add_subdirectory so that any
# test Eigen registered would be generated into this tree too.
enable_testing()
set(EIGEN_BUILD_TESTING OFF CACHE BOOL "" FORCE)
set(EIGEN_BUILD_DOC OFF CACHE BOOL "" FORCE)
set(EIGEN_BUILD_DEMOS OFF CACHE BOOL "" FORCE)
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
if(EIGEN_USE_EXCLUDE_FROM_ALL)
add_subdirectory("${EIGEN_SOURCE_DIR}" eigen-build EXCLUDE_FROM_ALL)
else()
add_subdirectory("${EIGEN_SOURCE_DIR}" eigen-build)
endif()
add_executable(consumer ../main.cpp)
target_link_libraries(consumer Eigen3::Eigen)
add_test(NAME consumer_runs COMMAND consumer)
# The consumer's own install rule must keep working whatever Eigen's does, so
# the scenarios can tell "Eigen installed nothing" apart from "nothing ran".
install(TARGETS consumer RUNTIME DESTINATION bin)
+255
View File
@@ -0,0 +1,255 @@
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
# Driver for the build-system integration tests. Run in script mode:
#
# cmake -DEIGEN_SOURCE_DIR=<src> -DSCENARIO=<name> -DWORK_DIR=<dir>
# -DGENERATOR=<gen> -DCXX_COMPILER=<path> -DBUILD_CONFIG=<cfg>
# -P run_scenario.cmake
#
# The driver defines the helpers below and then includes
# scenarios/${SCENARIO}.cmake, which performs the assertions. A failed
# assertion aborts with FATAL_ERROR, which CTest reports as a test failure.
cmake_minimum_required(VERSION 3.17)
foreach(var EIGEN_SOURCE_DIR SCENARIO WORK_DIR GENERATOR BUILD_CONFIG)
if(NOT DEFINED ${var})
message(FATAL_ERROR "run_scenario.cmake requires -D${var}=<value>")
endif()
endforeach()
set(BS_CONSUMER_DIR "${CMAKE_CURRENT_LIST_DIR}/consumers")
set(BS_PREFIX "${WORK_DIR}/prefix")
# The parts of Eigen's own build that no scenario inspects are slow to
# configure and drag in tools a scenario should not depend on.
set(BS_EIGEN_QUIET_OPTIONS
-DEIGEN_BUILD_TESTING=OFF
-DEIGEN_BUILD_DOC=OFF
-DEIGEN_BUILD_DEMOS=OFF
-DEIGEN_BUILD_BLAS=OFF
-DEIGEN_BUILD_LAPACK=OFF
)
# Eigen calls export(PACKAGE Eigen3), so CMake's user package registry names
# every Eigen build tree on the machine. A find_package scenario that let the
# registry answer would pass without ever reading the install tree it just
# built, so every such configure disables both registries and additionally
# asserts, in the consumer project, that the package came from BS_PREFIX.
set(BS_FIND_PACKAGE_ISOLATION
-DCMAKE_FIND_USE_PACKAGE_REGISTRY=OFF
-DCMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY=OFF
)
# Start from an empty work directory so a rerun cannot observe a previous
# run's cache or install tree.
file(REMOVE_RECURSE "${WORK_DIR}")
file(MAKE_DIRECTORY "${WORK_DIR}")
function(bs_fail message)
message(FATAL_ERROR "[${SCENARIO}] ${message}")
endfunction()
# Runs a command, failing the scenario with its merged output unless the exit
# status matches EXPECT_RESULT. EXPECT_RESULT accepts a number or the word
# FAILURE, which admits any non-zero status.
function(bs_run)
cmake_parse_arguments(BS "" "WHAT;EXPECT_RESULT;OUTPUT_VARIABLE" "COMMAND" ${ARGN})
if(NOT DEFINED BS_EXPECT_RESULT)
set(BS_EXPECT_RESULT 0)
endif()
execute_process(COMMAND ${BS_COMMAND}
RESULT_VARIABLE result
OUTPUT_VARIABLE output
ERROR_VARIABLE output)
if(BS_EXPECT_RESULT STREQUAL "FAILURE")
if(result EQUAL 0)
bs_fail("${BS_WHAT} was expected to fail but succeeded\n----\n${output}\n----")
endif()
elseif(NOT result EQUAL BS_EXPECT_RESULT)
bs_fail("${BS_WHAT} exited ${result}, expected ${BS_EXPECT_RESULT}\n----\n${output}\n----")
endif()
if(BS_OUTPUT_VARIABLE)
set(${BS_OUTPUT_VARIABLE} "${output}" PARENT_SCOPE)
endif()
endfunction()
# Eigen's own project() call enables C as well as CXX, so a nested configure of
# Eigen needs both compilers. An image whose compilers are versioned names such
# as gcc-10/g++-10, with no plain cc on PATH, has none to detect on its own.
function(bs_cmake_compiler_args out_var)
set(args "")
if(C_COMPILER)
list(APPEND args "-DCMAKE_C_COMPILER=${C_COMPILER}")
endif()
if(CXX_COMPILER)
list(APPEND args "-DCMAKE_CXX_COMPILER=${CXX_COMPILER}")
endif()
set(${out_var} "${args}" PARENT_SCOPE)
endfunction()
# Configures <src> into <bin>. Remaining arguments are passed to CMake.
function(bs_configure what src bin)
bs_cmake_compiler_args(compiler_args)
bs_run(WHAT "configure ${what}"
COMMAND ${CMAKE_COMMAND} -S "${src}" -B "${bin}" -G "${GENERATOR}"
"-DCMAKE_BUILD_TYPE=${BUILD_CONFIG}" ${compiler_args} ${ARGN})
endfunction()
# Configures <src> expecting failure, returning the output through <output_var>.
function(bs_configure_expect_failure what src bin output_var)
bs_cmake_compiler_args(compiler_args)
bs_run(WHAT "configure ${what}" EXPECT_RESULT FAILURE OUTPUT_VARIABLE output
COMMAND ${CMAKE_COMMAND} -S "${src}" -B "${bin}" -G "${GENERATOR}"
"-DCMAKE_BUILD_TYPE=${BUILD_CONFIG}" ${compiler_args} ${ARGN})
set(${output_var} "${output}" PARENT_SCOPE)
endfunction()
function(bs_build what bin)
bs_run(WHAT "build ${what}"
COMMAND ${CMAKE_COMMAND} --build "${bin}" --config "${BUILD_CONFIG}" ${ARGN})
endfunction()
function(bs_install what bin)
bs_run(WHAT "install ${what}"
COMMAND ${CMAKE_COMMAND} --install "${bin}" --config "${BUILD_CONFIG}")
endfunction()
# Configures Eigen as a top-level project and installs it into BS_PREFIX.
# Remaining arguments are passed to the Eigen configure.
function(bs_install_eigen)
bs_configure("Eigen" "${EIGEN_SOURCE_DIR}" "${WORK_DIR}/eigen-build"
"-DCMAKE_INSTALL_PREFIX=${BS_PREFIX}"
${BS_EIGEN_QUIET_OPTIONS} ${ARGN})
bs_install("Eigen" "${WORK_DIR}/eigen-build")
endfunction()
# Every file in the install prefix, as paths relative to it. Matching against
# this list rather than globbing absolute paths keeps the assertions
# independent of how deep INCLUDE_INSTALL_DIR happens to be.
function(bs_installed_files out_var)
set(found "")
if(EXISTS "${BS_PREFIX}")
file(GLOB_RECURSE absolute LIST_DIRECTORIES false "${BS_PREFIX}/*")
foreach(path IN LISTS absolute)
file(RELATIVE_PATH relative "${BS_PREFIX}" "${path}")
list(APPEND found "${relative}")
endforeach()
endif()
list(SORT found)
set(${out_var} "${found}" PARENT_SCOPE)
endfunction()
# A readable excerpt of the install tree for failure messages.
function(bs_installed_summary out_var)
bs_installed_files(all)
list(LENGTH all count)
set(excerpt "${all}")
if(count GREATER 20)
list(SUBLIST excerpt 0 20 excerpt)
list(APPEND excerpt "... and ${count} files in total")
endif()
string(REPLACE ";" "\n " excerpt "${excerpt}")
set(${out_var} "${count} file(s) installed:\n ${excerpt}" PARENT_SCOPE)
endfunction()
function(bs_matching_installed_files regex out_var)
bs_installed_files(all)
set(hits "")
foreach(path IN LISTS all)
if(path MATCHES "${regex}")
list(APPEND hits "${path}")
endif()
endforeach()
set(${out_var} "${hits}" PARENT_SCOPE)
endfunction()
function(bs_assert_installed regex what)
bs_matching_installed_files("${regex}" hits)
if(NOT hits)
bs_installed_summary(summary)
bs_fail("expected ${what} matching /${regex}/ in the install tree; ${summary}")
endif()
endfunction()
function(bs_assert_not_installed regex what)
bs_matching_installed_files("${regex}" hits)
if(hits)
string(REPLACE ";" "\n " listing "${hits}")
bs_fail("expected no ${what} in the install tree, found:\n ${listing}")
endif()
endfunction()
# Asserts the install tree holds nothing Eigen contributes: headers from the
# supported and unsupported trees, the signature file, eigen3.pc, and the
# CMake package files.
function(bs_assert_no_eigen_installed)
bs_assert_not_installed("/Eigen/Core$" "supported Eigen header")
bs_assert_not_installed("/unsupported/Eigen/Tensor$" "unsupported Eigen header")
bs_assert_not_installed("signature_of_eigen3_matrix_library$" "Eigen signature file")
bs_assert_not_installed("eigen3\\.pc$" "eigen3.pc")
bs_assert_not_installed("Eigen3Config\\.cmake$" "Eigen3 CMake package")
bs_assert_not_installed("Eigen3Targets.*\\.cmake$" "Eigen3 CMake targets")
endfunction()
# Asserts the install tree holds the headers a consumer needs.
function(bs_assert_eigen_headers_installed)
bs_assert_installed("/Eigen/Core$" "supported Eigen header")
bs_assert_installed("/Eigen/Dense$" "Eigen/Dense umbrella")
bs_assert_installed("/unsupported/Eigen/Tensor$" "unsupported Eigen header")
bs_assert_installed("signature_of_eigen3_matrix_library$" "Eigen signature file")
endfunction()
# The version Eigen3ConfigVersion.cmake will carry, read from the same header
# the top-level CMakeLists parses. Deriving the version constraints a scenario
# tests from this keeps them meaningful across a release bump instead of
# pinning them to whatever version happened to be current.
function(bs_eigen_package_version out_major out_minor out_patch)
file(READ "${EIGEN_SOURCE_DIR}/Eigen/Version" header)
foreach(component MAJOR MINOR PATCH)
# Test the match itself, not CMAKE_MATCH_1: a legitimate component value of
# 0 is false to if().
string(REGEX MATCH "define[ \t]+EIGEN_${component}_VERSION[ \t]+([0-9]+)" matched "${header}")
if(NOT matched)
bs_fail("could not read EIGEN_${component}_VERSION from Eigen/Version")
endif()
set(value_${component} "${CMAKE_MATCH_1}")
endforeach()
set(${out_major} "${value_MAJOR}" PARENT_SCOPE)
set(${out_minor} "${value_MINOR}" PARENT_SCOPE)
set(${out_patch} "${value_PATCH}" PARENT_SCOPE)
endfunction()
function(bs_find_consumer_executable bin out_var)
foreach(candidate
"${bin}/consumer" "${bin}/consumer.exe"
"${bin}/${BUILD_CONFIG}/consumer" "${bin}/${BUILD_CONFIG}/consumer.exe")
if(EXISTS "${candidate}")
set(${out_var} "${candidate}" PARENT_SCOPE)
return()
endif()
endforeach()
set(${out_var} "" PARENT_SCOPE)
endfunction()
# Builds a consumer project and runs it, failing unless it prints the marker
# its source promises. Running it is what proves the headers a scenario
# assembled are usable, not merely present.
function(bs_build_and_run_consumer what bin)
bs_build("${what}" "${bin}")
bs_find_consumer_executable("${bin}" consumer_exe)
if(NOT consumer_exe)
bs_fail("${what}: built no consumer executable under ${bin}")
endif()
bs_run(WHAT "run ${what}" OUTPUT_VARIABLE output COMMAND "${consumer_exe}")
if(NOT output MATCHES "eigen-ok")
bs_fail("${what}: consumer ran but did not print its marker\n----\n${output}\n----")
endif()
endfunction()
set(BS_SCENARIO_FILE "${CMAKE_CURRENT_LIST_DIR}/scenarios/${SCENARIO}.cmake")
if(NOT EXISTS "${BS_SCENARIO_FILE}")
message(FATAL_ERROR "no such scenario: ${BS_SCENARIO_FILE}")
endif()
include("${BS_SCENARIO_FILE}")
@@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
# doc/TopicCMakeGuide.dox: Eigen can be embedded without installing it first,
# applying the guide's "Disabling Eigen build options" snippet beforehand. The
# consumer project sets exactly those options, so this covers both claims:
# Eigen3::Eigen is usable from a sub-project, and an embedded Eigen contributes
# nothing to the enclosing project's tests.
bs_configure("embedding consumer" "${BS_CONSUMER_DIR}/subproject" "${WORK_DIR}/consumer"
"-DEIGEN_SOURCE_DIR=${EIGEN_SOURCE_DIR}")
bs_build_and_run_consumer("embedding consumer" "${WORK_DIR}/consumer")
# Embedding Eigen must leave the enclosing project's test list alone: the
# consumer registers one test of its own, and that must be all CTest reports.
#
# CTest gained --test-dir in CMake 3.20. On the 3.17 Eigen supports it is
# accepted and ignored, leaving CTest to inspect its own working directory --
# which is the outer Eigen build tree, whose tests would then be reported as
# the consumer's. Select the consumer build directory by changing into it.
bs_run(WHAT "list tests of the embedding consumer" OUTPUT_VARIABLE tests
COMMAND ${CMAKE_COMMAND} -E chdir "${WORK_DIR}/consumer"
${CMAKE_CTEST_COMMAND} -N)
if(NOT tests MATCHES "consumer_runs")
bs_fail("CTest did not inspect the consumer build directory\n----\n${tests}\n----")
endif()
if(NOT tests MATCHES "Total Tests: 1")
bs_fail("embedding Eigen changed the enclosing project's test list:\n----\n${tests}\n----")
endif()
@@ -0,0 +1,23 @@
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
# doc/TopicCMakeGuide.dox: passing EXCLUDE_FROM_ALL to add_subdirectory "drops
# Eigen's install rules". CMake documents this on the EXCLUDE_FROM_ALL
# directory property rather than on add_subdirectory, and the wording only
# appeared in recent releases, so the behavior is worth pinning at whatever
# CMake version is running the suite.
#
# The guide's separate warning -- that FetchContent_Declare ignores the keyword
# before CMake 3.28 -- cannot be checked here, because a scenario only ever has
# the one CMake that launched it.
bs_configure("embedding consumer with EXCLUDE_FROM_ALL" "${BS_CONSUMER_DIR}/subproject"
"${WORK_DIR}/consumer"
"-DEIGEN_SOURCE_DIR=${EIGEN_SOURCE_DIR}"
"-DCMAKE_INSTALL_PREFIX=${BS_PREFIX}"
"-DEIGEN_USE_EXCLUDE_FROM_ALL=ON")
bs_build_and_run_consumer("embedding consumer with EXCLUDE_FROM_ALL" "${WORK_DIR}/consumer")
bs_install("embedding consumer with EXCLUDE_FROM_ALL" "${WORK_DIR}/consumer")
bs_assert_no_eigen_installed()
bs_assert_installed("bin/consumer" "the consumer's own executable")
@@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
# doc/TopicCMakeGuide.dox: an installed Eigen is consumed with
# find_package(Eigen3 REQUIRED NO_MODULE) and target_link_libraries against
# Eigen3::Eigen. Also pins what a default top-level install contains, since
# every later scenario asserting an absence is only meaningful against it.
bs_install_eigen()
bs_assert_eigen_headers_installed()
bs_assert_installed("Eigen3Config\\.cmake$" "Eigen3 CMake package")
bs_assert_installed("Eigen3ConfigVersion\\.cmake$" "Eigen3 package version file")
bs_assert_installed("Eigen3Targets\\.cmake$" "Eigen3 exported targets")
bs_assert_installed("/Eigen/Version$" "generated Eigen/Version header")
# pkgconfig_off only asserts that eigen3.pc is absent, so without the positive
# case here it would stay green even if pkg-config installation broke for every
# configuration. EIGEN_BUILD_PKGCONFIG, and with it eigen3.pc, exists only
# where the top-level CMakeLists offers the option.
if(NOT WIN32 OR NOT CMAKE_HOST_SYSTEM_NAME MATCHES Windows)
bs_assert_installed("eigen3\\.pc$" "eigen3.pc")
endif()
bs_configure("consumer" "${BS_CONSUMER_DIR}/installed" "${WORK_DIR}/consumer"
"-DCMAKE_PREFIX_PATH=${BS_PREFIX}"
"-DEIGEN_EXPECTED_PREFIX=${BS_PREFIX}"
${BS_FIND_PACKAGE_ISOLATION})
bs_build_and_run_consumer("consumer" "${WORK_DIR}/consumer")
@@ -0,0 +1,54 @@
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
# doc/TopicCMakeGuide.dox: "we also support a range spanning major versions",
# spelled find_package(Eigen3 3.4...5 REQUIRED NO_MODULE). The claim rests on
# the hand-written cmake/Eigen3ConfigVersion.cmake.in, whose inclusive upper
# bound admits everything below <max>+1, so the range has to be exercised
# against a real installed package rather than reasoned about.
#
# The upper bound is taken from Eigen's own major version, which is what makes
# the documented "3.4...5" span the current release; writing 5 here instead
# would turn this into a failing test the day Eigen 6 ships.
#
# test/buildsystem/CMakeLists.txt registers this scenario only for CMake 3.19
# and newer, which is where find_package learned range syntax.
bs_install_eigen()
bs_eigen_package_version(major minor patch)
bs_configure("consumer with range 3.4...${major}" "${BS_CONSUMER_DIR}/installed"
"${WORK_DIR}/consumer"
"-DCMAKE_PREFIX_PATH=${BS_PREFIX}"
"-DEIGEN_EXPECTED_PREFIX=${BS_PREFIX}"
"-DEIGEN_VERSION_SPEC=3.4...${major}"
${BS_FIND_PACKAGE_ISOLATION})
bs_build_and_run_consumer("consumer with a version range" "${WORK_DIR}/consumer")
# An in-range success alone would also be produced by a range branch that
# accepted every range, and find_package_version_reject cannot catch that: it
# only reaches the non-range branch. Bracket the installed version instead, so
# a bound that stops being enforced fails here.
math(EXPR above_major "${major}+1")
math(EXPR above_major_end "${major}+2")
bs_configure_expect_failure("consumer with range ${above_major}...${above_major_end}"
"${BS_CONSUMER_DIR}/installed" "${WORK_DIR}/consumer-above" output
"-DCMAKE_PREFIX_PATH=${BS_PREFIX}"
"-DEIGEN_EXPECTED_PREFIX=${BS_PREFIX}"
"-DEIGEN_VERSION_SPEC=${above_major}...${above_major_end}"
${BS_FIND_PACKAGE_ISOLATION})
if(NOT output MATCHES "Eigen3")
bs_fail("configure failed for some reason other than the version check\n----\n${output}\n----")
endif()
# "...<" is the exclusive upper bound, so this range stops just below the
# installed major and must not match it.
bs_configure_expect_failure("consumer with range 1...<${major}"
"${BS_CONSUMER_DIR}/installed" "${WORK_DIR}/consumer-below" output
"-DCMAKE_PREFIX_PATH=${BS_PREFIX}"
"-DEIGEN_EXPECTED_PREFIX=${BS_PREFIX}"
"-DEIGEN_VERSION_SPEC=1...<${major}"
${BS_FIND_PACKAGE_ISOLATION})
if(NOT output MATCHES "Eigen3")
bs_fail("configure failed for some reason other than the version check\n----\n${output}\n----")
endif()
@@ -0,0 +1,71 @@
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
# The complement of find_package_version_range: without a rejection case, a
# version file that accepted everything would pass that test.
#
# The non-range branch of cmake/Eigen3ConfigVersion.cmake.in accepts a package
# whose version is at least the requested one and below the next major, so a
# bare "3.4" spans >=3.4 to <4.0.0 rather than 3.4.z. Rejecting a request one
# major above the installed package would hold under either reading, so the
# cases below pin the implemented one: both bounds, plus -- whenever the
# installed version leaves room to state it -- a minor below the installed one
# that must still be accepted.
bs_install_eigen()
bs_eigen_package_version(major minor patch)
if(major LESS 4)
bs_fail("this scenario assumes Eigen's major version is at least 4, got ${major}")
endif()
# Upper bound: an earlier major must not answer, whatever its minor.
bs_configure_expect_failure("consumer pinned to 3.4" "${BS_CONSUMER_DIR}/installed"
"${WORK_DIR}/consumer-old" output
"-DCMAKE_PREFIX_PATH=${BS_PREFIX}"
"-DEIGEN_EXPECTED_PREFIX=${BS_PREFIX}"
"-DEIGEN_VERSION_SPEC=3.4"
${BS_FIND_PACKAGE_ISOLATION})
if(NOT output MATCHES "Eigen3")
bs_fail("configure failed for some reason other than the version check\n----\n${output}\n----")
endif()
math(EXPR too_new "${major}+1")
bs_configure_expect_failure("consumer requiring ${too_new}" "${BS_CONSUMER_DIR}/installed"
"${WORK_DIR}/consumer-new" output
"-DCMAKE_PREFIX_PATH=${BS_PREFIX}"
"-DEIGEN_EXPECTED_PREFIX=${BS_PREFIX}"
"-DEIGEN_VERSION_SPEC=${too_new}"
${BS_FIND_PACKAGE_ISOLATION})
if(NOT output MATCHES "Eigen3")
bs_fail("configure failed for some reason other than the version check\n----\n${output}\n----")
endif()
# Lower bound: a patch above the installed one is inside the same major and must
# still be rejected, which a file that only compared majors would not do.
math(EXPR next_patch "${patch}+1")
bs_configure_expect_failure("consumer requiring ${major}.${minor}.${next_patch}"
"${BS_CONSUMER_DIR}/installed"
"${WORK_DIR}/consumer-patch" output
"-DCMAKE_PREFIX_PATH=${BS_PREFIX}"
"-DEIGEN_EXPECTED_PREFIX=${BS_PREFIX}"
"-DEIGEN_VERSION_SPEC=${major}.${minor}.${next_patch}"
${BS_FIND_PACKAGE_ISOLATION})
if(NOT output MATCHES "Eigen3")
bs_fail("configure failed for some reason other than the version check\n----\n${output}\n----")
endif()
# The case that separates ">=X.Y, <X+1.0.0" from "X.Y.z": an older minor of the
# installed major must be accepted. Only statable once the release series has
# moved past .0, so it is a control the scenario adds when it can rather than a
# requirement on the version number.
if(minor GREATER 0)
math(EXPR previous_minor "${minor}-1")
bs_configure("consumer requiring ${major}.${previous_minor}" "${BS_CONSUMER_DIR}/installed"
"${WORK_DIR}/consumer-minor"
"-DCMAKE_PREFIX_PATH=${BS_PREFIX}"
"-DEIGEN_EXPECTED_PREFIX=${BS_PREFIX}"
"-DEIGEN_VERSION_SPEC=${major}.${previous_minor}"
${BS_FIND_PACKAGE_ISOLATION})
bs_build_and_run_consumer("consumer requiring an older minor" "${WORK_DIR}/consumer-minor")
endif()
@@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
# doc/TopicCMakeGuide.dox: "Eigen's install rules are enabled even when it is
# built as a sub-project." This is the baseline that makes exclude_from_all
# meaningful -- without it, a build that installed nothing at all would satisfy
# that scenario too.
bs_configure("embedding consumer" "${BS_CONSUMER_DIR}/subproject" "${WORK_DIR}/consumer"
"-DEIGEN_SOURCE_DIR=${EIGEN_SOURCE_DIR}"
"-DCMAKE_INSTALL_PREFIX=${BS_PREFIX}")
bs_build("embedding consumer" "${WORK_DIR}/consumer")
bs_install("embedding consumer" "${WORK_DIR}/consumer")
bs_assert_eigen_headers_installed()
bs_assert_installed("bin/consumer" "the consumer's own executable")
@@ -0,0 +1,12 @@
# SPDX-FileCopyrightText: The Eigen Authors
# SPDX-License-Identifier: MPL-2.0
# doc/TopicCMakeGuide.dox lists EIGEN_BUILD_PKGCONFIG among the options an
# embedding project turns off. find_package already pins that a default
# top-level install ships eigen3.pc, so this pins that the option suppresses
# it, and that the headers are unaffected either way.
bs_install_eigen(-DEIGEN_BUILD_PKGCONFIG=OFF)
bs_assert_not_installed("eigen3\\.pc$" "eigen3.pc")
bs_assert_eigen_headers_installed()