build: CMAKE_VERIFY_INTERFACE_HEADER_SETS support added

Resolves #441
This commit is contained in:
Mateusz Pusz
2024-01-12 12:30:57 +01:00
parent 6100cefbcb
commit 7c4d2cd5e4
5 changed files with 6 additions and 203 deletions

View File

@ -20,7 +20,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
cmake_minimum_required(VERSION 3.19)
cmake_minimum_required(VERSION 3.24)
project(mp-units-dev LANGUAGES CXX)
list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
@ -74,7 +74,3 @@ add_subdirectory(example)
enable_testing()
add_subdirectory(test)
# tests for standalone headers
include(TestPublicHeaders)
add_public_header_test(test_headers mp-units::mp-units)

View File

@ -1,130 +0,0 @@
# Copyright Louis Dionne 2013-2017
# Distributed under the Boost Software License, Version 1.0.
# (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt)
#
#
# This CMake module provides a function generating a unit test to make sure
# that every public header can be included on its own.
#
# When a C++ library or application has many header files, it can happen that
# a header does not include all the other headers it depends on. When this is
# the case, it can happen that including that header file on its own will
# break the compilation. This CMake module generates a dummy executable
# comprised of many .cpp files, each of which includes a header file that
# is part of the public API. In other words, the executable is comprised
# of .cpp files of the form:
#
# #include <the/public/header.hpp>
#
# and then exactly one `main` function. If this succeeds to compile, it means
# that the header can be included on its own, which is what clients expect.
# Otherwise, you have a problem. Since writing these dumb unit tests by hand
# is tedious and repetitive, you can use this CMake module to automate this
# task.
# add_header_test(<target> [EXCLUDE_FROM_ALL] [EXCLUDE excludes...] HEADERS headers...)
#
# Generates header-inclusion unit tests for all the specified headers.
#
# This function creates a target which builds a dummy executable including
# each specified header file individually. If this target builds successfully,
# it means that all the specified header files can be included individually.
#
# Parameters
# ----------
# <target>:
# The name of the target to generate.
#
# HEADERS headers:
# A list of header files to generate the inclusion tests for. All headers
# in this list must be represented as relative paths from the root of the
# include directory added to the compiler's header search path. In other
# words, it should be possible to include all headers in this list as
#
# #include <${header}>
#
# For example, for a library with the following structure:
#
# project/
# doc/
# test/
# ...
# include/
# boost/
# hana.hpp
# hana/
# transform.hpp
# tuple.hpp
# pair.hpp
# ...
#
# When building the unit tests for that library, we'll add `-I project/include'
# to the compiler's arguments. The list of public headers should therefore contain
#
# boost/hana.hpp
# boost/hana/transform.hpp
# boost/hana/tuple.hpp
# boost/hana/pair.hpp
# ...
#
# Usually, all the 'public' header files of a library should be tested for
# standalone inclusion. A header is considered 'public' if a client should
# be able to include that header on its own.
#
# [EXCLUDE excludes]:
# An optional list of headers or regexes for which no unit test should be
# generated. Basically, any header in the list specified by the `HEADERS`
# argument that matches anything in `EXCLUDE` will be skipped.
#
# [EXCLUDE_FROM_ALL]:
# If provided, the generated target is excluded from the 'all' target.
#
function(add_header_test target)
cmake_parse_arguments(
ARGS
"EXCLUDE_FROM_ALL" # options
"" # 1 value args
"HEADERS;EXCLUDE" # multivalued args
${ARGN}
)
if(NOT ARGS_HEADERS)
message(FATAL_ERROR "The `HEADERS` argument must be provided.")
endif()
if(ARGS_EXCLUDE_FROM_ALL)
set(ARGS_EXCLUDE_FROM_ALL "EXCLUDE_FROM_ALL")
else()
set(ARGS_EXCLUDE_FROM_ALL "")
endif()
foreach(header ${ARGS_HEADERS})
set(skip FALSE)
foreach(exclude ${ARGS_EXCLUDE})
if(${header} MATCHES ${exclude})
set(skip TRUE)
break()
endif()
endforeach()
if(skip)
continue()
endif()
get_filename_component(filename "${header}" NAME_WE)
get_filename_component(directory "${header}" DIRECTORY)
set(source "${CMAKE_CURRENT_BINARY_DIR}/headers/${directory}/${filename}.cpp")
if(NOT EXISTS "${source}")
file(WRITE "${source}" "#include <${header}>")
file(APPEND "${source}" "\n#include <${header}>") # do it twice to ensure that header guards are provided
endif()
list(APPEND sources "${source}")
endforeach()
set(standalone_main "${CMAKE_CURRENT_BINARY_DIR}/headers/_standalone_main.cpp")
if(NOT EXISTS "${standalone_main}")
file(WRITE "${standalone_main}" "int main() { }")
endif()
add_executable(
${target} ${ARGS_EXCLUDE_FROM_ALL} ${sources} "${CMAKE_CURRENT_BINARY_DIR}/headers/_standalone_main.cpp"
)
endfunction()

