[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