[llvm] [CMake] Propagate generated-header prerequisites through link dependencies (PR #222341)

Mehdi Amini via llvm-commits llvm-commits at lists.llvm.org
Fri Sep 11 14:46:07 PDT 2026


https://github.com/joker-eph updated https://github.com/llvm/llvm-project/pull/222341

>From ffc11e28ebaf0dd6c85949505f3e6105dc75a888 Mon Sep 17 00:00:00 2001
From: Mehdi Amini <joker.eph at gmail.com>
Date: Wed, 9 Sep 2026 03:51:24 -0700
Subject: [PATCH] [CMake] Propagate generated-header prerequisites through link
 dependencies

The goal is to simplify and make more robust the handling of generated-header
(TableGen) dependencies. Today these dependencies are managed manually, which
is fragile: missing edges often surface only in parallel builds, and sometimes
in incremental builds.

Record each llvm_add_library DEPENDS list independently from the cumulative
LLVM_COMMON_DEPENDS value, and mark public TableGen targets as generated-header
prerequisites. A deferred traversal follows every direct non-INTERFACE link and
transitive link interface, resolves aliases, and conservatively extracts target
candidates from generator expressions.

Represent generated-header prerequisites with internal INTERFACE libraries.
Let CMake propagate their utility dependencies through the header graph, and
reuse each provider's interface to handle cycles without computing closures.
Disabled generator-expression arms may generate extra headers, but cannot
introduce a provider library or object cycle.

Attach header interfaces only to consumer implementation links so they do not
leak into exports. Do not link provider libraries directly to object libraries:
that approach was reverted after an ODR regression with LLVM_LINK_LLVM_DYLIB.

Test independent clean consumer builds, cyclic interfaces, excluded unmarked
generators, and build-tree and install exports.

Assisted-by: Codex
Assisted-by: Claude Code
Co-Authored-By: Claude Fable 5.1 <noreply at anthropic.com>
---
 llvm/cmake/modules/AddLLVM.cmake              | 165 ++++++++++++++++--
 llvm/cmake/modules/TableGen.cmake             |   6 +
 .../CMake/generated-header-dependencies.test  |  80 +++++++++
 llvm/test/CMake/lit.local.cfg                 |   2 +
 llvm/test/lit.site.cfg.py.in                  |   2 +
 5 files changed, 241 insertions(+), 14 deletions(-)
 create mode 100644 llvm/test/CMake/generated-header-dependencies.test
 create mode 100644 llvm/test/CMake/lit.local.cfg

diff --git a/llvm/cmake/modules/AddLLVM.cmake b/llvm/cmake/modules/AddLLVM.cmake
index 3169756c0f28a..bc5f6dd46d5d2 100644
--- a/llvm/cmake/modules/AddLLVM.cmake
+++ b/llvm/cmake/modules/AddLLVM.cmake
@@ -554,6 +554,146 @@ function(set_windows_version_resource_properties name resource_file)
                "RC_PRODUCT_VERSION=\"${ARG_VERSION_STRING}\"")
 endfunction(set_windows_version_resource_properties)
 
