Skip to content

Instantly share code, notes, and snippets.

@mrowrpurr
Last active December 12, 2022 07:54
Show Gist options
  • Save mrowrpurr/03a279960528ce6bb35377dc40ae4f8d to your computer and use it in GitHub Desktop.
Save mrowrpurr/03a279960528ce6bb35377dc40ae4f8d to your computer and use it in GitHub Desktop.
CMake function which adds a target to check every that header in your project has all required #include's

CheckHeaderIncludes.cmake

CMake function which adds a target to check every that header in your project has all required #include's

Example usage:

check_header_includes(
    HEADERS   include/*.h
    LIBRARIES ${PROJECT_NAME}
)

Your CMake project will get a new target added (with one child target per header file).

Run the generated target and it will try to compile each header of your project INDIVIDUALLY.

If there are errors, you can run each header individually by running its target.

image


LIBRARIES specified libraries which the generated targets should link against.

If the rare circumstance that your *.h headers require libraries which your project doesn't link against (or links PRIVATE instead of PUBLIC) then you can specify a full list of libraries for the generated targets to link against:

check_header_includes(
    HEADERS       include/*.h
    LIBRARIES     ${PROJECT_NAME} CommonLibSSE::CommonLibSSE Catch2::Catch2 GTest::gtest
)

You can even provide PACKAGES and we'll find_package for you...

check_header_includes(
    HEADERS       include/*.h
    PACKAGES      GTest Catch2
    LIBRARIES     ${PROJECT_NAME} CommonLibSSE::CommonLibSSE Catch2::Catch2 GTest::gtest
)

And LIBRARY_PATHS and we'll both find_path and target_include_directories for you...

check_header_includes(
    HEADERS       include/*.h
    PACKAGES      GTest Catch2
    LIBRARIES     ${PROJECT_NAME} CommonLibSSE::CommonLibSSE Catch2::Catch2 GTest::gtest
    LIBRARY_PATHS bandit/adapters.h snowhouse/assert.h
)

If you get complaints from Ninja about file paths, you can provide your own TEMP_DIR

check_header_includes(
    ...
   TEMP_DIR      "${CMAKE_SOURCE_DIR}/.tmp"
)
# CMake function which adds a target to check every that header in your project has all required #include's
function(check_header_includes)
set(options)
set(oneValueArgs TARGET INCLUDE_DIR TEMP_DIR)
set(multiValueArgs HEADERS PACKAGES LIBRARY_PATHS LIBRARIES)
cmake_parse_arguments(CHECK_HEADER_INCLUDES "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(NOT CMAKE_OBJECT_PATH_MAX OR CMAKE_OBJECT_PATH_MAX LESS 1000)
# Bump up the max object path or CMake can get angry with some generated paths
# https://stackoverflow.com/questions/68351713/maximum-path-lengths-with-cmake
set(CMAKE_OBJECT_PATH_MAX 1000 PARENT_SCOPE)
endif()
file(RELATIVE_PATH folder_path "${CMAKE_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}")
string(REGEX REPLACE "[^a-zA-Z0-9]" _ folder_name "${folder_path}")
if(NOT CHECK_HEADER_INCLUDES_TARGET)
set(CHECK_HEADER_INCLUDES_TARGET "check_headers_${folder_name}")
endif()
if(NOT CHECK_HEADER_INCLUDES_INCLUDE_DIR)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/include")
set(CHECK_HEADER_INCLUDES_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include")
else()
set(CHECK_HEADER_INCLUDES_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
endif()
endif()
if(NOT CHECK_HEADER_INCLUDES_TEMP_DIR)
set(CHECK_HEADER_INCLUDES_TEMP_DIR "$ENV{TEMP}")
endif()
# Setup a temporary directory to output a bunch of .cpp files to
# where each .cpp file simply contains one #include for the header we're checking
string(SHA1 tmp_folder_name "${CMAKE_CURRENT_SOURCE_DIR}")
set(tmp_dir "${CHECK_HEADER_INCLUDES_TEMP_DIR}/${tmp_folder_name}/${CHECK_HEADER_INCLUDES_TARGET}")
if(EXISTS "${tmp_dir}")
file(REMOVE_RECURSE "${tmp_dir}")
endif()
file(MAKE_DIRECTORY "${tmp_dir}")
add_custom_target(${CHECK_HEADER_INCLUDES_TARGET})
foreach(package ${CHECK_HEADER_INCLUDES_PACKAGES})
find_package(${package} CONFIG REQUIRED)
endforeach()
set(library_paths "")
foreach(lib_path ${CHECK_HEADER_INCLUDES_LIBRARY_PATHS})
find_path(include_dir "${library_path}")
list(APPEND library_paths "${include_dir}")
endforeach()
file(GLOB_RECURSE headers ${CHECK_HEADER_INCLUDES_HEADERS})
set(header_i 1)
foreach(header ${headers})
file(RELATIVE_PATH header_relative "${CHECK_HEADER_INCLUDES_INCLUDE_DIR}" "${header}")
string(REGEX REPLACE "[^a-zA-Z0-9]" _ header_name "${header_relative}")
if(header_name MATCHES "^include_.*")
string(REGEX REPLACE ^include_ "" header_name ${header_name})
endif()
if(header_name MATCHES "\.h$")
string(REGEX REPLACE .h$ "" header_name ${header_name})
endif()
set(header_cpp_tmp_file "${tmp_dir}/${header_i}-${header_name}.cpp")
string(REPLACE "\\" / header_cpp_tmp_file "${header_cpp_tmp_file}") # or add_library gets angry (???)
file(WRITE "${header_cpp_tmp_file}" "#include \"${header_relative}\"")
set(header_target "check_${header_i}_${header_name}")
add_library("${header_target}" STATIC "${header_cpp_tmp_file}")
foreach(library ${CHECK_HEADER_INCLUDES_LIBRARIES})
target_link_libraries(${header_target} PRIVATE ${library})
endforeach()
foreach(include_dir ${library_paths})
target_include_directories(${header_target} PRIVATE ${include_dir})
endforeach()
add_dependencies(${CHECK_HEADER_INCLUDES_TARGET} ${header_target})
MATH(EXPR header_i "${header_i}+1")
endforeach()
endfunction()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment