De-duplicate dependencies propagated through interface libraries

When processing an indirect dependency of target `A` on target `C` via
an interface library B, `cmComputeTargetDepends::AddTargetDepend` was
not checking for duplicate dependencies, so that if `A` depended on `C`
via multiple interface libraries, the dependency graph built in
cmComputeTargetDepends would have duplicate edges.  In a real project
(LLVM libc) this was causing cmake to consume multiple gigabytes of RAM.

Fixes: #27386
This commit is contained in:
Simon Tatham
2025-11-20 15:37:27 +00:00
parent 3e5f4aa5c5
commit 725ea968ac
6 changed files with 67 additions and 11 deletions

View File

@@ -224,7 +224,7 @@ void cmComputeTargetDepends::CollectTargetDepends(size_t depender_index)
for (cmLinkItem const& lib : impl->Libraries) {
// Don't emit the same library twice for this target.
if (emitted.insert(lib).second) {
this->AddTargetDepend(depender_index, lib, true, false);
this->AddTargetDepend(depender_index, lib, true, false, emitted);
this->AddInterfaceDepends(depender_index, lib, it, emitted);
}
}
@@ -255,7 +255,8 @@ void cmComputeTargetDepends::CollectTargetDepends(size_t depender_index)
for (cmLinkItem const& litem : tutils) {
// Don't emit the same utility twice for this target.
if (emitted.insert(litem).second) {
this->AddTargetDepend(depender_index, litem, false, litem.Cross);
this->AddTargetDepend(depender_index, litem, false, litem.Cross,
emitted);
}
}
}
@@ -277,7 +278,7 @@ void cmComputeTargetDepends::AddInterfaceDepends(
// code in the project that caused this dependency to be added.
cmLinkItem libBT = lib;
libBT.Backtrace = dependee_backtrace;
this->AddTargetDepend(depender_index, libBT, true, false);
this->AddTargetDepend(depender_index, libBT, true, false, emitted);
this->AddInterfaceDepends(depender_index, libBT, config, emitted);
}
}
@@ -343,7 +344,8 @@ void cmComputeTargetDepends::AddObjectDepends(size_t depender_index,
void cmComputeTargetDepends::AddTargetDepend(size_t depender_index,
cmLinkItem const& dependee_name,
bool linking, bool cross)
bool linking, bool cross,
std::set<cmLinkItem>& emitted)
{
// Get the depender.
cmGeneratorTarget const* depender = this->Targets[depender_index];
@@ -370,22 +372,25 @@ void cmComputeTargetDepends::AddTargetDepend(size_t depender_index,
if (dependee) {
this->AddTargetDepend(depender_index, dependee, dependee_name.Backtrace,
linking, cross);
linking, cross, emitted);
}
}
void cmComputeTargetDepends::AddTargetDepend(
size_t depender_index, cmGeneratorTarget const* dependee,
cmListFileBacktrace const& dependee_backtrace, bool linking, bool cross)
cmListFileBacktrace const& dependee_backtrace, bool linking, bool cross,
std::set<cmLinkItem>& emitted)
{
if (!dependee->IsInBuildSystem()) {
// Skip targets that are not in the buildsystem but follow their
// utility dependencies.
std::set<cmLinkItem> const& utils = dependee->GetUtilityItems();
for (cmLinkItem const& i : utils) {
if (cmGeneratorTarget const* transitive_dependee = i.Target) {
this->AddTargetDepend(depender_index, transitive_dependee, i.Backtrace,
false, i.Cross);
if (emitted.insert(i).second) {
if (cmGeneratorTarget const* transitive_dependee = i.Target) {
this->AddTargetDepend(depender_index, transitive_dependee,
i.Backtrace, false, i.Cross, emitted);
}
}
}
} else {

View File

@@ -54,11 +54,13 @@ private:
void CollectDepends();
void CollectTargetDepends(size_t depender_index);
void AddTargetDepend(size_t depender_index, cmLinkItem const& dependee_name,
bool linking, bool cross);
bool linking, bool cross,
std::set<cmLinkItem>& emitted);
void AddTargetDepend(size_t depender_index,
cmGeneratorTarget const* dependee,
cmListFileBacktrace const& dependee_backtrace,
bool linking, bool cross);
bool linking, bool cross,
std::set<cmLinkItem>& emitted);
void CollectSideEffects();
void CollectSideEffectsForTarget(std::set<size_t>& visited,
size_t depender_index);

View File

@@ -10,6 +10,9 @@ run_cmake(add_custom_command-TARGET)
run_cmake(IMPORTED_LIBNAME-bad-value)
run_cmake(IMPORTED_LIBNAME-non-iface)
run_cmake(IMPORTED_LIBNAME-non-imported)
if(RunCMake_GENERATOR MATCHES "(Ninja|Make|FASTBuild)")
run_cmake(unique_dependencies)
endif()
function(run_WithSources CASE)
if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,26 @@
The initial target dependency graph is:
target 0 is \[lib1\]
target 1 is \[lib2\]
target 2 is \[top\]
depends on target 3 \[util\] \(strong\)
target 3 is \[util\]
target 4 is \[edit_cache\]
target 5 is \[rebuild_cache\]
.*
The intermediate target dependency graph is:
target 0 is \[lib1\]
target 1 is \[lib2\]
target 2 is \[top\]
depends on target 3 \[util\] \(strong\)
target 3 is \[util\]
target 4 is \[edit_cache\]
target 5 is \[rebuild_cache\]
.*
The final target dependency graph is:
target 0 is \[lib1\]
target 1 is \[lib2\]
target 2 is \[top\]
depends on target 3 \[util\] \(strong\)
target 3 is \[util\]
target 4 is \[edit_cache\]
target 5 is \[rebuild_cache\]

View File

@@ -0,0 +1,19 @@
# Make two interface libraries.
add_library(lib1 INTERFACE)
add_library(lib2 INTERFACE)
# Top-level target that depends on both of them.
add_custom_target(top
COMMAND ${CMAKE_COMMAND} -E echo top
DEPENDS lib1 lib2)
# Lowest-level utility target that both libraries depend on.
add_custom_target(util
COMMAND ${CMAKE_COMMAND} -E echo util)
add_dependencies(lib1 util)
add_dependencies(lib2 util)
# The dependency graph computed by cmComputeTargetDepends will include
# an edge directly from 'top' to 'util'. But it should only include
# one copy of it, even though there are two paths via lib1 and lib2.
set_property(GLOBAL PROPERTY GLOBAL_DEPENDS_DEBUG_MODE 1)