View File

@ -1,67 +0,0 @@
# The MIT License (MIT)
#
# Copyright (c) 2018 Mateusz Pusz
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
cmake_minimum_required(VERSION 3.19)
function(get_target_linked_targets target targets_out)
get_target_property(linked_libs ${target} INTERFACE_LINK_LIBRARIES)
foreach(linked_lib ${linked_libs})
if(NOT linked_lib IN_LIST targets)
if(TARGET ${linked_lib})
get_target_linked_targets(${linked_lib} child_targets)
list(APPEND targets ${linked_lib} ${child_targets})
endif()
endif()
endforeach()
list(REMOVE_DUPLICATES targets)
list(REMOVE_DUPLICATES link_libs)
set(${targets_out} ${targets} PARENT_SCOPE)
endfunction()
function(get_target_sources target paths_out)
get_target_linked_targets(${target} targets)
list(APPEND targets ${target})
foreach(t ${targets})
get_target_property(sources ${t} HEADER_SET)
if(sources)
get_target_property(source_dir ${t} SOURCE_DIR)
foreach(f ${sources})
file(REAL_PATH "${f}" path BASE_DIRECTORY "${source_dir}")
file(RELATIVE_PATH path ${CMAKE_CURRENT_LIST_DIR} "${path}")
list(APPEND paths "${path}")
endforeach()
endif()
endforeach()
set(${paths_out} ${paths} PARENT_SCOPE)
endfunction()
include(TestHeaders)
function(add_public_header_test target test_target)
get_target_sources(${test_target} sources)
add_header_test(${target} HEADERS ${sources})
target_link_libraries(${target} PRIVATE ${test_target})
target_include_directories(${target} PRIVATE .)
endfunction()

View File

@ -60,7 +60,7 @@ class MPUnitsConan(ConanFile):
default_options = {
"cxx_modules": False,
}
tool_requires = "cmake/[>=3.28.1]"
exports = ["LICENSE.md"]
exports_sources = [
"docs/*",
@ -140,6 +140,8 @@ class MPUnitsConan(ConanFile):
def generate(self):
tc = CMakeToolchain(self)
if self._build_all:
tc.variables["CMAKE_VERIFY_INTERFACE_HEADER_SETS"] = True
if self.options.cxx_modules:
tc.variables["CMAKE_CXX_SCAN_FOR_MODULES"] = True
tc.variables["MP_UNITS_BUILD_CXX_MODULES"] = True
@ -154,6 +156,7 @@ class MPUnitsConan(ConanFile):
cmake.configure(build_script_folder=None if self._build_all else "src")
cmake.build()
if self._build_all:
cmake.build(target="all_verify_interface_header_sets")
cmake.test()
def package_id(self):

View File

@ -460,6 +460,7 @@ the `conan build` with `conan install` command and then follow with a regular CM
```shell
cmake --preset conan-default
cmake --build --preset conan-release
cmake --build --preset conan-release --target all_verify_interface_header_sets
cmake --build --preset conan-release --target test
```