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)

set(CMAKE_EXPORT_COMPILE_COMMANDS YES)

project(clang-tidy-issue LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_EXTENSIONS NO)
set(CMAKE_CXX_STANDARD_REQUIRED YES)

set(CMAKE_CXX_SCAN_FOR_MODULES YES)

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

# https://clang.llvm.org/docs/StandardCPlusPlusModules.html#how-to-build-projects-using-modules
# 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 https://gitlab.kitware.com/cmake/cmake/-/blob/v3.26.3/Help/dev/experimental.rst?ref_type=tags#c-20-module-apis
# and https://gitlab.kitware.com/cmake/cmake/-/blob/v3.26.3/.gitlab/ci/cxx_modules_rules_gcc.cmake?ref_type=tags
#     https://gitlab.kitware.com/cmake/cmake/-/blob/v3.26.3/.gitlab/ci/cxx_modules_rules_clang.cmake?ref_type=tags

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 https://gitlab.kitware.com/cmake/cmake/-/issues/24618
set(CMAKE_CXX_COMPILE_OBJECT "${CMAKE_CXX_COMPILE_OBJECT} @<OBJECT>.modmap")

set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
  string(CONCAT CMAKE_EXPERIMENTAL_CXX_SCANDEP_SOURCE
                "<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_FORMAT "gcc")
  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")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
  set(CMAKE_EXPERIMENTAL_CXX_MODULE_MAP_FORMAT "clang")
  set(BMI ${CMAKE_CURRENT_BINARY_DIR}/foo.pcm)

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

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

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

export module foo;

export class foo {
public:
  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;
  myFoo.helloworld();
  return 0;
}
]=]
)

add_library(foo)
target_sources(
  foo
  PUBLIC FILE_SET
         cxx_modules
         TYPE
         CXX_MODULES
         FILES
         foo.cppm
)
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  # 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 )
endif()

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

enable_testing()
add_test(NAME hello COMMAND hello)

install(TARGETS foo ARCHIVE PUBLIC_HEADER
  FILE_SET cxx_modules DESTINATION include
  CXX_MODULES_BMI DESTINATION share/cxx-modules
)
include(cpack)

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/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk -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/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk -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"
}
]bash-3.2$ 
``

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/clang-tidy-issue-0.1.1-Darwin.sh 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
bash-3.2$ 

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).
>