+# Resolve an alias to the target that carries its build metadata.
+function(_llvm_resolve_target_alias output target)
+  if(TARGET "${target}")
+    get_target_property(aliased_target "${target}" ALIASED_TARGET)
+    if(aliased_target)
+      set(target "${aliased_target}")
+    endif()
+  endif()
+  set(${output} "${target}" PARENT_SCOPE)
+endfunction()
+
+# Collect the targets named by a link item. Generator expressions cannot be
+# evaluated at configure time, so conservatively return every token that names
+# a target. This can add prerequisites from disabled expression arms, but those
+# prerequisites generate headers only and cannot introduce library cycles.
+function(_llvm_link_item_targets output item)
+  if(item MATCHES "\\$<")
+    string(REGEX MATCHALL "[A-Za-z0-9_.+-]+(::[A-Za-z0-9_.+-]+)*"
+      candidates "${item}")
+  else()
+    set(candidates "${item}")
+  endif()
+
+  set(result)
+  foreach(candidate ${candidates})
+    if(TARGET "${candidate}")
+      _llvm_resolve_target_alias(candidate "${candidate}")
+      list(APPEND result "${candidate}")
+    endif()
+  endforeach()
+  list(REMOVE_DUPLICATES result)
+  set(${output} ${result} PARENT_SCOPE)
+endfunction()
+
+# Represent a library's generated headers with a build-local interface target.
+# CMake follows the interface graph and its utility dependencies, including
+# cycles, without adding dependencies on the provider's objects or archive.
+function(_llvm_generated_header_target output provider)
+  set(${output} PARENT_SCOPE)
+  if(NOT TARGET "${provider}")
+    return()
+  endif()
+  _llvm_resolve_target_alias(provider "${provider}")
+  get_property(headers TARGET "${provider}" PROPERTY LLVM_GENERATED_HEADER_INTERFACE)
+  if(headers)
+    set(${output} "${headers}" PARENT_SCOPE)
+    return()
+  endif()
+
+  get_target_property(imported "${provider}" IMPORTED)
+  get_target_property(type "${provider}" TYPE)
+  if(imported OR NOT type MATCHES
+     "^(STATIC|SHARED|MODULE|OBJECT|INTERFACE|UNKNOWN)_LIBRARY$")
+    # Imported generated headers already exist. Non-library targets cannot
+    # publish a transitive link interface.
+    return()
+  endif()
+
+  set(headers "llvm.headers.${provider}")
+  add_library("${headers}" INTERFACE)
+  # Publish the target before following edges so cyclic references reuse it.
+  set_property(TARGET "${provider}" PROPERTY LLVM_GENERATED_HEADER_INTERFACE
+    "${headers}")
+  set(${output} "${headers}" PARENT_SCOPE)
+
+  get_property(dependencies TARGET "${provider}" PROPERTY LLVM_EXPLICIT_DEPENDS)
+  foreach(dependency ${dependencies})
+    if(TARGET "${dependency}")
+      get_target_property(is_generated_header "${dependency}"
+        LLVM_GENERATED_HEADER_TARGET)
+      if(is_generated_header)
+        add_dependencies("${headers}" "${dependency}")
+      endif()
+    endif()
+  endforeach()
+
+  # Only a provider's interface is transitive. PRIVATE implementation links
+  # have already contributed to the provider's own compilation ordering.
+  get_property(links TARGET "${provider}" PROPERTY INTERFACE_LINK_LIBRARIES)
+  foreach(item ${links})
+    _llvm_link_item_targets(link_targets "${item}")
+    foreach(link_target ${link_targets})
+      _llvm_generated_header_target(child_headers "${link_target}")
+      if(child_headers)
+        target_link_libraries("${headers}" INTERFACE "${child_headers}")
+      endif()
+    endforeach()
+  endforeach()
+endfunction()
+
+# Record direct link dependencies for deferred generated-header resolution.
+# The target is the compilation target (normally an object library), not a
+# logical link provider.
+function(_llvm_record_link_dependencies target)
+  if(NOT ARGN)
+    return()
+  endif()
+  set_property(TARGET "${target}" APPEND PROPERTY LLVM_RECORDED_LINK_LIBS
+    ${ARGN})
+  set_property(GLOBAL APPEND PROPERTY LLVM_RECORDED_LINK_CONSUMERS "${target}")
+  get_property(scheduled GLOBAL PROPERTY
+    LLVM_LINK_DEPENDENCIES_SCHEDULED)
+  if(NOT scheduled)
+    cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}"
+      CALL _llvm_resolve_link_dependencies)
+    set_property(GLOBAL PROPERTY LLVM_LINK_DEPENDENCIES_SCHEDULED TRUE)
+  endif()
+endfunction()
+
+# Order compilation after generated-header utility targets reachable through
+# direct PUBLIC, PRIVATE, or unqualified links and transitive interfaces. Do
+# not depend on the provider libraries themselves: that would create strong
+# cycles for mutually linked static libraries and serialize compilation behind
+# archive creation on generators with strong target ordering.
+function(_llvm_resolve_link_dependencies)
+  get_property(consumers GLOBAL PROPERTY LLVM_RECORDED_LINK_CONSUMERS)
+  list(REMOVE_DUPLICATES consumers)
+  foreach(consumer ${consumers})
+    if(NOT TARGET "${consumer}")
+      continue()
+    endif()
+    get_target_property(items "${consumer}" LLVM_RECORDED_LINK_LIBS)
+    set(generated_headers)
+    foreach(item ${items})
+      _llvm_link_item_targets(providers "${item}")
+      foreach(provider ${providers})
+        _llvm_generated_header_target(provider_headers "${provider}")
+        list(APPEND generated_headers ${provider_headers})
+      endforeach()
+    endforeach()
+    list(REMOVE_DUPLICATES generated_headers)
+    if(generated_headers)
+      # These interface targets carry only build ordering. Keep them out of
+      # INTERFACE_LINK_LIBRARIES, including exported static/object interfaces.
+      set_property(TARGET "${consumer}" APPEND PROPERTY LINK_LIBRARIES
+        ${generated_headers})
+    endif()
+  endforeach()
+endfunction()
+
 # llvm_add_library(name sources...
 #   SHARED;STATIC
 #     STATIC by default w/o BUILD_SHARED_LIBS.
