We are developing embedded firmware for ARM Cortex-M, Xtensa and RISC-V microcontrollers.
All come with a GCC based toolchain, for instance arm-none-eabi-gcc (arm-none-eabi-g++).
We are using CMake 3.28.1 (or slightly older), with a recent version of ninja.
The toolchain is arm-none-eabi-gcc v10.3_2021.10 and clang-tidy v18.1.4 is being used.
We are trying to configure CMake so it also runs clang-tidy based on the compilation database (e.g. CMAKE_EXPORT_COMPILE_COMMANDS
is on and we also put -p ${CMAKE_BINARY_DIR}/compile_commands.json
in the CXX_CLANG_TIDY
target property.
This kinda works… at least it does run clang-tidy with the configuration file that’s in the root of our CMake project.
Unfortunately, as the normal compiler is arm-none-eabi-g++, it struggles to find certain “system” header files, like stddef.h
.
So, in my root CMakeLists.txt
I added set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES})
after the call to project
.
As this kinda resolved the issue, but I didn’t want to pollute my compile commands with these unnecessary arguments, I instead expanded the CXX_CLANG_TIDY
variable to include an --extra-args=-isystem...
for each entry in CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES
:
# Add implicit include directories to the clang-tidy arguments
set(_implicit_includes ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES} ${CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES})
list(REMOVE_DUPLICATES _implicit_includes)
foreach(include ${_implicit_includes})
list(APPEND _additional_clang_tidy_args "--extra-arg=-isystem${include}")
endforeach()
Again, this seems to improve the situation, but soon the next problem arose: clang-tidy is unaware of certain predefined macros that GCC has.
For instance, when processing std-int-gcc.h
, it complains that __WCHAR_MIN__
is not defined.
So I figured, how do I get all the predefined macros that GCC uses into clang-tidy?
Using execute_process
, I run the compiler during the configuration phase with the -dM flag to dump all the predefined macros.
Then I use some regex to extract the #define
and turn them into --extra-arg-=D...
for clang-tidy.
set(_compiler_dump_args ${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_DEBUG})
# It seems that the flags are now a string rather than a list, so we need to split them up again.
# Not sure if this always works, but for now assume that every option starts with a dash.
string(REPLACE " -" ";-" _compiler_dump_args "${_compiler_dump_args}")
# Append arguments to dump all predefined macros
list(APPEND _compiler_dump_args -dM -E -x c++ -)
set(_null_input /dev/null)
if(CMAKE_HOST_WIN32)
set(_null_input NUL)
endif()
execute_process(COMMAND ${CMAKE_CXX_COMPILER} ${_compiler_dump_args}
INPUT_FILE ${_null_input}
OUTPUT_VARIABLE predefined_macros_result
COMMAND_ERROR_IS_FATAL ANY
)
# Get all #define lines from the output and convert them to -D options
string(REGEX MATCHALL "#define [A-Za-z0-9_]+( +[^\n]+)" defines "${predefined_macros_result}")
foreach(define ${defines})
string(REGEX REPLACE "#define ([A-Za-z0-9_]+)" "-D\\1" define "${define}")
# If the define has a value, make sure an equals sign is used
if(define MATCHES "(-D[A-Za-z0-9_]+) +(.+)")
set(define "${CMAKE_MATCH_1}=${CMAKE_MATCH_2}")
endif()
list(APPEND _additional_clang_tidy_args "--extra-arg=${define}")
endforeach()
Again, this seems to bring me a step closer, but… you guessed it… the next problem arose.
First of all, I got a lot of redefining builtin macro [clang-diagnostic-builtin-macro-redefined]
warnings.
I reckon this is because knows about some of the predefined macros from GCC, but I’m still passing them explicitly now.
We can probably suppress that specific warning, so I’m not too worried about that.
Secondly, it starts complaing when processing some other system header files, but this time I believe it’s some built-in functions that it doesn’t know about.
Just a few of the errors it gives:
[build] C:/cc/Arm/10.3_2021.10/arm-none-eabi/include/c++/10.3.1\array:245:29: error: use of undeclared identifier 'is_same_v' [clang-diagnostic-error]
[build] 245 | -> array<enable_if_t<(is_same_v<_Tp, _Up> && ...), _Tp>,
[build] | ^
[build] C:/cc/Arm/10.3_2021.10/arm-none-eabi/include/c++/10.3.1\array:245:52: error: pack expansion does not contain any unexpanded parameter packs [clang-diagnostic-error]
[build] 245 | -> array<enable_if_t<(is_same_v<_Tp, _Up> && ...), _Tp>,
[build] | ~~~~~~~~~ ^
[build] C:/cc/Arm/10.3_2021.10/arm-none-eabi/include/c++/10.3.1\bits/basic_string.h:6021:17: error: cannot use parentheses when declaring variable with deduced class template specialization type [clang-diagnostic-error]
[build] 6021 | basic_string(basic_string_view<_CharT, _Traits>, const _Allocator& = _Allocator())
[build] | ^
[build] C:/cc/Arm/10.3_2021.10/arm-none-eabi/include/c++/10.3.1\bits/basic_string.h:6021:18: error: no variable template matches partial specialization [clang-diagnostic-error]
[build] 6021 | basic_string(basic_string_view<_CharT, _Traits>, const _Allocator& = _Allocator())
[build] | ^
[build] C:/cc/Arm/10.3_2021.10/arm-none-eabi/include/c++/10.3.1\bits/basic_string.h:6021:52: error: expected ')' [clang-diagnostic-error]
[build] 6021 | basic_string(basic_string_view<_CharT, _Traits>, const _Allocator& = _Allocator())
[build] | ^
[build] C:/cc/Arm/10.3_2021.10/arm-none-eabi/include/c++/10.3.1\bits/basic_string.h:6021:17: note: to match this '('
[build] 6021 | basic_string(basic_string_view<_CharT, _Traits>, const _Allocator& = _Allocator())
[build] | ^
[build] C:/cc/Arm/10.3_2021.10/arm-none-eabi/include/c++/10.3.1\bits/basic_string.h:6021:87: error: expected ';' at end of declaration [clang-diagnostic-error]
[build] 6021 | basic_string(basic_string_view<_CharT, _Traits>, const _Allocator& = _Allocator())
To be honest, I’m not sure if this is the way to proceed.
However, I’d love to hear from others whom have tried to do something similar (using clang-tidy on a codebase built with GCC).
Hopefully that can provide some insights to get to a working solution.