From 725ea968acd715552fa056099ceffec1e0ff90eb Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 20 Nov 2025 15:37:27 +0000 Subject: [PATCH] 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 --- Source/cmComputeTargetDepends.cxx | 23 +++++++++------- Source/cmComputeTargetDepends.h | 6 +++-- .../InterfaceLibrary/RunCMakeTest.cmake | 3 +++ .../unique_dependencies-result.txt | 1 + .../unique_dependencies-stderr.txt | 26 +++++++++++++++++++ .../unique_dependencies.cmake | 19 ++++++++++++++ 6 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 Tests/RunCMake/InterfaceLibrary/unique_dependencies-result.txt create mode 100644 Tests/RunCMake/InterfaceLibrary/unique_dependencies-stderr.txt create mode 100644 Tests/RunCMake/InterfaceLibrary/unique_dependencies.cmake diff --git a/Source/cmComputeTargetDepends.cxx b/Source/cmComputeTargetDepends.cxx index 986a314c52..4b165b1197 100644 --- a/Source/cmComputeTargetDepends.cxx +++ b/Source/cmComputeTargetDepends.cxx @@ -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& 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& emitted) { if (!dependee->IsInBuildSystem()) { // Skip targets that are not in the buildsystem but follow their // utility dependencies. std::set 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 { diff --git a/Source/cmComputeTargetDepends.h b/Source/cmComputeTargetDepends.h index cb5bd16945..26569b599c 100644 --- a/Source/cmComputeTargetDepends.h +++ b/Source/cmComputeTargetDepends.h @@ -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& emitted); void AddTargetDepend(size_t depender_index, cmGeneratorTarget const* dependee, cmListFileBacktrace const& dependee_backtrace, - bool linking, bool cross); + bool linking, bool cross, + std::set& emitted); void CollectSideEffects(); void CollectSideEffectsForTarget(std::set& visited, size_t depender_index); diff --git a/Tests/RunCMake/InterfaceLibrary/RunCMakeTest.cmake b/Tests/RunCMake/InterfaceLibrary/RunCMakeTest.cmake index 10a2d511b9..a92b06ab20 100644 --- a/Tests/RunCMake/InterfaceLibrary/RunCMakeTest.cmake +++ b/Tests/RunCMake/InterfaceLibrary/RunCMakeTest.cmake @@ -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) diff --git a/Tests/RunCMake/InterfaceLibrary/unique_dependencies-result.txt b/Tests/RunCMake/InterfaceLibrary/unique_dependencies-result.txt new file mode 100644 index 0000000000..573541ac97 --- /dev/null +++ b/Tests/RunCMake/InterfaceLibrary/unique_dependencies-result.txt @@ -0,0 +1 @@ +0 diff --git a/Tests/RunCMake/InterfaceLibrary/unique_dependencies-stderr.txt b/Tests/RunCMake/InterfaceLibrary/unique_dependencies-stderr.txt new file mode 100644 index 0000000000..5829b1528c --- /dev/null +++ b/Tests/RunCMake/InterfaceLibrary/unique_dependencies-stderr.txt @@ -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\] diff --git a/Tests/RunCMake/InterfaceLibrary/unique_dependencies.cmake b/Tests/RunCMake/InterfaceLibrary/unique_dependencies.cmake new file mode 100644 index 0000000000..d27720aecf --- /dev/null +++ b/Tests/RunCMake/InterfaceLibrary/unique_dependencies.cmake @@ -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)