[llvm-branch-commits] [llvm] [mlir] [MLIR][CMake] Add HEADER_LIBS and document CMake infrastructure (PR #222397)

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


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

>From 8a5ff01a74f0e3409c4963c4cf3db511cfeed355 Mon Sep 17 00:00:00 2001
From: Mehdi Amini <joker.eph at gmail.com>
Date: Wed, 9 Sep 2026 06:07:45 -0700
Subject: [PATCH] [MLIR][CMake] Add HEADER_LIBS and document CMake
 infrastructure

Add HEADER_LIBS as a flat list of literal library targets for generated headers
included without a link relationship. Resolve aliases and forward references,
accept imported libraries as already generated, follow nested HEADER_LIBS and
link interfaces, and reject missing, executable, utility, or generator-
expression entries with configure-time diagnostics. Represent header-only
edges in the common internal INTERFACE graph, including cyclic relationships.

Record links added by mlir_target_link_libraries for the same deferred ordering.
Add explicit, commented HEADER_LIBS edges for every audited header-only include
that is not covered by mlir-generic-headers, and keep this facility a rare
layering escape hatch.

Document dialects, interfaces, passes, PDLL, generated documentation, library
visibility, C API aggregation, tools, exports, standalone consumers, and the
generated-file model. Add a CMake fixture covering cycles, aliases, conditions,
LINK_ONLY, imported and ignored items, post-hoc links, and invalid providers.
Build each consumer independently from a clean state to verify transitive
generated-header ordering without unrelated generators masking missing edges.

Assisted-by: Codex
Assisted-by: Claude Code
Co-Authored-By: Claude Fable 5.1 <noreply at anthropic.com>
---
 llvm/cmake/modules/AddLLVM.cmake              |   5 +-
 mlir/cmake/modules/AddMLIR.cmake              | 111 +++++-
 mlir/docs/CMakeInfrastructure.md              | 343 ++++++++++++++++++
 .../DefiningDialects/AttributesAndTypes.md    |   2 +-
 .../lib/Conversion/SPIRVCommon/CMakeLists.txt |   4 +
 mlir/lib/Dialect/Arith/IR/CMakeLists.txt      |   4 +
 mlir/lib/Dialect/Func/IR/CMakeLists.txt       |   4 +
 mlir/lib/Dialect/LLVMIR/CMakeLists.txt        |   8 +
 mlir/lib/Dialect/Linalg/IR/CMakeLists.txt     |   4 +
 .../Dialect/Shape/Transforms/CMakeLists.txt   |   4 +-
 mlir/lib/Dialect/Tensor/IR/CMakeLists.txt     |   4 +
 mlir/lib/Dialect/Tosa/CMakeLists.txt          |   4 +
 mlir/lib/Rewrite/CMakeLists.txt               |  11 +-
 mlir/lib/Target/SPIRV/CMakeLists.txt          |   4 +
 mlir/test/CMake/header-dependencies.test      | 192 ++++++++++
 mlir/test/CMake/lit.local.cfg                 |   7 +
 16 files changed, 704 insertions(+), 7 deletions(-)
 create mode 100644 mlir/docs/CMakeInfrastructure.md
 create mode 100644 mlir/test/CMake/header-dependencies.test
 create mode 100644 mlir/test/CMake/lit.local.cfg

diff --git a/llvm/cmake/modules/AddLLVM.cmake b/llvm/cmake/modules/AddLLVM.cmake
index bc5f6dd46d5d2..03d90892c6a77 100644
--- a/llvm/cmake/modules/AddLLVM.cmake
+++ b/llvm/cmake/modules/AddLLVM.cmake
@@ -633,7 +633,10 @@ function(_llvm_generated_header_target output provider)
   # 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})
+  # MLIR uses this property for generated headers included without a link.
+  # Both kinds of edges belong to the same header interface graph.
+  get_property(header_libraries TARGET "${provider}" PROPERTY LLVM_HEADER_LIBS)
+  foreach(item ${links} ${header_libraries})
     _llvm_link_item_targets(link_targets "${item}")
     foreach(link_target ${link_targets})
       _llvm_generated_header_target(child_headers "${link_target}")
diff --git a/mlir/cmake/modules/AddMLIR.cmake b/mlir/cmake/modules/AddMLIR.cmake
index d3f534a02f9de..945908649a3d6 100644
--- a/mlir/cmake/modules/AddMLIR.cmake
+++ b/mlir/cmake/modules/AddMLIR.cmake
@@ -273,6 +273,103 @@ function(_check_llvm_components_usage name)
   endforeach()
 endfunction()
 
+# Normalize providers while directory-local targets are still visible.
+function(_mlir_normalize_header_libraries)
+  get_property(consumers DIRECTORY PROPERTY MLIR_HEADER_LIBS_CONSUMERS)
+  list(REMOVE_DUPLICATES consumers)
+  foreach(consumer ${consumers})
+    get_target_property(items "${consumer}" LLVM_HEADER_LIBS)
+    set(providers)
+    foreach(provider ${items})
+      # Imported targets and their aliases may be visible only in this
+      # directory. Their headers already exist, so handle them before the
+      # top-level resolver runs. Waiting until the directory is complete also
+      # supports forward declarations of these local targets.
+      if(TARGET "${provider}")
+        _llvm_resolve_target_alias(provider "${provider}")
+        get_target_property(imported "${provider}" IMPORTED)
+        get_target_property(type "${provider}" TYPE)
+        if(imported AND type MATCHES
+           "^(STATIC|SHARED|MODULE|OBJECT|INTERFACE|UNKNOWN)_LIBRARY$")
+          continue()
+        endif()
+      endif()
+      list(APPEND providers "${provider}")
+    endforeach()
+    set_property(TARGET "${consumer}" PROPERTY LLVM_HEADER_LIBS "${providers}")
+  endforeach()
+endfunction()
+
+# Record generated-header providers that are intentionally not linked. Resolve
+# them after the full project has been declared so aliases and forward target
+# references work and misspellings receive a useful diagnostic.
+function(_mlir_add_header_libraries consumer)
+  if(NOT ARGN)
+    return()
+  endif()
+  set_property(TARGET "${consumer}" APPEND PROPERTY LLVM_HEADER_LIBS ${ARGN})
+  set_property(DIRECTORY APPEND PROPERTY MLIR_HEADER_LIBS_CONSUMERS "${consumer}")
+  get_property(normalization_scheduled DIRECTORY PROPERTY
+    MLIR_HEADER_LIBS_NORMALIZATION_SCHEDULED)
+  if(NOT normalization_scheduled)
+    cmake_language(DEFER CALL _mlir_normalize_header_libraries)
+    set_property(DIRECTORY PROPERTY MLIR_HEADER_LIBS_NORMALIZATION_SCHEDULED TRUE)
+  endif()
+  set_property(GLOBAL APPEND PROPERTY MLIR_HEADER_LIBS_CONSUMERS "${consumer}")
+  get_property(scheduled GLOBAL PROPERTY MLIR_HEADER_LIBS_SCHEDULED)
+  if(NOT scheduled)
+    cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}"
+      CALL _mlir_resolve_header_libraries)
+    set_property(GLOBAL PROPERTY MLIR_HEADER_LIBS_SCHEDULED TRUE)
+  endif()
+endfunction()
+
+# Attach build-local header interfaces, which carry only generator ordering.
+# CMake follows nested and cyclic interface relationships without depending on
+# the provider libraries themselves.
+function(_mlir_resolve_header_libraries)
+  get_property(consumers GLOBAL PROPERTY MLIR_HEADER_LIBS_CONSUMERS)
+  list(REMOVE_DUPLICATES consumers)
+  foreach(consumer ${consumers})
+    get_target_property(providers "${consumer}" LLVM_HEADER_LIBS)
+    set(generated_headers)
+    foreach(provider ${providers})
+      if(provider MATCHES "\\$<")
+        message(SEND_ERROR
+          "${consumer}: HEADER_LIBS '${provider}' is not a literal target name")
+        continue()
+      endif()
+      if(NOT TARGET "${provider}")
+        message(SEND_ERROR
+          "${consumer}: HEADER_LIBS '${provider}' is not a target")
+        continue()
+      endif()
+
+      _llvm_resolve_target_alias(resolved_provider "${provider}")
+      get_target_property(type "${resolved_provider}" TYPE)
+      if(NOT type MATCHES
+         "^(STATIC|SHARED|MODULE|OBJECT|INTERFACE|UNKNOWN)_LIBRARY$")
+        message(SEND_ERROR
+          "${consumer}: HEADER_LIBS '${provider}' must name a library, not ${type}")
+        continue()
+      endif()
+
+      _llvm_generated_header_target(provider_headers "${resolved_provider}")
+      list(APPEND generated_headers ${provider_headers})
+    endforeach()
+
+    list(REMOVE_DUPLICATES generated_headers)
+    if(generated_headers)
+      set_property(TARGET "${consumer}" APPEND PROPERTY LINK_LIBRARIES
+        ${generated_headers})
+      if(TARGET "obj.${consumer}")
+        set_property(TARGET "obj.${consumer}" APPEND PROPERTY LINK_LIBRARIES
+          ${generated_headers})
+      endif()
+    endif()
+  endforeach()
+endfunction()
+
 function(add_mlir_example_library name)
   cmake_parse_arguments(ARG
     "SHARED;DISABLE_INSTALL"
@@ -325,11 +422,16 @@ endfunction()
 #   are compatible with building an object library.
 # STANDALONE
 #   Don't link against LLVMSupport.
+# HEADER_LIBS
+#   A flat list of MLIR library targets whose generated headers are included
+#   without linking. This is a rare escape hatch for header-only or circular
+#   layering; linked libraries belong in LINK_LIBS and a library's own
+#   generators belong in DEPENDS.
 function(add_mlir_library name)
   cmake_parse_arguments(ARG
     "SHARED;INSTALL_WITH_TOOLCHAIN;EXCLUDE_FROM_LIBMLIR;DISABLE_INSTALL;ENABLE_AGGREGATION;OBJECT;STANDALONE"
     ""
-    "ADDITIONAL_HEADERS;DEPENDS;LINK_COMPONENTS;LINK_LIBS"
+    "ADDITIONAL_HEADERS;DEPENDS;HEADER_LIBS;LINK_COMPONENTS;LINK_LIBS"
     ${ARGN})
   _set_mlir_additional_headers_as_srcs(${ARG_ADDITIONAL_HEADERS})
 
@@ -401,6 +503,7 @@ function(add_mlir_library name)
     # Add empty "phony" target
     add_custom_target(${name})
   endif()
+  _mlir_add_header_libraries(${name} ${ARG_HEADER_LIBS})
   set_target_properties(${name} PROPERTIES FOLDER "MLIR/Libraries")
 
   # Setup aggregate.
@@ -748,6 +851,12 @@ endfunction(mlir_check_all_link_libraries)
 function(mlir_target_link_libraries target type)
   if (TARGET obj.${target})
     target_link_libraries(obj.${target} ${type} ${ARGN})
+    cmake_parse_arguments(LINK_LIBS_ARG "" "" "PUBLIC;PRIVATE;INTERFACE"
+      ${type} ${ARGN})
+    _llvm_record_link_dependencies(obj.${target}
+      ${LINK_LIBS_ARG_PUBLIC}
+      ${LINK_LIBS_ARG_PRIVATE}
+      ${LINK_LIBS_ARG_UNPARSED_ARGUMENTS})
   endif()
 
   if (MLIR_LINK_MLIR_DYLIB)
diff --git a/mlir/docs/CMakeInfrastructure.md b/mlir/docs/CMakeInfrastructure.md
new file mode 100644
index 0000000000000..a6fe8f6f87dea
--- /dev/null
+++ b/mlir/docs/CMakeInfrastructure.md
@@ -0,0 +1,343 @@
+# CMake Infrastructure
+
+[TOC]
+
+MLIR extends LLVM's CMake infrastructure with helpers for TableGen, libraries,
+dialects, interfaces, tools, installation, and aggregate libraries. This guide
+describes the MLIR-specific conventions implemented by
+[`AddMLIR.cmake`](../cmake/modules/AddMLIR.cmake). The LLVM CMake documentation
+still applies to the underlying LLVM helpers.
+
+## Loading the MLIR CMake modules
+
+The monorepo build loads the required modules. An out-of-tree project using an
+installed MLIR package normally starts with:
+
+~~~cmake
+find_package(MLIR REQUIRED CONFIG)
+
+list(APPEND CMAKE_MODULE_PATH "${MLIR_CMAKE_DIR}")
+list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}")
+include(TableGen)
+include(AddLLVM)
+include(AddMLIR)
+include(HandleLLVMOptions)
+~~~
+
+The project under `mlir/examples/standalone` is the canonical out-of-tree
+template. It demonstrates package discovery, generated files, libraries,
+tools, tests, and installation without depending on the monorepo build.
+
+## Source and generated-file layout
+
+Public declarations normally live below `mlir/include/mlir`, with
+implementations in the corresponding directory below `mlir/lib`. For example,
+a dialect declared in `include/mlir/Dialect/Foo/IR` is normally implemented by
+a library in `lib/Dialect/Foo/IR`.
+
+Generated target names such as `MLIRFooOpsIncGen` are build-tree details. The
+library that compiles or publishes those generated files lists the target in
+its own `DEPENDS`. Other libraries normally depend on the logical library
+through `LINK_LIBS`, not on its private generation target.
+
+Generated source files are different from published generated headers. A
+source-generation target, such as one created for sharded operations, remains
+an explicit `DEPENDS` entry of the library compiling those sources.
+
+## TableGen
+
+Set `LLVM_TARGET_DEFINITIONS`, call `mlir_tablegen` once for each output, and
+finish with the helper matching the output:
+
+| Helper | Intended output |
+| --- | --- |
+| `add_mlir_dialect` | Standard operation, type, and dialect fragments |
+| `add_mlir_dialect_tablegen_target` | Other dialect-specific headers |
+| `add_mlir_generic_tablegen_target` | Dialect-independent headers |
+| `add_public_tablegen_target` | A library-specific header or source |
+
+For example:
+
+~~~cmake
+set(LLVM_TARGET_DEFINITIONS FooPatterns.td)
+mlir_tablegen(FooPatterns.h.inc -gen-rewriters)
+add_public_tablegen_target(MLIRFooPatternsIncGen)
+~~~
+
+The library that includes `FooPatterns.h.inc` then lists
+`MLIRFooPatternsIncGen` in `DEPENDS`.
+
+`mlir-generic-headers` collects dialect-independent public generation targets,
+and every MLIR library depends on it. `mlir-headers` also includes
+dialect-specific targets. The latter is a conservative compatibility aggregate
+and should not replace precise library dependencies in new code.
+
+### Dialects
+
+The common dialect declaration is:
+
+~~~cmake
+add_mlir_dialect(FooOps foo)
+~~~
+
+This generates operation, type, and dialect declaration and definition
+fragments and creates `MLIRFooOpsIncGen`. The implementation library lists that
+target explicitly:
+
+~~~cmake
+add_mlir_dialect_library(MLIRFooDialect
+  FooDialect.cpp
+  FooOps.cpp
+
+  DEPENDS
+  MLIRFooOpsIncGen
+
+  LINK_LIBS PUBLIC
+  MLIRIR
+  )
+~~~
+
+### Operation, type, and attribute interfaces
+
+`add_mlir_interface(FooOpInterface)` emits operation-interface declaration and
+definition fragments. `add_mlir_type_interface(FooTypeInterface)` does the same
+for a type interface. Attribute interfaces and specialized interface forms use
+the corresponding `mlir_tablegen` generators followed by a dialect or generic
+TableGen target.
+
+The library implementing an interface lists its generation target in
+`DEPENDS`. A consumer links that interface library when its public or private
+C++ interface uses the generated declarations.
+
+### Passes
+
+Pass declarations use `-gen-pass-decls`; C API fragments may be emitted from
+the same `.td` file:
+
+~~~cmake
+set(LLVM_TARGET_DEFINITIONS Passes.td)
+mlir_tablegen(Passes.h.inc -gen-pass-decls -name Foo)
+mlir_tablegen(Passes.capi.h.inc -gen-pass-capi-header --prefix Foo)
+mlir_tablegen(Passes.capi.cpp.inc -gen-pass-capi-impl --prefix Foo)
+add_mlir_dialect_tablegen_target(MLIRFooPassIncGen)
+~~~
+
+The library that defines or publishes these passes keeps
+`MLIRFooPassIncGen` in `DEPENDS`. A header-only consumer uses the logical pass
+library through `HEADER_LIBS` only when linking it would be incorrect.
+
+### PDLL and generated documentation
+
+Use `add_mlir_pdll_library` to compile a PDLL source and make its generated
+output available to another target. Use `add_mlir_doc` for generated dialect,
+operation, type, attribute, interface, or pass documentation. Documentation
+targets are collected under the `mlir-doc` aggregate and are not compilation
+prerequisites unless a source target explicitly consumes their output.
+
+## Libraries
+
+`add_mlir_library` is the base helper for MLIR libraries:
+
+~~~cmake
+add_mlir_library(MLIRFooTransforms
+  FooTransforms.cpp
+
+  ADDITIONAL_HEADER_DIRS
+  ${MLIR_MAIN_INCLUDE_DIR}/mlir/Dialect/Foo
+
+  DEPENDS
+  MLIRFooTransformsIncGen
+
+  LINK_COMPONENTS
+  Support
+
+  LINK_LIBS PUBLIC
+  MLIRFooDialect
+  MLIRPass
+  )
+~~~
+
+`LINK_COMPONENTS` names LLVM components. `LINK_LIBS` names CMake or MLIR
+library targets. Keeping them separate allows LLVM and MLIR to substitute their
+monolithic shared libraries correctly.
+
+Frequently used options include:
+
+| Option | Purpose |
+| --- | --- |
+| `SHARED` or `OBJECT` | Select a non-default library form |
+| `INSTALL_WITH_TOOLCHAIN` | Install with the toolchain distribution |
+| `EXCLUDE_FROM_LIBMLIR` | Exclude the library from monolithic MLIR |
+| `DISABLE_INSTALL` | Omit standard installation rules |
+| `ENABLE_AGGREGATION` | Make objects available to an MLIR aggregate |
+| `STANDALONE` | Do not add the implicit `LLVMSupport` dependency |
+| `ADDITIONAL_HEADERS` | Associate individual headers with the target |
+| `ADDITIONAL_HEADER_DIRS` | Add public headers to IDE source groups |
+| `DEPENDS` | Add the library's non-library build prerequisites |
+| `HEADER_LIBS` | Order after generated headers without linking |
+
+Prefer the wrapper describing a library's role:
+
+| Helper | Additional behavior |
+| --- | --- |
+| `add_mlir_dialect_library` | Records a target in `MLIR_DIALECT_LIBS` |
+| `add_mlir_conversion_library` | Records it in `MLIR_CONVERSION_LIBS` |
+| `add_mlir_extension_library` | Records it in `MLIR_EXTENSION_LIBS` |
+| `add_mlir_translation_library` | Records it in `MLIR_TRANSLATION_LIBS` |
+| `add_mlir_example_library` | Applies the conventions to examples |
+| `add_mlir_public_c_api_library` | Creates an aggregatable C API library |
+
+The global categories support tools and aggregates that intentionally collect
+an entire class of libraries. Ordinary libraries should list only their actual
+dependencies.
+
+### Link visibility
+
+Choose `LINK_LIBS` visibility from the C++ interface:
+
+| Declaration | Meaning |
+| --- | --- |
+| `PUBLIC A` | The target and its consumers use `A` |
+| `PRIVATE A` | Only the target implementation uses `A` |
+| `INTERFACE A` | Only consumers use `A` |
+
+Unqualified entries retain CMake's legacy signature behavior. New code should
+use explicit visibility when the distinction matters.
+
+Use `mlir_target_link_libraries` when adding links after an MLIR target was
+created, particularly for a library excluded from `libMLIR`. It applies
+`MLIR_LINK_MLIR_DYLIB` substitution and also records the generated-header
+ordering needed by an object-backed library.
+
+### Generated-header dependencies
+
+Three rules cover generated headers:
+
+1. A library lists its own TableGen and generated-source targets in `DEPENDS`.
+2. `LINK_LIBS` orders compilation after the generated-header targets reachable
+   through the linked library's interface. PUBLIC, PRIVATE, and unqualified
+   direct links are considered; INTERFACE-only links are not used by the
+   current target's compilation.
+3. `HEADER_LIBS` names a library whose generated header is included without a
+   link relationship.
+
+`HEADER_LIBS` is a rare layering escape hatch. It is appropriate when linking
+the provider would be semantically wrong or would create a circular link
+relationship. Keep it a flat list of literal target names and document the
+source-level include that requires every entry:
+
+~~~cmake
+# FooAnalysis.cpp includes mlir/Dialect/Bar/IR/BarOps.h.
+HEADER_LIBS
+MLIRBarDialect
+~~~
+
+Aliases and forward declarations are supported. Imported library targets are
+accepted because their installed generated headers already exist. Misspelled
+targets, generator expressions, executables, and utility targets are diagnosed
+at configure time. Nested and cyclic `HEADER_LIBS` relationships are safe: the
+resolver adds only generated-header leaf targets, never provider libraries.
+
+Libraries outside `add_mlir_library` do not accept `HEADER_LIBS`. A non-MLIR
+header-only consumer should retain the relevant generator in `DEPENDS`.
+
+### How ordering is modeled
+
+`add_public_tablegen_target` marks generated-header utility targets. Each
+`llvm_add_library` invocation records its exact `DEPENDS` list separately from
+the cumulative `LLVM_COMMON_DEPENDS` directory state. After the full target
+graph exists, a deferred traversal:
+
+* resolves aliases and forward references;
+* follows direct implementation links and transitive link interfaces;
+* follows nested `HEADER_LIBS` relationships;
+* conservatively extracts targets from link generator expressions; and
+* creates a build-local interface target for each header provider.
+
+These interface targets depend only on marked generators and link to other
+header interfaces. CMake handles transitive ordering and cycles in that graph.
+Consumers reference the header interfaces through their implementation links,
+without publishing them in exported link interfaces. Provider objects and
+archives are never added by this mechanism, so mutually linked static libraries
+do not form strong build cycles. Disabled generator-expression arms may
+generate extra headers. Imported libraries, paths, and flags require no
+build-tree ordering.
+
+This target-level modeling works with all supported CMake generators. Ninja's
+weaker ordering still completes a dependency's custom commands before compiling
+the consumer, while generators with stronger target ordering see only the leaf
+generator targets and do not wait for provider archives.
+
+## C API libraries and aggregation
+
+`add_mlir_public_c_api_library` creates an object-enabled MLIR library with the
+visibility definitions needed by the C API. Use `add_mlir_aggregate` to build a
+shared or static library from such components. `EMBED_LIBS` contribute their
+objects; `PUBLIC_LIBS` remain normal exported link dependencies.
+
+Aggregation metadata is exported only for libraries created with
+`ENABLE_AGGREGATION`. `MLIR_INSTALL_AGGREGATE_OBJECTS` controls whether the
+object libraries needed by an out-of-tree aggregate are installed. Imported
+components must have been installed with compatible aggregation metadata.
+
+## Tools, installation, and exports
+
+Use `add_mlir_tool` for MLIR command-line tools. It delegates to LLVM's tool
+infrastructure and participates in the normal runtime and installation layout.
+
+`add_mlir_library` installs and exports its target by default.
+`add_mlir_library_install` exposes the same rules for a non-standard library
+construction path. `DISABLE_INSTALL` suppresses those rules, while
+`INSTALL_WITH_TOOLCHAIN` includes the library when only the toolchain component
+is installed.
+
+Installed packages contain generated headers already, so imported targets do
+not contribute build-tree generator prerequisites. Exported logical library
+interfaces, rather than private `*IncGen` target names, are the contract for
+standalone consumers.
+
+## Validating dependency changes
+
+Generated-header correctness must be tested from empty generated-header state.
+An incremental build can leave files behind, and a broad aggregate can generate
+a missing header incidentally before its consumer compiles.
+
+Configure a fresh Ninja build and first build representative leaf libraries at
+normal parallelism:
+
+~~~shell
+cmake -S llvm -B <build> -G Ninja <configuration options>
+cmake --build <build> --target \
+  MLIRArmNeonDialect MLIRLinalgDialect --parallel 32
+~~~
+
+Then build the broader graph to populate compiler dependency files. Only after
+compilation succeeds, run:
+
+~~~shell
+ninja -C <build> -t missingdeps
+~~~
+
+`missingdeps` compares generated-file producers with include relationships in
+compiler depfiles. Running it before compilation cannot discover those
+includes. The clean leaf build and populated-depfile audit cover different
+failure modes.
+
+When changing the CMake infrastructure, also configure the focused CMake tests
+with Ninja and Unix Makefiles, test the oldest supported CMake release, and
+configure `mlir/examples/standalone` against an installed or build-tree MLIR
+package. Inspect generated object-order rules to confirm they contain generator
+targets and not provider archives.
+
+## Common mistakes
+
+* Depending on another library's `MLIRFooOpsIncGen` instead of linking
+  `MLIRFooDialect`.
+* Using `HEADER_LIBS` where an ordinary link accurately models the C++ layer.
+* Putting generator expressions or visibility keywords in `HEADER_LIBS`.
+* Moving dialect-specific generators into `mlir-generic-headers` to hide a
+  missing logical dependency.
+* Repairing a leaf race with the global `mlir-headers` aggregate without first
+  identifying the owning library.
+* Running `missingdeps` before compiler depfiles have been populated.
+* Validating only an incremental or broad aggregate build.
diff --git a/mlir/docs/DefiningDialects/AttributesAndTypes.md b/mlir/docs/DefiningDialects/AttributesAndTypes.md
index a339a763bd607..076b44b58f29c 100644
--- a/mlir/docs/DefiningDialects/AttributesAndTypes.md
+++ b/mlir/docs/DefiningDialects/AttributesAndTypes.md
@@ -192,7 +192,7 @@ mlir_tablegen(<Your Dialect>AttrDefs.h.inc -gen-attrdef-decls
               -attrdefs-dialect=<Your Dialect>)
 mlir_tablegen(<Your Dialect>AttrDefs.cpp.inc -gen-attrdef-defs 
               -attrdefs-dialect=<Your Dialect>)