@@ -674,25 +814,18 @@ function(llvm_add_library name)
     if(ARG_DEPENDS)
       add_dependencies(${obj_name} ${ARG_DEPENDS})
     endif()
-    # Treat link libraries like PUBLIC dependencies.  LINK_LIBS might
-    # result in generating header files.  Add a dependendency so that
-    # the generated header is created before this object library.
+    # Record direct compilation dependencies after all targets have been
+    # declared. INTERFACE entries are not used by this target's sources.
     if(ARG_LINK_LIBS)
       cmake_parse_arguments(LINK_LIBS_ARG
         ""
         ""
-        "PUBLIC;PRIVATE"
+        "PUBLIC;PRIVATE;INTERFACE"
         ${ARG_LINK_LIBS})
-      foreach(link_lib ${LINK_LIBS_ARG_PUBLIC})
-        if(LLVM_PTHREAD_LIB)
-          # Can't specify a dependence on -lpthread
-          if(NOT ${link_lib} STREQUAL ${LLVM_PTHREAD_LIB})
-            add_dependencies(${obj_name} ${link_lib})
-          endif()
-        else()
-          add_dependencies(${obj_name} ${link_lib})
-        endif()
-      endforeach()
+      _llvm_record_link_dependencies(${obj_name}
+        ${LINK_LIBS_ARG_PUBLIC}
+        ${LINK_LIBS_ARG_PRIVATE}
+        ${LINK_LIBS_ARG_UNPARSED_ARGUMENTS})
     endif()
 
     if(ARG_DISABLE_LLVM_LINK_LLVM_DYLIB)
@@ -731,6 +864,10 @@ function(llvm_add_library name)
   else()
     add_library(${name} STATIC ${ALL_FILES})
   endif()
+  # Preserve the exact DEPENDS list for generated-header propagation. Do not
+  # use MANUALLY_ADDED_DEPENDENCIES here: it also contains cumulative and
+  # post-hoc utility dependencies unrelated to the library's public headers.
+  set_property(TARGET ${name} PROPERTY LLVM_EXPLICIT_DEPENDS ${ARG_DEPENDS})
   set_target_properties(${name} PROPERTIES FOLDER "${subproject_title}/Libraries")
 
   ## If were compiling with clang-cl use /Zc:dllexportInlines- to exclude inline
diff --git a/llvm/cmake/modules/TableGen.cmake b/llvm/cmake/modules/TableGen.cmake
index 397089a2d3efe..fc70919b3e255 100644
--- a/llvm/cmake/modules/TableGen.cmake
+++ b/llvm/cmake/modules/TableGen.cmake
@@ -171,6 +171,12 @@ function(add_public_tablegen_target target)
   endif()
   add_custom_target(${target}
     DEPENDS ${TABLEGEN_OUTPUT})
+  # Libraries propagate only explicitly declared generated-header prerequisites
+  # through their link interfaces.  Keep this marker separate from the target
+  # type: other utility targets may generate sources or run build tools and are
+  # not safe compilation-order dependencies for arbitrary consumers.
+  set_target_properties(${target} PROPERTIES
+    LLVM_GENERATED_HEADER_TARGET TRUE)
   if(LLVM_COMMON_DEPENDS)
     add_dependencies(${target} ${LLVM_COMMON_DEPENDS})
   endif()
