VS: AllConfigSources cache can break PCH compilation
Minimal example:
project(Test)
add_library(target_a source_a.cpp)
add_library(target_b SHARED source_b.cpp)
target_precompile_headers(target_b PRIVATE header.h)
target_link_libraries(target_a "$<TARGET_LINKER_FILE:target_b>")
Generator: Visual Studio 16 2019
The generated target_b.vcxproj
lacks the ClCompile
entry for cmake_pch.cxx
file which would normally cause the creation of the PCH. This causes the build to fail. Removing the last line fixes the issue.
Analysis
After long investigation, I found this issue to be caused by this chain of events:
-
cmGlobalGenerator::AddAutomaticSources()
callscmLocalGenerator::AddPchDependencies()
ontarget_a
. - To determine the list of languages that need PCHs,
AddPchDependencies()
callscmGeneratorTarget::GetSourceFiles()
. - This function causes a lookup of
target_a
's dependencies'INTERFACE_SOURCES
, which triggers the evaluation of the generator expression. - In order to get the
ArtifactType
argument forcmGeneratorTarget::GetFullPath()
, the genexp evaluator callscmGeneratorTarget::HasImportLibrary()
ontarget_b
. - On Windows,
HasImportLibrary()
checks whether a shared library is managed-only. -
cmGeneratorTarget::GetManagedType()
callsIsCSharpOnly()
, callingGetAllConfigCompileLangues()
, callingGetAllConfigSources()
– still ontarget_b
- This causes
cmGeneratorTarget(target_a)->AllConfigSources
to be calculated beforetarget_b
'scmake_pch.cxx
is added to its source list.
Now, target_b
is processed:
-
cmGlobalGenerator::AddAutomaticSources()
callscmLocalGenerator::AddPchDependencies()
ontarget_b
. Acmake_pch.cxx
is added totarget_b
's sources list, butAllConfigSources
is not touched. - This cache is never cleared. So later on in the generation stage,
target_b
'scmake_pch.cxx
is not written to the generated project. - This happens in
cmVisualStudio10TargetGenerator::WriteSource()
, which usesGetAllConfigSources()
to get the list of source files to be compiled.
Patch
I don't know the code enough to be sure that this is the correct fix for the issue, but this change stops the issue:
diff --git a/Source/cmGeneratorTarget.cxx b/Source/cmGeneratorTarget.cxx
index a44126c13c..e25be6bd05 100644
--- a/Source/cmGeneratorTarget.cxx
+++ b/Source/cmGeneratorTarget.cxx
@@ -694,6 +694,7 @@ const char* cmGeneratorTarget::GetFileSuffixInternal(
void cmGeneratorTarget::ClearSourcesCache()
{
this->KindedSourcesMap.clear();
+ this->AllConfigSources.clear();
this->LinkImplementationLanguageIsContextDependent = true;
this->Objects.clear();
this->VisitedConfigsForObjects.clear();
ClearSourcesCache()
is called by AddAutomaticSources()
after all the sources have been added, presumably to prevent exactly this kind of issue?
History
A git bisect
showed that this started with bcaecf6b. There, the call from IsCSharpOnly()
to GetAllConfigCompileLanguages()
was added, completing the chain detailed above.
This makes 3.17.0 the first affected release.
Misc
This is the cause for #20702 (closed). As my colleague explained there, we saw a similar problem with an older version, but struggled to reproduce it. I think that was also before we added the generator expression in our target_link_library
call. It doesn't seem unlikely that other paths through the code could cause a similar issue.
The generator expression in the target_link_library
call might look odd because it's just a convoluted way to link against target_b
. We do this in our codebase because we need target_b
's DLL to be loaded as the very first DLL in our process. This can be achieved on windows by listing target_b
's lib first on the linker command line. But cmake does a toposort on the linker inputs, so if a target_c
that depends on target_b
, target_b.lib
is moved to the back of the linker command line. So we do this generator expresion trick to prevent cmake from seeing this dependency and moving it in the toposort. I couldn't find one, but does cmake have a prettier way of implementing this?