-add_public_tablegen_target(<Your Dialect>AttrDefsIncGen)
+add_mlir_dialect_tablegen_target(<Your Dialect>AttrDefsIncGen)
 ```
 
 The generated `<Your Dialect>AttrDefs.h.inc` will need to be included whereever
diff --git a/mlir/lib/Conversion/SPIRVCommon/CMakeLists.txt b/mlir/lib/Conversion/SPIRVCommon/CMakeLists.txt
index cd5a4c225efbf..7e2c426428361 100644
--- a/mlir/lib/Conversion/SPIRVCommon/CMakeLists.txt
+++ b/mlir/lib/Conversion/SPIRVCommon/CMakeLists.txt
@@ -3,4 +3,8 @@ add_mlir_conversion_library(MLIRSPIRVAttrToLLVMConversion
 
   DEPENDS
   MLIRSPIRVEnumsIncGen
+
+  # AttrToLLVMConverter.h includes the generated SPIRVEnums.h.
+  HEADER_LIBS
+  MLIRSPIRVDialect
 )
diff --git a/mlir/lib/Dialect/Arith/IR/CMakeLists.txt b/mlir/lib/Dialect/Arith/IR/CMakeLists.txt
index 3423e11a7d0f0..dae42285d02b0 100644
--- a/mlir/lib/Dialect/Arith/IR/CMakeLists.txt
+++ b/mlir/lib/Dialect/Arith/IR/CMakeLists.txt
@@ -23,6 +23,10 @@ add_mlir_dialect_library(MLIRArithDialect
   MLIRArithOpsIncGen
   MLIRArithOpsInterfacesIncGen
 
+  # ArithDialect.cpp includes generated Bufferization interfaces.
+  HEADER_LIBS
+  MLIRBufferizationDialect
+
   LINK_LIBS PUBLIC
   MLIRCastInterfaces
   MLIRDialect
diff --git a/mlir/lib/Dialect/Func/IR/CMakeLists.txt b/mlir/lib/Dialect/Func/IR/CMakeLists.txt
index 329301c6fbafd..f23335800c9be 100644
--- a/mlir/lib/Dialect/Func/IR/CMakeLists.txt
+++ b/mlir/lib/Dialect/Func/IR/CMakeLists.txt
@@ -7,6 +7,10 @@ add_mlir_dialect_library(MLIRFuncDialect
   DEPENDS
   MLIRFuncOpsIncGen
 
+  # FuncOps.cpp includes generated Bufferization interfaces.
+  HEADER_LIBS
+  MLIRBufferizationDialect
+
   LINK_LIBS PUBLIC
   MLIRCallInterfaces
   MLIRControlFlowInterfaces
diff --git a/mlir/lib/Dialect/LLVMIR/CMakeLists.txt b/mlir/lib/Dialect/LLVMIR/CMakeLists.txt
index 50351fde8128f..df935dd681aee 100644
--- a/mlir/lib/Dialect/LLVMIR/CMakeLists.txt
+++ b/mlir/lib/Dialect/LLVMIR/CMakeLists.txt
@@ -49,6 +49,10 @@ add_mlir_dialect_library(MLIRLLVMDialect
   BinaryFormat
   Core
 
+  # LLVMAttrs.cpp includes mlir/Dialect/Ptr/IR/PtrEnums.h.
+  HEADER_LIBS
+  MLIRPtrDialect
+
   LINK_LIBS PUBLIC
   MLIRCallInterfaces
   MLIRControlFlowInterfaces
@@ -121,6 +125,8 @@ add_mlir_dialect_library(MLIRROCDLDialect
   ${MLIR_MAIN_INCLUDE_DIR}/mlir/Dialect/LLVMIR
 
   DEPENDS
+  # TODO: Migrate this IncGen dependency to HEADER_LIBS after refactoring
+  # MLIRGPUCompilationInterfaces into a separate link target.
   MLIRGPUCompilationAttrInterfacesIncGen
   MLIRROCDLOpsIncGen
   MLIRROCDLOpsShardGen
@@ -165,6 +171,8 @@ add_mlir_dialect_library(MLIRXeVMDialect
   ${MLIR_MAIN_INCLUDE_DIR}/mlir/Dialect/LLVMIR
 
   DEPENDS
+  # TODO: Migrate this IncGen dependency to HEADER_LIBS after refactoring
+  # MLIRGPUCompilationInterfaces into a separate link target.
   MLIRGPUCompilationAttrInterfacesIncGen
   MLIRXeVMOpsIncGen
   MLIRXeVMConversionsIncGen
diff --git a/mlir/lib/Dialect/Linalg/IR/CMakeLists.txt b/mlir/lib/Dialect/Linalg/IR/CMakeLists.txt
index fcba43785b6fd..960981d0e6f80 100644
--- a/mlir/lib/Dialect/Linalg/IR/CMakeLists.txt
+++ b/mlir/lib/Dialect/Linalg/IR/CMakeLists.txt
@@ -17,6 +17,10 @@ add_mlir_dialect_library(MLIRLinalgDialect
   MLIRShardingInterfaceIncGen
   MLIRRelayoutOpInterfaceIncGen
 
+  # LinalgDialect.cpp includes ShardingInterface.h for promised interfaces.
+  HEADER_LIBS
+  MLIRShardingInterface
+
   LINK_LIBS PUBLIC
   MLIRAffineDialect
   MLIRArithDialect
diff --git a/mlir/lib/Dialect/Shape/Transforms/CMakeLists.txt b/mlir/lib/Dialect/Shape/Transforms/CMakeLists.txt
index a51c6780c2866..89755a175495e 100644
--- a/mlir/lib/Dialect/Shape/Transforms/CMakeLists.txt
+++ b/mlir/lib/Dialect/Shape/Transforms/CMakeLists.txt
@@ -9,10 +9,8 @@ add_mlir_dialect_library(MLIRShapeOpsTransforms
 
   DEPENDS
   MLIRShapeTransformsIncGen
-  )
 
-target_link_libraries(MLIRShapeOpsTransforms
-  PUBLIC
+  LINK_LIBS PUBLIC
   MLIRArithDialect
   MLIRBufferizationDialect
   MLIRBufferizationTransforms
diff --git a/mlir/lib/Dialect/Tensor/IR/CMakeLists.txt b/mlir/lib/Dialect/Tensor/IR/CMakeLists.txt
index 66b73f9bead5d..493c2cd0efeb1 100644
--- a/mlir/lib/Dialect/Tensor/IR/CMakeLists.txt
+++ b/mlir/lib/Dialect/Tensor/IR/CMakeLists.txt
@@ -17,6 +17,10 @@ add_mlir_dialect_library(MLIRTensorDialect
   DEPENDS
   MLIRTensorOpsIncGen
 
+  # TensorDialect.cpp includes TransformInterfaces.h.
+  HEADER_LIBS
+  MLIRTransformDialectInterfaces
+
   LINK_LIBS PUBLIC
   MLIRAffineDialect
   MLIRArithDialect
diff --git a/mlir/lib/Dialect/Tosa/CMakeLists.txt b/mlir/lib/Dialect/Tosa/CMakeLists.txt
index 6404ca8fc5a56..42bfc0837d2c3 100644
--- a/mlir/lib/Dialect/Tosa/CMakeLists.txt
+++ b/mlir/lib/Dialect/Tosa/CMakeLists.txt
@@ -16,6 +16,10 @@ add_mlir_dialect_library(MLIRTosaDialect
   MLIRTosaEnumsIncGen
   MLIRShardingInterfaceIncGen
 
+  # TosaOps.cpp includes ShardingInterface.h for promised interfaces.
+  HEADER_LIBS
+  MLIRShardingInterface
+
   LINK_LIBS PUBLIC
   MLIRIR
   MLIRDialect
diff --git a/mlir/lib/Rewrite/CMakeLists.txt b/mlir/lib/Rewrite/CMakeLists.txt
index 15b3739e4c633..eefd3808c1409 100644
--- a/mlir/lib/Rewrite/CMakeLists.txt
+++ b/mlir/lib/Rewrite/CMakeLists.txt
@@ -1,5 +1,10 @@
 set(LLVM_OPTIONAL_SOURCES ByteCode.cpp)
 
+set(pdl_header_libraries)
+if(MLIR_ENABLE_PDL_IN_PATTERNMATCH)
+  set(pdl_header_libraries MLIRPDLInterpDialect)
+endif()
+
 add_mlir_library(MLIRRewrite
   FrozenRewritePatternSet.cpp
   PatternApplicator.cpp
@@ -11,6 +16,11 @@ add_mlir_library(MLIRRewrite
   mlir-generic-headers
   MLIRConversionPassIncGen
 
+  # FrozenRewritePatternSet.cpp includes PDLInterp and PDL when enabled.
+  # PDLInterp's public interface also provides the PDL generated headers.
+  HEADER_LIBS
+  ${pdl_header_libraries}
+
   LINK_LIBS PUBLIC
   MLIRIR
   MLIRSideEffectInterfaces
@@ -40,4 +50,3 @@ if(MLIR_ENABLE_PDL_IN_PATTERNMATCH)
     MLIRPDLToPDLInterp
     MLIRRewritePDL)
 endif()
-
diff --git a/mlir/lib/Target/SPIRV/CMakeLists.txt b/mlir/lib/Target/SPIRV/CMakeLists.txt
index 1785cf92a9334..b65854e72ac31 100644
--- a/mlir/lib/Target/SPIRV/CMakeLists.txt
+++ b/mlir/lib/Target/SPIRV/CMakeLists.txt
@@ -10,6 +10,10 @@ set(LLVM_OPTIONAL_SOURCES
 add_mlir_translation_library(MLIRSPIRVBinaryUtils
   SPIRVBinaryUtils.cpp
 
+  # SPIRVBinaryUtils.h includes the generated SPIRVEnums.h.
+  HEADER_LIBS
+  MLIRSPIRVDialect
+
   LINK_LIBS PUBLIC
   MLIRIR
   MLIRSupport
diff --git a/mlir/test/CMake/header-dependencies.test b/mlir/test/CMake/header-dependencies.test
new file mode 100644
index 0000000000000..cc1115b95e165
--- /dev/null
+++ b/mlir/test/CMake/header-dependencies.test
@@ -0,0 +1,192 @@
+# RUN: rm -rf %t
+# RUN: split-file %s %t
+# RUN: "%cmake_exe" -S %t/valid -B %t/valid-build -G "%cmake_generator" \
+# RUN:   -DMLIR_DIR=%mlir_cmake_dir -DCMAKE_C_COMPILER=%host_cc \
+# RUN:   -DCMAKE_CXX_COMPILER=%host_cxx 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=VALID
+# RUN: "%cmake_exe" --build %t/valid-build --target obj.HeaderConsumer --parallel 4
+# RUN: "%cmake_exe" --build %t/valid-build --target clean
+# RUN: "%cmake_exe" --build %t/valid-build --target obj.LinkConsumer --parallel 4
+# RUN: "%cmake_exe" --build %t/valid-build --target clean
+# RUN: "%cmake_exe" --build %t/valid-build --target SharedHeaderConsumer --parallel 4
+# RUN: not "%cmake_exe" -S %t/invalid -B %t/invalid-build \
+# RUN:   -G "%cmake_generator" -DMLIR_DIR=%mlir_cmake_dir \
+# RUN:   -DCMAKE_C_COMPILER=%host_cc -DCMAKE_CXX_COMPILER=%host_cxx 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=INVALID
+
+# VALID: -- Generated header dependency checks passed
+# INVALID-DAG: BadConsumer: HEADER_LIBS 'MisspelledProvider' is not a target
+# INVALID-DAG: BadConsumer: HEADER_LIBS 'ProviderIncGen' must name a library, not UTILITY
+# INVALID-DAG: BadConsumer: HEADER_LIBS 'NotALibrary' must name a library, not EXECUTABLE
+# INVALID-DAG: BadConsumer: HEADER_LIBS '$<$<BOOL:1>:Provider>' is not a literal target
+
+#--- common.cmake
+find_package(MLIR REQUIRED CONFIG)
+list(APPEND CMAKE_MODULE_PATH "${MLIR_CMAKE_DIR}" "${LLVM_CMAKE_DIR}")
+include(TableGen)
+include(AddLLVM)
+include(AddMLIR)
+include(HandleLLVMOptions)
+set(LLVM_RUNTIME_OUTPUT_INTDIR ${CMAKE_BINARY_DIR}/bin)
+set(LLVM_LIBRARY_OUTPUT_INTDIR ${CMAKE_BINARY_DIR}/lib)
+
+foreach(target
+    Provider Base Conditional LinkOnly Disabled Plain InterfaceOnly
+    CycleA CycleB HeaderA HeaderB HeaderBase 3rdParty)
+  set(LLVM_COMMON_DEPENDS)
+  set(output ${CMAKE_CURRENT_BINARY_DIR}/${target}.h.inc)
+  add_custom_command(OUTPUT ${output}
+    COMMAND ${CMAKE_COMMAND} -E touch ${output})
+  set(TABLEGEN_OUTPUT ${output})
+  add_public_tablegen_target(${target}IncGen)
+endforeach()
+add_custom_target(SourceGenerator COMMAND ${CMAKE_COMMAND} -E false)
+# Keep the fixture libraries from inheriting all preceding TableGen targets.
+set(LLVM_COMMON_DEPENDS)
+
+#--- valid/CMakeLists.txt
+cmake_minimum_required(VERSION 3.20)
+project(HeaderDependencies LANGUAGES C CXX)
+include(../common.cmake)
+
+add_library(Imported UNKNOWN IMPORTED)
+set_target_properties(Imported PROPERTIES
+  IMPORTED_LOCATION /absolute/libimported.a)
+add_library(ImportedAlias ALIAS Imported)
+
+# Consumers precede providers to exercise deferred forward resolution.
+add_mlir_library(LinkConsumer empty.cpp DISABLE_INSTALL
+  LINK_LIBS PlainProvider /absolute/libunused.a -lpthread ImportedAlias
+  PRIVATE ProviderAlias "$<$<BOOL:0>:DisabledProvider>"
+  PUBLIC "$<$<BOOL:1>:ConditionalProvider>" "$<LINK_ONLY:LinkOnlyProvider>"
+    "$<$<BOOL:1>:3rdPartyProvider>"
+  INTERFACE InterfaceOnlyProvider)
+add_mlir_library(CycleConsumer empty.cpp DISABLE_INSTALL
+  LINK_LIBS PRIVATE CycleA)
+add_mlir_library(HeaderConsumer empty.cpp DISABLE_INSTALL
+  HEADER_LIBS HeaderAlias)
+add_mlir_library(PostHocConsumer empty.cpp DISABLE_INSTALL)
+add_mlir_library(PostHocInterfaceConsumer empty.cpp DISABLE_INSTALL)
+add_mlir_library(SharedHeaderConsumer empty.cpp SHARED STANDALONE DISABLE_INSTALL
+  HEADER_LIBS HeaderAlias)
+target_include_directories(SharedHeaderConsumer PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
+target_compile_definitions(SharedHeaderConsumer PRIVATE CHECK_HEADER_HEADERS)
+target_include_directories(obj.HeaderConsumer PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
+target_compile_definitions(obj.HeaderConsumer PRIVATE CHECK_HEADER_HEADERS)
+target_include_directories(obj.LinkConsumer PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
+target_compile_definitions(obj.LinkConsumer PRIVATE CHECK_LINK_HEADERS)
+
+add_mlir_library(Provider empty.cpp DISABLE_INSTALL
+  DEPENDS ProviderIncGen SourceGenerator
+  LINK_LIBS PUBLIC Base)
+add_library(ProviderAlias ALIAS Provider)
+add_mlir_library(Base empty.cpp DISABLE_INSTALL DEPENDS BaseIncGen)
+add_mlir_library(ConditionalProvider empty.cpp DISABLE_INSTALL
+  DEPENDS ConditionalIncGen)
+add_mlir_library(LinkOnlyProvider empty.cpp DISABLE_INSTALL
+  DEPENDS LinkOnlyIncGen)
+add_mlir_library(DisabledProvider empty.cpp DISABLE_INSTALL
+  DEPENDS DisabledIncGen)
+add_mlir_library(PlainProvider empty.cpp DISABLE_INSTALL
+  DEPENDS PlainIncGen)
+add_mlir_library(InterfaceOnlyProvider empty.cpp DISABLE_INSTALL
+  DEPENDS InterfaceOnlyIncGen)
+add_mlir_library(3rdPartyProvider empty.cpp DISABLE_INSTALL
+  DEPENDS 3rdPartyIncGen)
+
+# Mutual static-library links must not become strong target cycles.
+add_mlir_library(CycleA empty.cpp DISABLE_INSTALL
+  DEPENDS CycleAIncGen LINK_LIBS PRIVATE CycleB)
+add_mlir_library(CycleB empty.cpp DISABLE_INSTALL
+  DEPENDS CycleBIncGen LINK_LIBS CycleA)
+
+# HEADER_LIBS follows nested and link relationships while tolerating cycles.
+add_mlir_library(HeaderA empty.cpp DISABLE_INSTALL
+  DEPENDS HeaderAIncGen
+  HEADER_LIBS HeaderB
+  LINK_LIBS PUBLIC HeaderBase)
+add_library(HeaderAlias ALIAS HeaderA)
+add_mlir_library(HeaderB empty.cpp DISABLE_INSTALL
+  DEPENDS HeaderBIncGen HEADER_LIBS HeaderA)
+add_mlir_library(HeaderBase empty.cpp DISABLE_INSTALL
+  DEPENDS HeaderBaseIncGen)
+
+mlir_target_link_libraries(PostHocConsumer PRIVATE ProviderAlias
+  INTERFACE InterfaceOnlyProvider)
+mlir_target_link_libraries(PostHocInterfaceConsumer INTERFACE InterfaceOnlyProvider)
+add_subdirectory(local-providers)
+
+function(expect_header_libraries target expected)
+  get_target_property(actual ${target} LINK_LIBRARIES)
+  list(FILTER actual INCLUDE REGEX "^llvm\\.headers\\.")
+  list(SORT actual)
+  list(SORT expected)
+  if(NOT "${actual}" STREQUAL "${expected}")
+    message(FATAL_ERROR
+      "${target}: expected dependencies '${expected}', got '${actual}'")
+  endif()
+endfunction()
+
+function(check_dependencies)
+  # Record direct header interfaces only. The subsequent clean object builds
+  # verify that CMake follows their transitive and cyclic dependencies.
+  expect_header_libraries(obj.LinkConsumer
+    "llvm.headers.3rdPartyProvider;llvm.headers.ConditionalProvider;llvm.headers.DisabledProvider;llvm.headers.LinkOnlyProvider;llvm.headers.PlainProvider;llvm.headers.Provider")
+  expect_header_libraries(obj.CycleConsumer "llvm.headers.CycleA")
+  expect_header_libraries(obj.HeaderConsumer "llvm.headers.HeaderA")
+  expect_header_libraries(HeaderConsumer "llvm.headers.HeaderA")
+  expect_header_libraries(obj.PostHocConsumer "llvm.headers.Provider")
+  expect_header_libraries(obj.PostHocInterfaceConsumer "")
+  expect_header_libraries(obj.LocalHeaderConsumer "")
+  expect_header_libraries(SharedHeaderConsumer "llvm.headers.HeaderA")
+  message(STATUS "Generated header dependency checks passed")
+endfunction()
+# The dependency resolvers were deferred first and therefore run before this.
+cmake_language(DEFER CALL check_dependencies)
+
+#--- valid/empty.cpp
+#ifdef CHECK_HEADER_HEADERS
+#include "HeaderA.h.inc"
+#include "HeaderB.h.inc"
+#include "HeaderBase.h.inc"
+#endif
+#ifdef CHECK_LINK_HEADERS
+#include "Provider.h.inc"
+#include "Base.h.inc"
+#include "Conditional.h.inc"
+#include "LinkOnly.h.inc"
+#include "Disabled.h.inc"
+#include "Plain.h.inc"
+#include "3rdParty.h.inc"
+#endif
+void empty() {}
+
+#--- valid/Provider.td
+// Only configuration is under test.
+
+#--- valid/local-providers/CMakeLists.txt
+# Non-global imported targets and aliases are visible only in this directory.
+add_mlir_library(LocalHeaderConsumer empty.cpp DISABLE_INSTALL
+  HEADER_LIBS LocalImported LocalImportedAlias)
+add_library(LocalImported INTERFACE IMPORTED)
+add_library(LocalImportedAlias ALIAS LocalImported)
+
+#--- valid/local-providers/empty.cpp
+void localEmpty() {}
+
+#--- invalid/CMakeLists.txt
+cmake_minimum_required(VERSION 3.20)
+project(HeaderDependenciesInvalid LANGUAGES C CXX)
+include(../common.cmake)
+add_executable(NotALibrary empty.cpp)
+add_mlir_library(Provider empty.cpp DISABLE_INSTALL
+  DEPENDS ProviderIncGen)
+add_mlir_library(BadConsumer empty.cpp DISABLE_INSTALL
+  HEADER_LIBS MisspelledProvider ProviderIncGen NotALibrary
+  "$<$<BOOL:1>:Provider>")
+
+#--- invalid/empty.cpp
+void empty() {}
+
+#--- invalid/Provider.td
+// Only configuration is under test.
diff --git a/mlir/test/CMake/lit.local.cfg b/mlir/test/CMake/lit.local.cfg
new file mode 100644
index 0000000000000..af315fd5da836
--- /dev/null
+++ b/mlir/test/CMake/lit.local.cfg
@@ -0,0 +1,7 @@
+if (config.host_cmake_generator == "Xcode" or
+        config.host_cmake_generator.startswith("Visual Studio")):
+    config.unsupported = True
+
+config.substitutions.append(("%cmake_exe", config.host_cmake))
+config.substitutions.append(("%cmake_generator", config.host_cmake_generator))
+config.substitutions.append(("%mlir_cmake_dir", config.mlir_cmake_dir))



More information about the llvm-branch-commits mailing list