cxx.cmake
84b9b61f
 # https://git.rcrnstn.net/rcrnstn/cmake-cxx
 
 function(cxx)
     ## Arguments
     set(OPTIONS
         DISABLE_SANITIZERS
         DISABLE_CPPCHECK
         DISABLE_CLANG_TIDY
         DISABLE_INCLUDE_WHAT_YOU_USE
     )
     set(ONE_VALUE_ARGS
         CXX_STANDARD
     )
     set(MULTI_VALUE_ARGS
         PACKAGES
         FETCHCONTENT
         DEPENDENCIES_PUBLIC
         DEPENDENCIES_PRIVATE
         DEPENDENCIES_TESTS
         DEFINES
     )
     cmake_parse_arguments(PARSE_ARGV 0 ARG
         "${OPTIONS}"
         "${ONE_VALUE_ARGS}"
         "${MULTI_VALUE_ARGS}"
     )
 
     ## Helper variables
     set(NON_INTERFACE_TARGETS)
     set(PROJECT_IS_INTERFACE_LIBRARY)
     set(PROJECT_IS_TOP_LEVEL)
     get_target_property(PROJECT_TYPE ${PROJECT_NAME} TYPE)
     if("${PROJECT_TYPE}" STREQUAL "INTERFACE_LIBRARY")
         set(PROJECT_IS_INTERFACE_LIBRARY TRUE)
     endif()
     if("${CMAKE_PROJECT_NAME}" STREQUAL "${PROJECT_NAME}")
         set(PROJECT_IS_TOP_LEVEL TRUE)
     endif()
 
     ## CMake modules
     include(FetchContent)
     include(CMakePackageConfigHelpers)
     include(CheckIPOSupported)
     include(GNUInstallDirs)
     if(PROJECT_IS_TOP_LEVEL)
         include(CTest)
         include(CPack)
     endif()
 
     ## Packages
     foreach(PACKAGE ${ARG_PACKAGES})
         find_package(${PACKAGE} REQUIRED)
     endforeach()
 
     ## Fetch content
     foreach(FETCHCONTENT ${ARG_FETCHCONTENT})
         get_filename_component(FETCHCONTENT_NAME ${FETCHCONTENT} NAME)
         FetchContent_Declare(${FETCHCONTENT_NAME}
             GIT_REPOSITORY ${FETCHCONTENT}
         )
         FetchContent_MakeAvailable(${FETCHCONTENT_NAME})
     endforeach()
 
     ## Configure main target
     file(GLOB_RECURSE SRC CONFIGURE_DEPENDS
         src/*
     )
     file(GLOB_RECURSE INCLUDE CONFIGURE_DEPENDS
         include/*
     )
     if(NOT ("${SRC}" STREQUAL "" AND "${INCLUDE}" STREQUAL ""))
         target_sources(${PROJECT_NAME}
             PRIVATE
                 ${SRC}
                 ${INCLUDE}
         )
         set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY
             PUBLIC_HEADER ${INCLUDE}
         )
         if(PROJECT_IS_INTERFACE_LIBRARY)
             target_include_directories(${PROJECT_NAME}
                 INTERFACE
                     "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>"
                     "$<INSTALL_INTERFACE:include>"
             )
         else()
             target_include_directories(${PROJECT_NAME}
                 PUBLIC
                     "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>"
                     "$<INSTALL_INTERFACE:include>"
                 PRIVATE
                     src
             )
             list(APPEND NON_INTERFACE_TARGETS ${PROJECT_NAME})
         endif()
         target_link_libraries(${PROJECT_NAME}
             PUBLIC
                 ${ARG_DEPENDENCIES_PUBLIC}
             PRIVATE
                 "$<BUILD_INTERFACE:${ARG_DEPENDENCIES_PRIVATE}>"
         )
         if(PROJECT_IS_TOP_LEVEL)
             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()
 
     ## Declare and configure test targets
     if(PROJECT_IS_TOP_LEVEL AND BUILD_TESTING)
         file(GLOB_RECURSE TESTS_COMMON CONFIGURE_DEPENDS
             tests/common/*
         )
         file(GLOB TESTS LIST_DIRECTORIES FALSE CONFIGURE_DEPENDS
             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 NON_INTERFACE_TARGETS ${TEST_TARGET})
         endforeach()
     endif()
 
     ## Declare and configure assets
     file(REMOVE_RECURSE
         ${CMAKE_BINARY_DIR}/assets
     )
     file(GLOB_RECURSE ASSETS RELATIVE "${PROJECT_SOURCE_DIR}" CONFIGURE_DEPENDS
         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()
 
     ## Preprocessor defines
     set_property(TARGET ${NON_INTERFACE_TARGETS} APPEND PROPERTY
         COMPILE_DEFINITIONS ${ARG_DEFINES}
     )
 
     ## Language version
     if(NOT "${CXX_STANDARD}" STREQUAL "")
         set_target_properties(${NON_INTERFACE_TARGETS} PROPERTIES
             CXX_STANDARD          ${ARG_CXX_STANDARD}
             CXX_STANDARD_REQUIRED ON
             CXX_EXTENSIONS        OFF
         )
     endif()
 
     ## Build options
     if(MSVC)
         set(COMPILE_OPTIONS
             /permissive-
             /WX
             /W4
         )
     elseif("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|.*Clang")
         if(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(COMPILE_OPTIONS
             -pedantic
             -Werror # -Wfatal-errors
             -Wall -Wextra
             -Wconversion -Wsign-conversion
             -Wdouble-promotion
             -Wimplicit-fallthrough
             -Wvla
             # -Wzero-as-null-pointer-constant
             # -Wshadow
             -Weffc++
             ${SANITIZE_OPTIONS}
         )
         set(LINK_OPTIONS
             ${SANITIZE_OPTIONS}
         )
         check_ipo_supported(RESULT INTERPROCEDURAL_OPTIMIZATION)
     endif()
     set_property(TARGET ${NON_INTERFACE_TARGETS} APPEND PROPERTY
         COMPILE_OPTIONS "${COMPILE_OPTIONS}"
     )
     set_property(TARGET ${NON_INTERFACE_TARGETS} APPEND PROPERTY
         LINK_OPTIONS "${LINK_OPTIONS}"
     )
     set_property(TARGET ${NON_INTERFACE_TARGETS} APPEND PROPERTY
         INTERPROCEDURAL_OPTIMIZATION "${INTERPROCEDURAL_OPTIMIZATION}"
     )
 
     ## 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 ${NON_INTERFACE_TARGETS} APPEND PROPERTY
         ENVIRONMENT
             ASAN_OPTIONS=${ASAN_OPTIONS}
             LSAN_OPTIONS=${LSAN_OPTIONS}
             UBSAN_OPTIONS=${UBSAN_OPTIONS}
     )
 
     ## Tools
     if(PROJECT_IS_TOP_LEVEL)
         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
                 --error-exitcode=1
             )
         endif()
         if(CLANG_TIDY AND NOT ARG_DISABLE_CLANG_TIDY)
             set(CXX_CLANG_TIDY ${CLANG_TIDY})
         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(${NON_INTERFACE_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()