How to control the location of the c++20 Binary Module Interface (BMI) output directory?

With this CMakeLists.txt, I can build and check the simple example of a module interface.

But clang-tidy does not find the BMI file?

cmake_minimum_required(VERSION 3.26)


project(clang-tidy-issue LANGUAGES CXX)



# First find clang-tidy, this also allows users to provide hints
find_program(CLANG_TIDY NAMES clang-tidy REQUIRED)

# clang++ -std=c++20 -x c++-module foo.cxx --precompile -o build/foo.pcm
# clang++ -std=c++20 main.cxx -fprebuilt-module-path=build build/foo.pcm -o build/Hello
# see
# and

set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API 2182bf5c-ef0d-489a-91da-49dbc3090d2a)
set(CMake_TEST_CXXModules_UUID "a246741c-d067-4019-a8fb-3d16b0c9d1d3")
# Workaround for C++Modules: Missing module map flags in exported compile commands
# see

                "<CMAKE_CXX_COMPILER> <DEFINES> <INCLUDES> <FLAGS> -E -x c++ <SOURCE>"
                " -MT <DYNDEP_FILE> -MD -MF <DEP_FILE>"
                " -fmodules-ts -fdep-file=<DYNDEP_FILE> -fdep-output=<OBJECT> -fdep-format=trtbd"
                " -o <PREPROCESSED_SOURCE>"
  set(CMAKE_EXPERIMENTAL_CXX_MODULE_MAP_FLAG "-fmodules-ts -fmodule-mapper=<MODULE_MAP_FILE> -fdep-format=trtbd -x c++")

  set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY};-checks=*,-llvmlibc-*,-fuchsia-*,-cppcoreguidelines-init-variables")

  # FIXME: This does not work, clang-tidy is a pre, not a post compile step! CK
  #XXX   "-extra-arg=-fprebuilt-module-path=${CMAKE_CURRENT_BINARY_DIR}"
  #XXX )