diff --git a/llvm/test/CMake/generated-header-dependencies.test b/llvm/test/CMake/generated-header-dependencies.test
new file mode 100644
index 0000000000000..c98cd3904cb43
--- /dev/null
+++ b/llvm/test/CMake/generated-header-dependencies.test
@@ -0,0 +1,80 @@
+# RUN: rm -rf %t
+# RUN: split-file %s %t
+# RUN: "%cmake_exe" -S %t -B %t/build -G "%cmake_generator" \
+# RUN:   -DLLVM_SOURCE_DIR="%llvm_src_root" 2>&1 | FileCheck %s
+# RUN: "%cmake_exe" --build %t/build --target Consumer --parallel 4
+# RUN: "%cmake_exe" --build %t/build --target clean
+# RUN: "%cmake_exe" --build %t/build --target OtherCycleConsumer --parallel 4
+
+# CHECK: -- Generated header dependency checks passed
+
+#--- CMakeLists.txt
+cmake_minimum_required(VERSION 3.20)
+project(GeneratedHeaderDependencies LANGUAGES C CXX)
+set(CMAKE_INSTALL_LIBDIR lib)
+list(APPEND CMAKE_MODULE_PATH "${LLVM_SOURCE_DIR}/cmake/modules")
+include(AddLLVM)
+include(TableGen)
+
+foreach(provider Base Provider 3rdParty Disabled CycleA CycleB)
+  set(LLVM_COMMON_DEPENDS)
+  set(header "${CMAKE_CURRENT_BINARY_DIR}/${provider}.h")
+  add_custom_command(OUTPUT "${header}"
+    COMMAND ${CMAKE_COMMAND} -E touch "${header}")
+  set(TABLEGEN_OUTPUT "${header}")
+  add_public_tablegen_target(${provider}IncGen)
+  add_library(${provider} INTERFACE)
+  set_property(TARGET ${provider} PROPERTY
+    LLVM_EXPLICIT_DEPENDS ${provider}IncGen)
+endforeach()
+add_custom_target(SourceGenerator
+  COMMAND ${CMAKE_COMMAND} -E false)
+set_property(TARGET Provider APPEND PROPERTY LLVM_EXPLICIT_DEPENDS
+  SourceGenerator)
+target_link_libraries(Provider INTERFACE Base)
+add_library(ProviderAlias ALIAS Provider)
+target_link_libraries(CycleA INTERFACE CycleB)
+target_link_libraries(CycleB INTERFACE CycleA)
+
+add_library(Consumer OBJECT consumer.cpp)
+target_include_directories(Consumer PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
+_llvm_record_link_dependencies(Consumer ProviderAlias
+  "$<$<BOOL:1>:3rdParty>" "$<$<BOOL:0>:Disabled>" CycleA
+  /absolute/libunused.a -lpthread)
+add_library(OtherCycleConsumer OBJECT cycle.cpp)
+target_include_directories(OtherCycleConsumer PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
+_llvm_record_link_dependencies(OtherCycleConsumer CycleB)
+
+# The build-local interfaces must not leak into exported consumers.
+export(TARGETS Consumer OtherCycleConsumer FILE consumers.cmake)
+install(TARGETS Consumer OtherCycleConsumer EXPORT consumers OBJECTS DESTINATION lib)
+install(EXPORT consumers DESTINATION lib/cmake)
+
+function(check_dependencies)
+  get_target_property(actual Consumer LINK_LIBRARIES)
+  set(expected llvm.headers.Provider llvm.headers.3rdParty
+    llvm.headers.Disabled llvm.headers.CycleA)
+  list(SORT actual)
+  list(SORT expected)
+  if(NOT "${actual}" STREQUAL "${expected}")
+    message(FATAL_ERROR "Expected '${expected}', got '${actual}'")
+  endif()
+  get_target_property(interface Consumer INTERFACE_LINK_LIBRARIES)
+  if(interface)
+    message(FATAL_ERROR "Build-local header targets escaped: '${interface}'")
+  endif()
+  message(STATUS "Generated header dependency checks passed")
+endfunction()
+cmake_language(DEFER CALL check_dependencies)
+
+#--- consumer.cpp
+#include "Base.h"
+#include "Provider.h"
+#include "3rdParty.h"
+#include "Disabled.h"
+#include "CycleA.h"
+#include "CycleB.h"
+
+#--- cycle.cpp
+#include "CycleA.h"
+#include "CycleB.h"
diff --git a/llvm/test/CMake/lit.local.cfg b/llvm/test/CMake/lit.local.cfg
new file mode 100644
index 0000000000000..07cfb650c817c
--- /dev/null
+++ b/llvm/test/CMake/lit.local.cfg
@@ -0,0 +1,2 @@
+config.substitutions.append(("%cmake_exe", config.host_cmake))
+config.substitutions.append(("%cmake_generator", config.host_cmake_generator))
diff --git a/llvm/test/lit.site.cfg.py.in b/llvm/test/lit.site.cfg.py.in
index f72ab5a205edf..f5fcd92a7ad7c 100644
--- a/llvm/test/lit.site.cfg.py.in
+++ b/llvm/test/lit.site.cfg.py.in
@@ -33,6 +33,8 @@ config.llvm_bindings = "@LLVM_BINDINGS@".split(' ')
 config.target_os = "@HOST_OS@"
 config.host_cc = "@HOST_CC@"
 config.host_cxx = "@HOST_CXX@"
+config.host_cmake = "@CMAKE_COMMAND@"
+config.host_cmake_generator = "@CMAKE_GENERATOR@"
 # Note: ldflags can contain double-quoted paths, so must use single quotes here.
 config.host_ldflags = '@HOST_LDFLAGS@'
 config.llvm_use_intel_jitevents = @LLVM_USE_INTEL_JITEVENTS@



More information about the llvm-commits mailing list