set(CLANG_PATH "" CACHE PATH "Path to Clang Toolset (if not in environment)") function(string_escape) cmake_parse_arguments( PARSE_ARGV 0 _ARGS "" "OUTPUT;INPUT" "" ) set(_el "${_ARGS_INPUT}") string(REPLACE "\\" "\\\\" _el "${_el}") string(REPLACE "\"" "\\\"" _el "${_el}") set(${_ARGS_OUTPUT} "${_el}" PARENT_SCOPE) endfunction() function(string_append_escaped) cmake_parse_arguments( PARSE_ARGV 0 _ARGS "" "OUTPUT;INPUT" "" ) set(_el "${_ARGS_INPUT}") if(_el) string_escape(OUTPUT _el INPUT "${_el}") set(${_ARGS_OUTPUT} "${${_ARGS_OUTPUT}}${_el}" PARENT_SCOPE) endif() endfunction() function(string_append_target_includes) cmake_parse_arguments( PARSE_ARGV 0 _ARGS "LINKED" "OUTPUT;PREFIX;TARGET" "" ) set(_out "") if(TARGET ${_ARGS_TARGET}) get_target_property(target_type ${_ARGS_TARGET} TYPE) if( (${target_type} STREQUAL "MODULE_LIBRARY") OR (${target_type} STREQUAL "STATIC_LIBRARY") OR (${target_type} STREQUAL "SHARED_LIBRARY") OR (${target_type} STREQUAL "EXECUTABLE") ) set(prop "$") set(test "$<$:${_ARGS_PREFIX}$>") set(_out "${_out}${test} ") set(prop "$") set(test "$<$:${_ARGS_PREFIX}$>") set(_out "${_out}${test} ") set(prop "$") set(test "$<$:${_ARGS_PREFIX}$>") set(_out "${_out}${test} ") if(NOT _ARGS_LINKED) # Scan linked libraries as well. get_target_property(_els ${_target} LINK_LIBRARIES) foreach(_lib ${_els}) #string_append_target_includes(LINKED TARGET "${_lib}" OUTPUT "_out" PREFIX "${_ARGS_PREFIX}") endforeach() endif() elseif((${target_type} STREQUAL "INTERFACE_LIBRARY")) set(prop "$") set(test "$<$:${_ARGS_PREFIX}$>") set(_out "${_out}${test} ") else() message("clang: Unsupported Target type '${target_type}', please open an issue for this.") endif() endif() set(${_ARGS_OUTPUT} "${${_ARGS_OUTPUT}}${_out}" PARENT_SCOPE) endfunction() function(generate_compile_commands_json) cmake_parse_arguments( PARSE_ARGV 0 _ARGS "" "REGEX" "TARGETS" ) set(COMPILER_TAG "") if((CMAKE_C_COMPILER_ID STREQUAL "MSVC") AND (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")) set(COMPILER_TAG "MSVC") set(COMPILER_INCLUDE_PREFIX "/I") set(COMPILER_DEFINE_PREFIX "/D") elseif((CMAKE_C_COMPILER_ID STREQUAL "GNU") AND (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")) set(COMPILER_TAG "GNU") set(COMPILER_INCLUDE_PREFIX "-I") set(COMPILER_DEFINE_PREFIX "-D") elseif((CMAKE_C_COMPILER_ID STREQUAL "Clang") AND (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")) set(COMPILER_TAG "CLANG") # Compatible with GNU set(COMPILER_INCLUDE_PREFIX "-I") set(COMPILER_DEFINE_PREFIX "-D") else() message("clang-tidy: C ID '${CMAKE_C_COMPILER_ID}'") message("clang-tidy: C Version '${CMAKE_C_COMPILER_VERSION}'") message("clang-tidy: C++ ID '${CMAKE_CXX_COMPILER_ID}'") message("clang-tidy: C++ Version '${CMAKE_CXX_COMPILER_VERSION}'") message(FATAL_ERROR "clang-tidy: Current Compiler is not yet supported, please open an issue for it.") endif() # Default Filter if(NOT _ARGS_REGEX) set(_ARGS_REGEX "\.(h|hpp|c|cpp)$") endif() foreach(_target ${_ARGS_TARGETS}) set(COMPILE_COMMAND_JSON "") # Source Directory get_target_property(target_source_dir_rel ${_target} SOURCE_DIR) get_filename_component(target_source_dir ${target_source_dir_rel} ABSOLUTE) unset(target_source_dir_rel) # Binary Directory get_target_property(target_binary_dir_rel ${_target} BINARY_DIR) get_filename_component(target_binary_dir ${target_binary_dir_rel} ABSOLUTE) unset(target_binary_dir_rel) # Sources get_target_property(_els ${_target} SOURCES) set(target_sources "") foreach(_el ${_els}) get_filename_component(_el "${_el}" ABSOLUTE) list(APPEND target_sources "${_el}") endforeach() list(FILTER target_sources INCLUDE REGEX "${_ARGS_REGEX}") # Combine Compiler String set(COMPILER_OPTIONS "") ## Compiler Options get_target_property(_els ${_target} COMPILE_OPTIONS) foreach(_el ${_els}) string_append_escaped(OUTPUT COMPILER_OPTIONS INPUT "${_el} ") endforeach() string(APPEND COMPILER_OPTIONS "${CMAKE_CXX_FLAGS} ${CMAKE_C_FLAGS} ") ## C++ Standard get_target_property(_el ${_target} CXX_STANDARD) if(COMPILER_TAG STREQUAL "MSVC") if(${MSVC_VERSION} LESS "1920") message(FATAL_ERROR "clang-tidy: Current Compiler is not yet supported, please open an issue for it.") else() if((_el EQUAL 98) OR (_el EQUAL 11)) # Nothing to do, this is the default. elseif(_el EQUAL 14) string(APPEND COMPILER_OPTIONS "/std:c++14 ") elseif(_el EQUAL 17) string(APPEND COMPILER_OPTIONS "/std:c++17 ") elseif(_el EQUAL 20) string(APPEND COMPILER_OPTIONS "/std:c++latest ") endif() endif() elseif((COMPILER_TAG STREQUAL "CLANG") OR (COMPILER_TAG STREQUAL "GNU")) if(_el EQUAL 98) string(APPEND COMPILER_OPTIONS "-std=c++98 ") elseif(_el EQUAL 11) string(APPEND COMPILER_OPTIONS "-std=c++11 ") elseif(_el EQUAL 14) string(APPEND COMPILER_OPTIONS "-std=c++14 ") elseif(_el EQUAL 17) string(APPEND COMPILER_OPTIONS "-std=c++17 ") elseif(_el EQUAL 20) if((COMPILER_TAG STREQUAL "CLANG") OR (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 9)) string(APPEND COMPILER_OPTIONS "-std=c++20 ") else() string(APPEND COMPILER_OPTIONS "-std=c++2a ") endif() endif() endif() ## Definitions get_target_property(_els ${_target} COMPILE_DEFINITIONS) foreach(_el ${_els}) string_append_escaped(OUTPUT COMPILER_OPTIONS INPUT "${COMPILER_DEFINE_PREFIX}${_el} ") endforeach() ## Includes string_append_target_includes(TARGET ${_target} PREFIX ${COMPILER_INCLUDE_PREFIX} OUTPUT COMPILER_OPTIONS) # Create Compilation Database set(target_compile_db "${target_binary_dir}/compile_commands.json") string(APPEND COMPILE_COMMAND_JSON "[\n") foreach(_el ${target_sources}) file(TO_NATIVE_PATH "${_el}" _el) string(REPLACE "\\" "\\\\" _el "${_el}") string(REPLACE "\"" "\\\"" _el "${_el}") string(APPEND COMPILE_COMMAND_JSON "\t{\n") string(APPEND COMPILE_COMMAND_JSON "\t\t\"directory\": \"${target_binary_dir}\",\n") string(APPEND COMPILE_COMMAND_JSON "\t\t\"file\": \"${_el}\",\n") if(MSVC) string(APPEND COMPILE_COMMAND_JSON "\t\t\"command\": \"cl ") else() string(APPEND COMPILE_COMMAND_JSON "\t\t\"command\": \"cc ") endif() string(APPEND COMPILE_COMMAND_JSON "${COMPILER_OPTIONS}") if(MSVC) string(APPEND COMPILE_COMMAND_JSON "${_el}") else() string(APPEND COMPILE_COMMAND_JSON " -c ${_el}") endif() string(APPEND COMPILE_COMMAND_JSON "\",\n\t},\n") endforeach() string(APPEND COMPILE_COMMAND_JSON "]") file(GENERATE OUTPUT "$/$/compile_commands.json" CONTENT "${COMPILE_COMMAND_JSON}") endforeach() endfunction() function(clang_format) cmake_parse_arguments( PARSE_ARGV 0 _ARGS "DEPENDENCY;GLOBAL" "REGEX;VERSION" "TARGETS" ) find_program(CLANG_FORMAT_BIN "clang-format" DOC "Path (or name) of the clang-format binary" HINTS "${CLANG_PATH}" PATHS /bin /sbin /usr/bin /usr/local/bin PATH_SUFFIXES bin bin64 bin32 ) if(NOT CLANG_FORMAT_BIN) message(WARNING "Clang: Could not find clang-format at path '${CLANG_FORMAT_BIN}', disabling clang-format...") return() endif() # Validate Version if (_ARGS_VERSION) set(_VERSION_RESULT "") set(_VERSION_OUTPUT "") execute_process( COMMAND "${CLANG_FORMAT_BIN}" --version WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" RESULT_VARIABLE _VERSION_RESULT OUTPUT_VARIABLE _VERSION_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_STRIP_TRAILING_WHITESPACE ERROR_QUIET ) if(NOT _VERSION_RESULT EQUAL 0) message(WARNING "Clang: Could not discover version, disabling clang-format...") return() endif() string(REGEX MATCH "([0-9]+\.[0-9]+\.[0-9]+)" _VERSION_MATCH ${_VERSION_OUTPUT}) if(NOT ${_VERSION_MATCH} VERSION_GREATER_EQUAL ${_ARGS_VERSION}) message(WARNING "Clang: Old version discovered, disabling clang-format...") return() endif() endif() # Default Filter if(NOT _ARGS_REGEX) set(_ARGS_REGEX "\.(h|hpp|c|cpp)$") endif() # Go through each target foreach(_target ${_ARGS_TARGETS}) get_target_property(target_sources_rel ${_target} SOURCES) set(target_sources "") foreach(_el ${target_sources_rel}) get_filename_component(_el "${_el}" ABSOLUTE) file(TO_NATIVE_PATH "${_el}" _el) list(APPEND target_sources "${_el}") endforeach() list(FILTER target_sources INCLUDE REGEX "${_ARGS_REGEX}") unset(target_sources_rel) get_target_property(target_source_dir_rel ${_target} SOURCE_DIR) get_filename_component(target_source_dir ${target_source_dir_rel} ABSOLUTE) unset(target_source_dir_rel) add_custom_target(${_target}_CLANG-FORMAT COMMAND "${CLANG_FORMAT_BIN}" -style=file -i ${target_sources} COMMENT "clang-format: Formatting ${_target}..." WORKING_DIRECTORY "${target_source_dir}" ) if(_ARGS_DEPENDENCY) add_dependencies(${_target} ${_target}_CLANG-FORMAT) endif() if(_ARGS_GLOBAL) if(TARGET CLANG-FORMAT) add_dependencies(CLANG-FORMAT ${_target}_CLANG-FORMAT) else() add_custom_target(CLANG-FORMAT DEPENDS ${_target}_CLANG-FORMAT COMMENT "clang-format: Formatting..." ) endif() endif() endforeach() endfunction() function(clang_tidy) cmake_parse_arguments( PARSE_ARGV 0 _ARGS "DEPENDENCY;GLOBAL" "REGEX;VERSION" "TARGETS" ) find_program(CLANG_TIDY_BIN "clang-tidy" DOC "Path (or name) of the clang-tidy binary" HINTS "${CLANG_PATH}" PATHS /bin /sbin /usr/bin /usr/local/bin PATH_SUFFIXES bin bin64 bin32 ) if(NOT CLANG_TIDY_BIN) message(WARNING "Clang: Could not find clang-tidy at path '${CLANG_TIDY_BIN}', disabling clang-tidy...") return() endif() # Validate Version if (_ARGS_VERSION) set(_VERSION_RESULT "") set(_VERSION_OUTPUT "") execute_process( COMMAND "${CLANG_TIDY_BIN}" --version WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" RESULT_VARIABLE _VERSION_RESULT OUTPUT_VARIABLE _VERSION_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_STRIP_TRAILING_WHITESPACE ERROR_QUIET ) if(NOT _VERSION_RESULT EQUAL 0) message(WARNING "Clang: Could not discover version, disabling clang-tidy...") return() endif() string(REGEX MATCH "([0-9]+\.[0-9]+\.[0-9]+)" _VERSION_MATCH ${_VERSION_OUTPUT}) if(NOT ${_VERSION_MATCH} VERSION_GREATER_EQUAL ${_ARGS_VERSION}) message(WARNING "Clang: Old version discovered, disabling clang-tidy...") return() endif() endif() # Default Filter if(NOT _ARGS_REGEX) set(_ARGS_REGEX "\.(h|hpp|c|cpp)$") endif() # Go through each target foreach(_target ${_ARGS_TARGETS}) # Source Directory get_target_property(_els ${_target} SOURCE_DIR) get_filename_component(target_source_dir ${_els} ABSOLUTE) file(TO_NATIVE_PATH "${target_source_dir}" target_source_dir_nat) unset(_els) # Binary Directory get_target_property(_els ${_target} BINARY_DIR) get_filename_component(target_binary_dir ${_els} ABSOLUTE) file(TO_NATIVE_PATH "${target_binary_dir}" target_binary_dir_nat) unset(_els) # Sources get_target_property(_els ${_target} SOURCES) set(target_sources "") foreach(_el ${_els}) get_filename_component(_el ${_el} ABSOLUTE) file(TO_NATIVE_PATH "${_el}" _el) list(APPEND target_sources "${_el}") endforeach() list(FILTER target_sources INCLUDE REGEX "${_ARGS_REGEX}") unset(_els) add_custom_target(${_target}_CLANG-TIDY COMMENT "clang-tiy: Tidying ${_target}..." WORKING_DIRECTORY "${target_binary_dir}" VERBATIM ) foreach(_el ${target_sources}) add_custom_command( TARGET ${_target}_CLANG-TIDY POST_BUILD COMMAND "${CLANG_TIDY_BIN}" ARGS --quiet -p="$/$" "${_el}" WORKING_DIRECTORY "${target_binary_dir}" COMMAND_EXPAND_LISTS ) endforeach() if(_ARGS_DEPENDENCY) add_dependencies(${_target} ${_target}_CLANG-TIDY) endif() if(_ARGS_GLOBAL) if(TARGET CLANG-TIDY) add_dependencies(CLANG-TIDY ${_target}_CLANG-FORTIDYMAT) else() add_custom_target(CLANG-TIDY DEPENDS ${_target}_CLANG-TIDY COMMENT "clang-tiy: Tidying..." ) endif() endif() endforeach() endfunction()