Configure clang-tidy when using an arm-none-eabi-gcc/g++ toolchain

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.

You were right with the implicit include directories on the command line. For some headers, clang uses its own but that directory must come first! To pass that via clang-tidy, I explicitly had to mention it before the other system include directories. I never had to do the macro stuff.

Sorry for no better details, I do not have access to that project anymore.

Some additional information: We are typically only installing clang-tidy and clang-format, but even if I provide the full LLVM suite (e.g. use the path to the binary in the full installation), it yields the same behavior.

Some random thoughts, in case they help:

  • Is your clang-tidy executable from the same version of LLVM as your clang installation?
  • Is your clang-tidy executable installed in the same directory as your clang executable?

Also keep in mind that your code is being built with gcc, but clang-tidy is a clang-based tool and will parse the code effectively as though it was clang (that’s my understanding of how it works internally at least). If your code doesn’t compile with clang, I’d expect you’re going to hit problems with trying to run clang-tidy on it. Something you may want to consider trying is seeing if you can get the code to compile with clang, even if that is to a set of binaries that can’t be run, or possibly even won’t link. The key is to at least come up with a set of clang flags that allow it to compile to object files. If you can do that, then run clang-tidy on that build directory, I suspect you’ll have more success. Note that this approach would not actually require to you compile with clang, only to configure a build directory (i.e. run CMake, but not the build tool). If you want to use CMake’s co-compile functionality to run clang-tidy, then yes a compile (and probably link) will be needed, but you could avoid that by running the run-clang-tidy tool which acts on the compile_commands.json file and runs clang-tidy as a batch.

We are currently using the “co-compile” functionality, so that developers immediately get feedback while building whatever they are working on.

As mentioned in my other comment: typically we only have clang-tidy and clang-format available, not the entire LLVM suite.
However, my test with the entire LLVM suite installed (and using the executable from that location), yielded the same results.

So far I was always under the impression that Clang was made to be compatible with GCC, but it’s been quite a PITA to proceed with this.
Perhaps the best course of action is to indeed also have a clang build, although that will add quite some work in configuration (as we currently already have two different GCC compilers to take into account).