1# Copyright Louis Dionne 2013-2017 2# Distributed under the Boost Software License, Version 1.0. 3# (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt) 4# 5# 6# This CMake module provides a function generating a unit test to make sure 7# that every public header can be included on its own. 8# 9# When a C++ library or application has many header files, it can happen that 10# a header does not include all the other headers it depends on. When this is 11# the case, it can happen that including that header file on its own will 12# break the compilation. This CMake module generates a dummy executable 13# comprised of many .cpp files, each of which includes a header file that 14# is part of the public API. In other words, the executable is comprised 15# of .cpp files of the form: 16# 17# #include <the/public/header.hpp> 18# 19# and then exactly one `main` function. If this succeeds to compile, it means 20# that the header can be included on its own, which is what clients expect. 21# Otherwise, you have a problem. Since writing these dumb unit tests by hand 22# is tedious and repetitive, you can use this CMake module to automate this 23# task. 24 25# add_header_test(<target> [EXCLUDE_FROM_ALL] [EXCLUDE excludes...] HEADERS headers...) 26# 27# Generates header-inclusion unit tests for all the specified headers. 28# 29# This function creates a target which builds a dummy executable including 30# each specified header file individually. If this target builds successfully, 31# it means that all the specified header files can be included individually. 32# 33# Parameters 34# ---------- 35# <target>: 36# The name of the target to generate. 37# 38# HEADERS headers: 39# A list of header files to generate the inclusion tests for. All headers 40# in this list must be represented as relative paths from the root of the 41# include directory added to the compiler's header search path. In other 42# words, it should be possible to include all headers in this list as 43# 44# #include <${header}> 45# 46# For example, for a library with the following structure: 47# 48# project/ 49# doc/ 50# test/ 51# ... 52# include/ 53# boost/ 54# hana.hpp 55# hana/ 56# transform.hpp 57# tuple.hpp 58# pair.hpp 59# ... 60# 61# When building the unit tests for that library, we'll add `-I project/include' 62# to the compiler's arguments. The list of public headers should therefore contain 63# 64# boost/hana.hpp 65# boost/hana/transform.hpp 66# boost/hana/tuple.hpp 67# boost/hana/pair.hpp 68# ... 69# 70# Usually, all the 'public' header files of a library should be tested for 71# standalone inclusion. A header is considered 'public' if a client should 72# be able to include that header on its own. 73# 74# [EXCLUDE excludes]: 75# An optional list of headers or regexes for which no unit test should be 76# generated. Basically, any header in the list specified by the `HEADERS` 77# argument that matches anything in `EXCLUDE` will be skipped. 78# 79# [EXCLUDE_FROM_ALL]: 80# If provided, the generated target is excluded from the 'all' target. 81# 82function(add_header_test target) 83 cmake_parse_arguments(ARGS "EXCLUDE_FROM_ALL" # options 84 "" # 1 value args 85 "HEADERS;EXCLUDE" # multivalued args 86 ${ARGN}) 87 if (NOT ARGS_HEADERS) 88 message(FATAL_ERROR "The `HEADERS` argument must be provided.") 89 endif() 90 91 if (ARGS_EXCLUDE_FROM_ALL) 92 set(ARGS_EXCLUDE_FROM_ALL "EXCLUDE_FROM_ALL") 93 else() 94 set(ARGS_EXCLUDE_FROM_ALL "") 95 endif() 96 97 foreach(header ${ARGS_HEADERS}) 98 set(skip FALSE) 99 foreach(exclude ${ARGS_EXCLUDE}) 100 if (${header} MATCHES ${exclude}) 101 set(skip TRUE) 102 break() 103 endif() 104 endforeach() 105 if (skip) 106 continue() 107 endif() 108 109 get_filename_component(filename "${header}" NAME_WE) 110 get_filename_component(directory "${header}" DIRECTORY) 111 112 set(source "${CMAKE_CURRENT_BINARY_DIR}/headers/${directory}/${filename}.cpp") 113 if (NOT EXISTS "${source}") 114 file(WRITE "${source}" "#include <${header}>") 115 endif() 116 list(APPEND sources "${source}") 117 endforeach() 118 119 set(standalone_main "${CMAKE_CURRENT_BINARY_DIR}/headers/_standalone_main.cpp") 120 if (NOT EXISTS "${standalone_main}") 121 file(WRITE "${standalone_main}" "int main() { }") 122 endif() 123 add_executable(${target} 124 ${ARGS_EXCLUDE_FROM_ALL} 125 ${sources} 126 "${CMAKE_CURRENT_BINARY_DIR}/headers/_standalone_main.cpp" 127 ) 128endfunction() 129