# 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 PACKAGES EXTERNAL FETCHCONTENT DEPENDENCIES_PUBLIC DEPENDENCIES_PRIVATE DEPENDENCIES_TESTS DEFINITIONS ) cmake_parse_arguments(PARSE_ARGV 0 ARG "${OPTIONS}" "${ONE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ) ## Variables 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() ## 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 target_compile_definitions(${PROJECT_NAME} PUBLIC ${ARG_DEFINITIONS}) 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 set(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() ## Build targets set(BUILD_TARGETS ${TEST_TARGETS}) if(NOT PROJECT_IS_INTERFACE_LIBRARY) list(APPEND BUILD_TARGETS ${PROJECT_NAME}) 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) if("${ASSET}" MATCHES "/tests/") set(ASSET_TEST TRUE) else() set(ASSET_TEST FALSE) endif() if(ASSET_TEST AND NOT PROJECT_IS_TOP_LEVEL) continue() endif() 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_TEST) 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 standard set_target_properties(${BUILD_TARGETS} PROPERTIES CXX_STANDARD "${ARG_CXX_STANDARD}" CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF ) ## Build options if(PROJECT_IS_TOP_LEVEL) 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 ${BUILD_TARGETS} APPEND PROPERTY COMPILE_OPTIONS "${COMPILE_OPTIONS}" ) set_property(TARGET ${BUILD_TARGETS} APPEND PROPERTY LINK_OPTIONS "${LINK_OPTIONS}" ) set_property(TARGET ${BUILD_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) 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} --error-exitcode=1 --enable=warning,style --inline-suppr --template "{file}:{line}: ({severity} {id}) {message}" ) endif() if(CLANG_TIDY AND NOT ARG_DISABLE_CLANG_TIDY) set(CXX_CLANG_TIDY ${CLANG_TIDY} --extra-arg=-Wno-unknown-warning-option --extra-arg=-Wno-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} -Wno-unknown-warning-option -Wno-ignored-optimization-argument ) endif() set_target_properties(${BUILD_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()