Hi,
I’ve been experimenting with the current C++20 modules implementation lately in CMake 4.3 and ran into some issues when consuming modules transitively, across multiple intermediate (imported) library targets. It’s very well possible that I’m misunderstanding how this is supposed to work, but I’d appreciate any pointers in the right direction in this case.
If you just want a quick example of what I mean, the minimal repro cases in this repository should work out of the box (see repro/cmake-transitive-usage subdirectory).
Issue 1
Consider the following dependency structure:
LibraryA (installed, exports MyModule) ← LibraryB (imports MyModule) ← MyExe (imports MyModule)
The arrows represent publicly linking the CMake targets via target_link_libraries().
TL;DR: LibraryB works, but MyExe doesn’t and can’t find MyModule even though I think there should be a transitive usage requirement.
Detailed description
LibraryA has been installed with CMake, including the FILE_SET TYPE CXX_MODULES with primary module interface MyModule.cppm containing export module MyModule, which is imported by both LibraryB and MyExe. This works fine for LibraryB , which is able to locate the module interface and generate its BMI, but MyExe fails to build with fatal error: module ‘MyModule’ not found in both latest GCC and Clang.
This surprises me - I would expect that the module information is propagated transitively from the imported LibraryA to MyExe, and this is in fact the case if I have all three targets in the same buildtree without an installation step and imported target in-between. The installed export files of LibraryA (so all the ProjectAConfig and Targets and cxx-modules files) look reasonable to me and contain the necessary IMPORTED_CXX_MODULES_<CONFIG> entries for the module interface. Looking through the generated CMake files, I can find a reference to the synth target containing the module in LibraryB’s CXXDependInfo.json and the modmap file, but it’s mentioned in neither for MyExe.
Reduced CMake Example
### LibraryA CMakeLists.txt
add_library(LibraryA STATIC)
target_sources(LibraryA PRIVATE src/MyModule.cpp)
target_sources(LibraryA PUBLIC
FILE_SET CXX_MODULES
TYPE CXX_MODULES
FILES modules/MyModule.cppm
)
target_compile_features(LibraryA PUBLIC cxx_std_23)
include(GNUInstallDirs)
set(CMAKE_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/cmake/LibraryA")
install(TARGETS LibraryA
EXPORT LibraryATargets
FILE_SET CXX_MODULES DESTINATION ${CMAKE_INSTALL_DIR}/modules
)
# Create and install package config files here...
install(EXPORT LibraryATargets
DESTINATION ${CMAKE_INSTALL_DIR}
CXX_MODULES_DIRECTORY modules
)
### LibraryB CMakeLists.txt
find_package(LibraryA REQUIRED)
add_library(LibraryB SHARED src/Library.h src/Library.cpp)
target_compile_features(LibraryB PUBLIC cxx_std_23)
# Works: LibraryA exposes location of MyModule, which is discovered, built and linked successfully
target_link_libraries(LibraryB PUBLIC ProjectA)
add_executable(MyExe src/Main.cpp)
# Doesn't work: MyModule not found, even though it should be transitively "known" via LibraryB
target_link_libraries(MyExe PUBLIC LibraryB)
Is this a bug, am I doing something wrong or is it just not working the way I think it does?
Issue 2
I like to use intermediate object libraries for shared library projects to make their non-exported symbols testable - so basically, the object library compiles all the code once, and then the shared library and test executable just link it together with the tests on top. Common settings/flags/usage requirements can then be set via an Interface target, for example.
So this time our dependency structure is the following:
ObjLibA (exports MyModule) ← SharedLibA (installed) ← MyExe (imports MyModule)
TL;DR: This is more of a usability question: Can I make this work without installing ObjLibA, which I could do with headers, but not with modules?
Detailed description
I didn’t find any way to make the module import work. With headers, I would just use target_link_libraries(SharedLibA PRIVATE $<TARGET_OBJECTS:ObjLibA>) and add the headers to SharedLibA via an INTERFACE or PUBLIC file set to make sure they are installed properly. However, CXX_MODULES file sets do not allow INTERFACE visibility, and using a PUBLIC one leads to compiling the primary module interface twice (resulting in error multiple definition of 'initializer for module MyModule' in GCC).
Linking publicly to ObjLibA would propagate the module usage requirement properly to SharedLibA, but then the install(EXPORT) command fails if I dont’t install the object library as well (makes sense and fair enough, but it is an implementation detail that I’d like to keep private). And even if I install it, I run into issue 1 again, since MyExe now again can’t consume the module transitively from the imported ObjLibA target.
Reduced CMake Example
### LibraryA CMakeLists.txt
add_library(ObjLibA OBJECT)
target_sources(ObjLibA PRIVATE src/MyModule.cpp modules/MyModule.cppm)
target_sources(ObjLibA PUBLIC
FILE_SET CXX_MODULES
TYPE CXX_MODULES
FILES modules/MyModule.cppm
)
target_compile_features(ObjLibA PUBLIC cxx_std_23)
add_library(SharedLibA SHARED)
target_sources(SharedLibA PRIVATE src/Empty.cpp)
# Leads to compilation errors due to compiling MyModule twice
target_sources(SharedLibA PUBLIC
FILE_SET CXX_MODULES
TYPE CXX_MODULES
FILES modules/MyModule.cppm
)
target_compile_features(SharedLibA PUBLIC cxx_std_23)
target_link_libraries(SharedLibA PRIVATE $<TARGET_OBJECTS:ObjLibA>)
# Alternatives I looked into:
# target_sources(SharedLibA PRIVATE $<TARGET_OBJECTS:ObjLibA>) # Same error
# target_link_libraries(SharedLibA PUBLIC ObjLibA) # Requires installing ObjLibA
include(GNUInstallDirs)
set(CMAKE_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/cmake/LibraryA")
install(TARGETS LibraryA
EXPORT LibraryATargets
FILE_SET CXX_MODULES DESTINATION ${CMAKE_INSTALL_DIR}/modules
)
# Create and install package config files here...
install(EXPORT LibraryATargets
DESTINATION ${CMAKE_INSTALL_DIR}
CXX_MODULES_DIRECTORY modules
)
### LibraryB CMakeLists.txt
find_package(LibraryA REQUIRED)
add_executable(MyExe src/Main.cpp)
target_compile_features(MyExe PUBLIC cxx_std_23)
target_link_libraries(MyExe PUBLIC LibraryA)
Is there another way to make this work that I’m overlooking?
It seems to me that having INTERFACE visibility available for CXX_MODULES file sets would help a lot with these kinds of problems and give us more freedom (see also this recent topic), but I imagine this might be tricky to implement with all the BMI mapping in the background.