file(WRITE foo.cppm
// Global module fragment where #includes can happen
#include <iostream>

// first thing after the Global module fragment must be a module command
// XXX import std.core;

export module foo;

export class foo {
  foo() = default;
  ~foo() = default;
  void helloworld();

void foo::helloworld() { std::cout << "hello world\n"; }

file(WRITE main.cxx
import foo;

auto main() -> int
  foo myFoo;
  return 0;

  # TODO(CK): How to control the location of the c++20 Binary Module Interface (BMI) output directory?
  #XXX target_compile_options(foo
  #XXX    PRIVATE -fmodule-output=${BMI} -x c++-module
  #XXX    PUBLIC -fmodule-file=foo=${BMI}
  #XXX )

add_executable(hello main.cxx)
target_link_libraries(hello foo)

add_test(NAME hello COMMAND hello)

  FILE_SET cxx_modules DESTINATION include

This is the Result on OSX with clang v16.0.1:

bash-3.2$ run-clang-tidy
Enabled checks:
 # ...

/usr/local/opt/llvm/bin/clang-tidy -p=/Users/clausklein/Workspace/cpp/cxx20/test/build /Users/clausklein/Workspace/cpp/cxx20/test/main.cxx
77759 warnings generated.
Suppressed 77759 warnings (77759 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
/usr/local/opt/llvm/bin/clang-tidy -p=/Users/clausklein/Workspace/cpp/cxx20/test/build /Users/clausklein/Workspace/cpp/cxx20/test/foo.cppm
warning: '-x c++-module' after last input file has no effect [clang-diagnostic-unused-command-line-argument]
/Users/clausklein/Workspace/cpp/cxx20/test/foo.cppm:17:11: warning: method 'helloworld' can be made static [readability-convert-member-functions-to-static]
void foo::helloworld() { std::cout << "hello world\n"; }
85169 warnings generated.
Suppressed 85167 warnings (85167 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
bash-3.2$ cat compile_commands.json 
  "directory": "/Users/clausklein/Workspace/cpp/cxx20/test/build",
  "command": "/usr/local/opt/llvm/bin/clang++ -std=c++20 -isysroot /Applications/ -o CMakeFiles/foo.dir/foo.cppm.o -c /Users/clausklein/Workspace/cpp/cxx20/test/foo.cppm @CMakeFiles/foo.dir/foo.cppm.o.modmap",
  "file": "/Users/clausklein/Workspace/cpp/cxx20/test/foo.cppm",
  "output": "CMakeFiles/foo.dir/foo.cppm.o"
  "directory": "/Users/clausklein/Workspace/cpp/cxx20/test/build",
  "command": "/usr/local/opt/llvm/bin/clang++ -std=c++20 -isysroot /Applications/ -o CMakeFiles/hello.dir/main.cxx.o -c /Users/clausklein/Workspace/cpp/cxx20/test/main.cxx @CMakeFiles/hello.dir/main.cxx.o.modmap",
  "file": "/Users/clausklein/Workspace/cpp/cxx20/test/main.cxx",
  "output": "CMakeFiles/hello.dir/main.cxx.o"

And the most important question is where to install the BMI file?

bash-3.2$ cpack
CPack: Create package using STGZ
CPack: Install projects
CPack: - Install project: clang-tidy-issue []
CPack: Create package
CPack: - package: /Users/clausklein/Workspace/cpp/cxx20/test/build/ generated.
CPack: Create package using TGZ
CPack: Install projects
CPack: - Install project: clang-tidy-issue []
CPack: Create package
CPack: - package: /Users/clausklein/Workspace/cpp/cxx20/test/build/clang-tidy-issue-0.1.1-Darwin.tar.gz generated.
bash-3.2$ tar tzvf clang-tidy-issue-0.1.1-Darwin.tar.gz
drwxr-xr-x  0 clausklein staff       0 Apr 24 17:56 clang-tidy-issue-0.1.1-Darwin/include/
-rw-r--r--  0 clausklein staff     348 Apr 24 17:38 clang-tidy-issue-0.1.1-Darwin/include/foo.cppm
drwxr-xr-x  0 clausklein staff       0 Apr 24 17:56 clang-tidy-issue-0.1.1-Darwin/lib/
-rw-r--r--  0 clausklein staff   17272 Apr 24 17:38 clang-tidy-issue-0.1.1-Darwin/lib/libfoo.a
drwxr-xr-x  0 clausklein staff       0 Apr 24 17:56 clang-tidy-issue-0.1.1-Darwin/share/
drwxr-xr-x  0 clausklein staff       0 Apr 24 17:56 clang-tidy-issue-0.1.1-Darwin/share/cxx-modules/
-rw-r--r--  0 clausklein staff 13821400 Apr 24 17:38 clang-tidy-issue-0.1.1-Darwin/share/cxx-modules/foo.pcm

This has not been worked on; clang-tidy may need to learn how to read the modmap file clang will use (though it should exist by the time we’re compiling…

The fact that it doesn’t work with compile_commands.json when the @modmap arguments are there tells me that clang-tidy is ignoring something…

Certainly not share as they are arch-dependent. I suspect where they go will be a Linux distro discussion in the FHS umbrella, Apple will just decree for itself, and Microsoft will do Windows things. ISO C++'s SG15 is also likely to discuss such things.

@ClausKlein, I have been experiencing this issue too, which gives clang-diagnostic-error with C++20 modules and clang-tidy.

FAILED: CMakeFiles/caldera_exe.dir/source/main.cpp.obj 
"C:\Program Files\CMake\bin\cmake.exe" -E __run_co_compile --tidy="clang-tidy;--header-filter=^D:/Libraries/Documents/Repositories/caldera;-extra-arg=-fprebuilt-module-path=D:/Libraries/Documents/Repositories/caldera/build/CMakeFiles/caldera_lib.dir;--extra-arg-before=--driver-mode=cl" --source=D:\Libraries\Documents\Repositories\caldera\source\main.cpp -- C:\PROGRA~1\MIB055~1\2022\ENTERP~1\VC\Tools\MSVC\1436~1.325\bin\Hostx64\x64\cl.exe  /nologo /TP  -ID:\Libraries\Documents\Repositories\caldera\source -external:ID:\Libraries\Documents\Repositories\caldera\build\dev-win64\vcpkg_installed\x64-windows-static-md\include -external:W0 /sdl /guard:cf /utf-8 /diagnostics:caret /w14165 /w44242 /w44254 /w44263 /w34265 /w34287 /w44296 /w44365 /w44388 /w44464 /w14545 /w14546 /w14547 /w14549 /w14555 /w34619 /w34640 /w24826 /w14905 /w14906 /w14928 /w45038 /W4 /permissive- /volatile:iso /Zc:inline /Zc:preprocessor /Zc:enumTypes /Zc:lambda /Zc:__cplusplus /Zc:externConstexpr /Zc:throwingNew /EHsc /O2 /Ob1 /DNDEBUG -std:c++20 -MD -Zi /showIncludes @CMakeFiles\caldera_exe.dir\source\main.cpp.obj.modmap /FoCMakeFiles\caldera_exe.dir\source\main.cpp.obj /FdCMakeFiles\caldera_exe.dir\ /FS -c D:\Libraries\Documents\Repositories\caldera\source\main.cpp
warning: unknown argument ignored in clang-cl: '-fprebuilt-module-path=D:/Libraries/Documents/Repositories/caldera/build/CMakeFiles/caldera_lib.dir' [clang-diagnostic-unknown-argument]
D:\Libraries\Documents\Repositories\caldera\source\main.cpp:1:8: error: module 'caldera' not found [clang-diagnostic-error]
import caldera;
132051 warnings and 1 error generated.
Error while processing D:\Libraries\Documents\Repositories\caldera\source\main.cpp.
Suppressed 132050 warnings (132050 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
Found compiler error(s).
ninja: build stopped: subcommand failed.

I am running Windows 10, running Visual Studio 17.6.2 (cl.exe 19.36), CMake 3.26.4, Ninja 1.11.0, and clang-tidy 16.0.0 (so effectively everything is the newest possible).

When I tried preparing a minimal working example, all was fine, but in my own project, I didn’t realise I had previously set CMAKE_CXX_CLANG_TIDY, and it admittedly took me quite a bit of time to notice the problem.

Like you, I wonder if it has to do with the way CMake is calling clang-tidy, or if it’s a bug in clang-tidy itself.

CMake is generating modules for MSVC to use, If clang-tidy cannot consume the .ifc files (regardless of the extension), it will always fail to import things.

Can we track this issue upstream?

Sure. I don’t 100% know what to do with it at the moment right now. Getting modules working at all is higher priority than figuring out heterogeneous tooling. If you want to use clang-tidy with modules today, I’d recommend using a matching clang toolchain at least.

For the record, I have changed my minimum not-working example linked in the LLVM Discourse thread to use a fully clang-only toolchain.

I changed line 14 of CMakePresets.json to clang.exe, but I still receive the same error. test.pcm is built in ${CMAKE_BINARY_DIR}/CMakeFiles/test_lib.dir/test.pcm. A full, verbose build log is below:

====================[ Build | test | Ninja - Ninja ]============================
cmake.exe --build --target test --preset Ninja
[1/8] cmd.exe /C ""C:/Program Files/LLVM/bin/clang-scan-deps.exe" -format=p1689 -- C:\PROGRA~1\LLVM\bin\clang.exe   -O0 -std=gnu++20 -D_DEBUG -D_DLL -D_MT -Xclang --dependent-lib=msvcrtd -g -Xclang -gcodeview -x c++ D:/Libraries/Downloads/test/main.cpp -c -o CMakeFiles\test.dir\main.cpp.obj -MT CMakeFiles\test.dir\main.cpp.obj.ddi -MD -MF CMakeFiles\test.dir\main.cpp.obj.ddi.d > CMakeFiles\test.dir\main.cpp.obj.ddi"
[2/8] cmd.exe /C ""C:/Program Files/LLVM/bin/clang-scan-deps.exe" -format=p1689 -- C:\PROGRA~1\LLVM\bin\clang.exe   -O0 -std=gnu++20 -D_DEBUG -D_DLL -D_MT -Xclang --dependent-lib=msvcrtd -g -Xclang -gcodeview -x c++ D:/Libraries/Downloads/test/test.cppm -c -o CMakeFiles\test_lib.dir\test.cppm.obj -MT CMakeFiles\test_lib.dir\test.cppm.obj.ddi -MD -MF CMakeFiles\test_lib.dir\test.cppm.obj.ddi.d > CMakeFiles\test_lib.dir\test.cppm.obj.ddi"
[3/8] "C:\Program Files\CMake\bin\cmake.exe" -E cmake_ninja_dyndep --tdi=CMakeFiles\test_lib.dir\CXXDependInfo.json --lang=CXX --modmapfmt=clang --dd=CMakeFiles/test_lib.dir/CXX.dd @CMakeFiles/test_lib.dir/CXX.dd.rsp
[4/8] "C:\Program Files\CMake\bin\cmake.exe" -E __run_co_compile --tidy=clang-tidy;-checks=-*,readability-*;--extra-arg=-Xclang=-fprebuilt-module-path=D:/Libraries/Downloads/test/build/CMakeFiles/test_lib.dir/;-p=D:/Libraries/Downloads/test/build;--extra-arg-before=--driver-mode=g++ --source=D:/Libraries/Downloads/test/test.cppm -- C:\PROGRA~1\LLVM\bin\clang.exe   -O0 -std=gnu++20 -D_DEBUG -D_DLL -D_MT -Xclang --dependent-lib=msvcrtd -g -Xclang -gcodeview -MD -MT CMakeFiles/test_lib.dir/test.cppm.obj -MF CMakeFiles\test_lib.dir\test.cppm.obj.d @CMakeFiles\test_lib.dir\test.cppm.obj.modmap -o CMakeFiles/test_lib.dir/test.cppm.obj -c D:/Libraries/Downloads/test/test.cppm
D:\Libraries\Downloads\test\test.cppm:5:30: warning: 5 is a magic number; consider replacing it with a named constant [readability-magic-numbers]
auto f() -> int { return 1 + 5; }
[5/8] cmd.exe /C "cd . && "C:\Program Files\CMake\bin\cmake.exe" -E rm -f test_lib.lib && C:\PROGRA~1\LLVM\bin\llvm-ar.exe qc test_lib.lib  CMakeFiles/test_lib.dir/test.cppm.obj && C:\PROGRA~1\LLVM\bin\LLVM-R~1.EXE test_lib.lib && cd ."
[6/8] "C:\Program Files\CMake\bin\cmake.exe" -E cmake_ninja_dyndep --tdi=CMakeFiles\test.dir\CXXDependInfo.json --lang=CXX --modmapfmt=clang --dd=CMakeFiles/test.dir/CXX.dd @CMakeFiles/test.dir/CXX.dd.rsp
[7/8] "C:\Program Files\CMake\bin\cmake.exe" -E __run_co_compile --tidy=clang-tidy;-checks=-*,readability-*;--extra-arg=-Xclang=-fprebuilt-module-path=D:/Libraries/Downloads/test/build/CMakeFiles/test_lib.dir/;-p=D:/Libraries/Downloads/test/build;--extra-arg-before=--driver-mode=g++ --source=D:/Libraries/Downloads/test/main.cpp -- C:\PROGRA~1\LLVM\bin\clang.exe   -O0 -std=gnu++20 -D_DEBUG -D_DLL -D_MT -Xclang --dependent-lib=msvcrtd -g -Xclang -gcodeview -MD -MT CMakeFiles/test.dir/main.cpp.obj -MF CMakeFiles\test.dir\main.cpp.obj.d @CMakeFiles\test.dir\main.cpp.obj.modmap -o CMakeFiles/test.dir/main.cpp.obj -c D:/Libraries/Downloads/test/main.cpp
FAILED: CMakeFiles/test.dir/main.cpp.obj 
"C:\Program Files\CMake\bin\cmake.exe" -E __run_co_compile --tidy=clang-tidy;-checks=-*,readability-*;--extra-arg=-Xclang=-fprebuilt-module-path=D:/Libraries/Downloads/test/build/CMakeFiles/test_lib.dir/;-p=D:/Libraries/Downloads/test/build;--extra-arg-before=--driver-mode=g++ --source=D:/Libraries/Downloads/test/main.cpp -- C:\PROGRA~1\LLVM\bin\clang.exe   -O0 -std=gnu++20 -D_DEBUG -D_DLL -D_MT -Xclang --dependent-lib=msvcrtd -g -Xclang -gcodeview -MD -MT CMakeFiles/test.dir/main.cpp.obj -MF CMakeFiles\test.dir\main.cpp.obj.d @CMakeFiles\test.dir\main.cpp.obj.modmap -o CMakeFiles/test.dir/main.cpp.obj -c D:/Libraries/Downloads/test/main.cpp
D:/Libraries/Downloads/test/main.cpp:1:8: fatal error: module 'test' not found
import test;
1 error generated.
ninja: build stopped: subcommand failed.

Is clang-tidy not reading this file properly?

I think we should create a clean example because when I tried it, clang-tidy failed regardless of cmake configuration, most probably because it does not read module files.

I think so. Like I mentioned in the LLVM thread, running clang-tidy standalone with clang-only tools (i.e. manually pre-compiling) also failed. Which is why I posted the issue there, too.

> clang-tidy.exe -checks="-*,readability-*" --extra-arg=-Xclang=-fprebuilt-module-path=D:/Libraries/Desktop/test/build/CMakeFiles/test.dir/ -p=D:/Libraries/Desktop/test/build --extra-arg-before=--driver-mode=cl .\main.cpp
1 error generated.
Error while processing D:\Libraries\Desktop\test\.\main.cpp.
D:\Libraries\Desktop\test\main.cpp:1:8: error: module 'test' not found [clang-diagnostic-error]
import test;
Found compiler error(s).

> clang-tidy.exe -checks="-*,readability-*" --extra-arg=-Xclang=-fprebuilt-module-path=D:/Libraries/Desktop/test/build/CMakeFiles/test_lib.dir/ -p=D:/Libraries/Desktop/test/build --extra-arg-before=--driver-mode=cl .\main.cpp
1 error generated.
Error while processing D:\Libraries\Desktop\test\.\main.cpp.
D:\Libraries\Desktop\test\main.cpp:1:8: error: module 'test' not found [clang-diagnostic-error]
import test;
Found compiler error(s).

Hi, I am here after a lot of scouring the internet due to my own clang tool failing when CMake updated to 3.28. The root of the issue is related to the @foo.modmap files as discussed here, but I think the reasons discussed so far miss the cause.

Nibble Stew: The road to hell is paved with good intentions and C++ modules explains (the author is not a fan, but please don’t focus on that here) what is going on during compilation with CMake now and what the @foo.modmap files are. I could not find any other docs that explained it.

The problem then is that CMake generates a @foo.modmap file during compilation for each .cc file that it builds. The file is not created as a result of configure, and it’s not left behind after compilation, only the .obj file is left behind.

So when I run a clang tool against the command line specified in compile_commands.json, it fails due to the @foo.modmap file not existing. AFAICT the command that CMake writes into the compile_commands.json is incorrect, since it’s pointing to a file (the modmap file) which does not actually exist (outside of cmake doing compilation which is not what compile_commands.json is for).

This renders the compile_commands.json broken, and requires tools to strip out the @foo.modmap argument in order to run the clang tool against the command line arguments in the compile_commands.json file. This will work until code actually uses modules at which point it will fall over again, as the root issue is unresolved: that the command line in the compile_commands.json file as generated by CMake is not able to independently reproduce the compilation step.

FTR, I am using CMake 3.28.1, and this issue occurs on Windows and Linux, targeting Clang and MSVC. If CMake is wedded to the idea of using a modmap file during compilation, it needs to write them all to disk at the same time that it generates the compile_commands.json.

My debugging FWIW: Subdoc fails with missing modmap files · Issue #437 · chromium/subspace · GitHub

Please read the Reddit comments on that blog post. I do need to write up docs on how CMake implements modules. It’s on my list.

Then it will need to wait until the build is complete. The .modmap files cannot be made at generate time without CMake having to regenerate the build on every .modmap-using source file. At that point, the file is the same as made during configure and the .modmap files are present for use by any compile_commands.json-using tool. Note that there is also no safe place in the graph to place a command to generate compile_commands.json because:

  • targets may be EXCLUDE_ALL and not build via the default target and still reference non-existent files
  • if it needs updated (e.g., after a re-configure, it is out-of-date until regenerated)

Note that this has always been an issue with build-generated sources or headers. There is a plan to solve this part of the problem in 2024 with something like a generated-sources target (name and details TBD) to generate all sources. A similar target to make .modmap files present would also be something that could be done at the same time.

If the projects do not actually use modules, they can set CMAKE_CXX_SCAN_FOR_MODULES to 0 to disable scanning (and therefore .modmap file usage).

Note that it will be the least of your worries as the BMIs that are made during the build will be compiler-specific and your tool will (likely) need to generate its own BMIs in order to perform the import statements in the analyzed. There are plans for generating enough information for tools to be able to generate their own BMIs in 2024 as well (future work can let CMake drive the tool to make these BMIs, but that is likely farther out).

Thanks, I hope that this can improve in 2024, glad to know it’s on your radar.

One note: I do not see any modmap files in my CMakefiles directories, even after I have done a compile. There’s no point at which the compile_commands.json command lines work for me. It seems that CMake generates them, does the compile, and then deletes them after the compilation is done.

.modmap files shouldn’t be deleted; I have tons of them in my testing tree. Can you provide a reproducer project to show the problem?

That sounds promising. Basically it would call all configure_file, custom_command, and cmake native like the cxx module files generation? That would be useful for the cmake pre-commit.

Does this also solve the original issue with clang-tidy + compile_commands.json?

configure_file is configure-time; this would basically make sure any sources with the GENERATED property are up-to-date. Or similar; there are a lot of test cases to write and decide support for.

For pre-modules code, yes.

This project: on Windows.

In cmd:

git clone --recurse-submodules test-sus
cd test-sus
"C:\Program Files\CMake\bin\cmake.exe" -B out -G Ninja -DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo -DSUBSPACE_BUILD_TESTS=ON
"C:\Program Files\CMake\bin\cmake.exe" --build out

No modmap files found in test-sus\out. For example test-sus\out\sus\CMakeFiles\subspace_unittests.dir\fn contains only two .obj files.

Thanks, reproduced on Windows. And on Linux.

Ok, so the build doesn’t use modules, so we don’t actually do any modmap stuff. The compile_commands.json logic reimplements some flag detection; this needs reworked to match.