common.cmake
7ef592e9
 # https://git.rcrnstn.net/rcrnstn/cmake-common
 
 ## CMake modules
 include(FetchContent)
 include(CMakePackageConfigHelpers)
 include(CheckIPOSupported)
 include(GNUInstallDirs)
 include(CTest)
 if(NOT CPack_CMake_INCLUDED)
     include(CPack)
 endif()
 
 function(common)
     ## Arguments
     set(OPTIONS
         DISABLE_WSHADOW
         DISABLE_SANITIZERS
         DISABLE_CPPCHECK
         DISABLE_CLANG_TIDY
         DISABLE_INCLUDE_WHAT_YOU_USE
     )
     set(ONE_VALUE_ARGS
         CXX_STANDARD
     )
     set(MULTI_VALUE_ARGS
         DEFINITIONS
         PACKAGES
         EXTERNAL
         FETCHCONTENT
         DEPENDENCIES_PUBLIC
         DEPENDENCIES_PRIVATE
         DEPENDENCIES_TESTS
     )
     cmake_parse_arguments(PARSE_ARGV 0 ARG
         "${OPTIONS}"
         "${ONE_VALUE_ARGS}"
         "${MULTI_VALUE_ARGS}"
     )
 
     ## Variables
     set(TEST_TARGETS)
     set(PROJECT_IS_TOP_LEVEL)
     set(PROJECT_IS_INTERFACE_LIBRARY)
     set(PROJECT_IS_DEBUG)
     get_target_property(PROJECT_TYPE ${PROJECT_NAME} TYPE)
     string(TOLOWER "${CMAKE_BUILD_TYPE}" PROJECT_BUILD_TYPE)
     if("${CMAKE_PROJECT_NAME}" STREQUAL "${PROJECT_NAME}")
         set(PROJECT_IS_TOP_LEVEL TRUE)
     endif()
     if("${PROJECT_TYPE}" STREQUAL "INTERFACE_LIBRARY")
         set(PROJECT_IS_INTERFACE_LIBRARY TRUE)
     endif()
     if("${PROJECT_BUILD_TYPE}" STREQUAL "debug")
         set(PROJECT_IS_DEBUG TRUE)
     endif()
 
     ## Definitions
     add_compile_definitions(${ARG_DEFINITIONS})
 
     ## Packages
     foreach(PACKAGE ${ARG_PACKAGES})
         find_package(${PACKAGE} REQUIRED)
     endforeach()
 
     ## External
     foreach(EXTERNAL ${ARG_EXTERNAL})
         add_subdirectory(external/${EXTERNAL} EXCLUDE_FROM_ALL)
     endforeach()
 
     ## FetchContent
     foreach(FETCHCONTENT ${ARG_FETCHCONTENT})
         get_filename_component(FETCHCONTENT_NAME ${FETCHCONTENT} NAME)
         FetchContent_Declare(${FETCHCONTENT_NAME}
             GIT_REPOSITORY ${FETCHCONTENT}
             GIT_SHALLOW TRUE
             GIT_PROGRESS TRUE
         )
         FetchContent_MakeAvailable(${FETCHCONTENT_NAME})
     endforeach()
 
     ## Main target
     file(GLOB_RECURSE SRC CONFIGURE_DEPENDS
         RELATIVE "${PROJECT_SOURCE_DIR}"
         src/*
     )
     file(GLOB_RECURSE INCLUDE CONFIGURE_DEPENDS
         RELATIVE "${PROJECT_SOURCE_DIR}"
         include/*
     )
     if(NOT ("${SRC}" STREQUAL "" AND "${INCLUDE}" STREQUAL ""))
         if(PROJECT_IS_INTERFACE_LIBRARY)
             target_include_directories(${PROJECT_NAME}
                 INTERFACE
                     "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>"
                     "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
             )
         else()
             target_sources(${PROJECT_NAME}
                 PRIVATE
                     ${SRC}
                     ${INCLUDE}
             )
             target_include_directories(${PROJECT_NAME}
                 PUBLIC
                     "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>"
                     "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
                 PRIVATE
                     src
             )
             target_link_libraries(${PROJECT_NAME}
                 PUBLIC
                     ${ARG_DEPENDENCIES_PUBLIC}
                 PRIVATE
                     "$<BUILD_INTERFACE:${ARG_DEPENDENCIES_PRIVATE}>"
             )
         endif()
         if(PROJECT_IS_TOP_LEVEL)
             install(
                 DIRECTORY
                     include/
                 DESTINATION
                     ${CMAKE_INSTALL_INCLUDEDIR}
             )
             install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME})
             install(EXPORT ${PROJECT_NAME}
                 FILE ${PROJECT_NAME}Config.cmake
                 DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
             )
             write_basic_package_version_file(${PROJECT_NAME}ConfigVersion.cmake
                 COMPATIBILITY SameMajorVersion
             )
             install(
                 FILES ${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
                 DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
             )
         endif()
     endif()
 
     ## Test targets
     if(PROJECT_IS_TOP_LEVEL AND BUILD_TESTING)
         file(GLOB_RECURSE TESTS_COMMON CONFIGURE_DEPENDS
             tests/common/*
         )
         file(GLOB TESTS CONFIGURE_DEPENDS
             LIST_DIRECTORIES FALSE
             tests/*
         )
         foreach(TEST ${TESTS})
             get_filename_component(TEST_NAME "${TEST}" NAME_WE)
             set(TEST_TARGET "${PROJECT_NAME}-test-${TEST_NAME}")
             add_executable(${TEST_TARGET}
                 ${TEST}
                 ${TESTS_COMMON}
             )
             target_include_directories(${TEST_TARGET}
                 PRIVATE
                     tests
             )
             target_link_libraries(${TEST_TARGET}
                 PRIVATE
                     ${PROJECT_NAME}
                     ${ARG_DEPENDENCIES_TESTS}
             )
             add_test(
                 NAME    ${TEST_TARGET}
                 COMMAND ${TEST_TARGET}
             )
             list(APPEND TEST_TARGETS ${TEST_TARGET})
         endforeach()
     endif()
 
     ## Assets
     file(REMOVE_RECURSE
         ${CMAKE_BINARY_DIR}/assets
     )
     file(GLOB_RECURSE ASSETS CONFIGURE_DEPENDS
         RELATIVE "${PROJECT_SOURCE_DIR}"
         assets/*
     )
     if(NOT "${ASSETS}" STREQUAL "")
         add_custom_target(${PROJECT_NAME}-assets)
         add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-assets)
     endif()
     foreach(ASSET ${ASSETS})
         get_filename_component(ASSET_DIR ${ASSET} DIRECTORY)
         add_custom_command(
             DEPENDS
                 ${PROJECT_SOURCE_DIR}/${ASSET}
             OUTPUT
                 ${CMAKE_BINARY_DIR}/${ASSET}
             COMMAND
                 ${CMAKE_COMMAND} -E make_directory
                     ${CMAKE_BINARY_DIR}/${ASSET_DIR}
             COMMAND
                 ${CMAKE_COMMAND} -E create_symlink
                     ${PROJECT_SOURCE_DIR}/${ASSET}
                     ${CMAKE_BINARY_DIR}/${ASSET}
             VERBATIM
         )
         set_property(TARGET ${PROJECT_NAME}-assets APPEND PROPERTY
             SOURCES ${CMAKE_BINARY_DIR}/${ASSET}
         )
         if(NOT "${ASSET}" MATCHES "/tests/")
             install(
                 FILES
                     ${PROJECT_SOURCE_DIR}/${ASSET}
                 DESTINATION
                     ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}/${ASSET_DIR}
             )
         endif()
     endforeach()
 
     ## Documentation
     if(PROJECT_IS_TOP_LEVEL)
         file(GLOB_RECURSE DOCS CONFIGURE_DEPENDS
             RELATIVE "${PROJECT_SOURCE_DIR}"
             doc/*
         )
         file(GLOB DOCS_ROOT CONFIGURE_DEPENDS
             RELATIVE "${PROJECT_SOURCE_DIR}"
             README*
             LICENSE*
             COPYING*
             CHANGELOG*
             CHANGES*
             HISTORY*
             NEWS*
             RELEASES*
             AUTHORS*
             ACKNOWLEDGMENTS*
             CONTRIBUTORS*
             CONTRIBUTING*
             CODE_OF_CONDUCT*
             SECURITY*
             SUPPORT*
         )
         list(APPEND DOCS ${DOCS_ROOT})
         foreach(DOC ${DOCS})
             get_filename_component(DOC_DIR ${DOC} DIRECTORY)
             install(
                 FILES
                     ${PROJECT_SOURCE_DIR}/${DOC}
                 DESTINATION
                     ${CMAKE_INSTALL_DOCDIR}/${DOC_DIR}
             )
         endforeach()
     endif()
 
     ## Man pages
     if(PROJECT_IS_TOP_LEVEL)
         file(GLOB_RECURSE MANS CONFIGURE_DEPENDS
             RELATIVE "${PROJECT_SOURCE_DIR}"
             man/*
         )
         foreach(MAN ${MANS})
             get_filename_component(MAN_DIR ${MAN} DIRECTORY)
             install(
                 FILES
                     ${PROJECT_SOURCE_DIR}/${MAN}
                 DESTINATION
                     ${CMAKE_INSTALL_MANDIR}/${MAN_DIR}
             )
         endforeach()
     endif()
 
     ## Language version
     if(PROJECT_IS_TOP_LEVEL AND NOT PROJECT_IS_INTERFACE_LIBRARY)
         set_target_properties(${PROJECT_NAME} ${TEST_TARGETS} PROPERTIES
             CXX_STANDARD          "${ARG_CXX_STANDARD}"
             CXX_STANDARD_REQUIRED ON
             CXX_EXTENSIONS        OFF
         )
     endif()
 
     ## Build options
     if(PROJECT_IS_TOP_LEVEL AND NOT PROJECT_IS_INTERFACE_LIBRARY)
         if(MSVC)
             set(COMPILE_OPTIONS
                 /permissive-
                 /WX
                 /W4
             )
         elseif("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|.*Clang")
             set(SANITIZE_OPTIONS)
             if(PROJECT_IS_DEBUG AND NOT ARG_DISABLE_SANITIZERS)
                 set(SANITIZE_OPTIONS
                     $<$<CONFIG:Debug>:
                         -g -fno-omit-frame-pointer
                         -fsanitize=address,leak,undefined
                         -fsanitize=float-divide-by-zero
                         -fsanitize=float-cast-overflow
                         # -fsanitize=pointer-compare
                         # -fsanitize=pointer-subtract
                         # -fsanitize=implicit-conversion
                         -fno-sanitize-recover=all
                     >
                 )
             endif()
             set(WSHADOW_OPTION)
             if(NOT ARG_DISABLE_WSHADOW)
                 set(WSHADOW_OPTION -Wshadow=local)
             endif()
             set(COMPILE_OPTIONS
                 -pedantic
                 -Werror -Wfatal-errors
                 -Wall -Wextra
                 -Wno-missing-braces -Wmissing-field-initializers
                 -Wconversion -Wsign-conversion
                 -Wdouble-promotion
                 -Wimplicit-fallthrough
                 -Wvla
                 -Wzero-as-null-pointer-constant
                 -Weffc++
                 ${WSHADOW_OPTION}
                 ${SANITIZE_OPTIONS}
             )
             set(LINK_OPTIONS
                 ${SANITIZE_OPTIONS}
             )
             check_ipo_supported(RESULT INTERPROCEDURAL_OPTIMIZATION)
         endif()
         set_property(TARGET ${PROJECT_NAME} ${TEST_TARGETS} APPEND PROPERTY
             COMPILE_OPTIONS "${COMPILE_OPTIONS}"
         )
         set_property(TARGET ${PROJECT_NAME} ${TEST_TARGETS} APPEND PROPERTY
             LINK_OPTIONS "${LINK_OPTIONS}"
         )
         set_property(TARGET ${PROJECT_NAME} ${TEST_TARGETS} APPEND PROPERTY
             INTERPROCEDURAL_OPTIMIZATION "${INTERPROCEDURAL_OPTIMIZATION}"
         )
     endif()
 
     ## Sanitizer environment variables
     set(SAN_STRIP_PATH_PREFIX strip_path_prefix=${PROJECT_BINARY_DIR})
     set(ASAN_OPTIONS  "${SAN_STRIP_PATH_PREFIX}")
     set(LSAN_OPTIONS  "${SAN_STRIP_PATH_PREFIX}")
     set(UBSAN_OPTIONS "${SAN_STRIP_PATH_PREFIX}")
     file(GLOB ASAN_SUPP CONFIGURE_DEPENDS
         .asan.supp
     )
     file(GLOB LSAN_SUPP CONFIGURE_DEPENDS
         .lsan.supp
     )
     file(GLOB UBSAN_SUPP CONFIGURE_DEPENDS
         .ubsan.supp
     )
     if(NOT "${ASAN_SUPP}" STREQUAL "")
         set(ASAN_OPTIONS "${ASAN_OPTIONS},suppressions=${ASAN_SUPP}")
     endif()
     if(NOT "${LSAN_SUPP}" STREQUAL "")
         set(LSAN_OPTIONS "${LSAN_OPTIONS},suppressions=${LSAN_SUPP}")
     endif()
     if(NOT "${UBSAN_SUPP}" STREQUAL "")
         set(UBSAN_OPTIONS "${UBSAN_OPTIONS},suppressions=${UBSAN_SUPP}")
     endif()
     # set(ASAN_OPTIONS "${ASAN_OPTIONS},detect_leaks=1")
     # set(UBSAN_OPTIONS "${UBSAN_OPTIONS},print_stacktrace=1")
     set_property(TEST ${TEST_TARGETS} APPEND PROPERTY
         ENVIRONMENT
             ASAN_OPTIONS=${ASAN_OPTIONS}
             LSAN_OPTIONS=${LSAN_OPTIONS}
             UBSAN_OPTIONS=${UBSAN_OPTIONS}
     )
 
     ## Tools
     if(
         PROJECT_IS_TOP_LEVEL AND
         PROJECT_IS_DEBUG AND
         NOT PROJECT_IS_INTERFACE_LIBRARY
     )
         find_program(CPPCHECK             cppcheck)
         find_program(CLANG_TIDY           clang-tidy)
         find_program(INCLUDE_WHAT_YOU_USE include-what-you-use)
         if(CPPCHECK AND NOT ARG_DISABLE_CPPCHECK)
             set(CXX_CPPCHECK ${CPPCHECK}
                 --enable=warning,style
                 --inline-suppr
                 --template "{file}:{line}: ({severity} {id}) {message}"
                 --error-exitcode=1
             )
         endif()
         if(CLANG_TIDY AND NOT ARG_DISABLE_CLANG_TIDY)
             set(CXX_CLANG_TIDY ${CLANG_TIDY}
                 --extra-arg=-Wno-error=unknown-warning-option
                 --extra-arg=-Wno-error=ignored-optimization-argument
             )
         endif()
         if(INCLUDE_WHAT_YOU_USE AND NOT ARG_DISABLE_INCLUDE_WHAT_YOU_USE)
             set(CXX_INCLUDE_WHAT_YOU_USE ${INCLUDE_WHAT_YOU_USE})
         endif()
         set_target_properties(${PROJECT_NAME} ${TEST_TARGETS} PROPERTIES
             EXPORT_COMPILE_COMMANDS  ON
             CXX_CPPCHECK             "${CXX_CPPCHECK}"
             CXX_CLANG_TIDY           "${CXX_CLANG_TIDY}"
             CXX_INCLUDE_WHAT_YOU_USE "${CXX_INCLUDE_WHAT_YOU_USE}"
         )
     endif()
 endfunction()