https://github.com/cyndyishida created 
https://github.com/llvm/llvm-project/pull/229611

Before this patch, the `-fmodule-map-file`s on an explicitly-built module's 
command line came solely from what that module's implicitly-built PCM recorded 
as input files. Since that is an observed trace, it can omit a direct 
dependency's module map. For example, if a dependency was already registered by 
an earlier PCM read, a later `@import` can short-circuit & nothing enters its 
module map locally as an input file. However, that's not always guaranteed. 
Like when relocation checking is enabled, the direct dep's module map can end 
up being entered, resulting in mismatched invocations/hashes.

Instead, collect each direct dependency's module map, then let the trace 
contribute only what that didn't cover. The collection happens in the order of 
`MF.Imports`, which is stable across runs, this is important since modulemap 
order is baked into the explicit module's hash. After, collect any remaining 
module maps through the trace order, that is not guaranteed to be stable 
ordering but is so far not observable, and strictly less likely to occur now.

The module maps are deduped by their `FileEntry`, and reporting is skipped 
under EagerLoadModules.

This patch also adds more test coverage for order-sensitive modulemap 
command-line tracking.

>From 578e1136bec979b9515a06e4fe41dbd8f5cce497 Mon Sep 17 00:00:00 2001
From: Cyndy Ishida <[email protected]>
Date: Fri, 2 Oct 2026 08:49:46 -0700
Subject: [PATCH] [clang][deps] Report module map deps based on module
 dependencies, in import order

Before this patch, the `-fmodule-map-file`s on an explicitly-built module's 
command line came solely from what that module's implicitly-built PCM recorded 
as input files.
Since that is an observed trace, it can omit a direct dependency's module map. 
For example, if a dependency was already registered by an earlier PCM read, a 
later `@import` can short-circuit & nothing enters its module map locally as an 
input file. However, that's not always guaranteed. Like when relocation 
checking is enabled, the direct dep's module map can end up being entered, 
resulting in mismatching invocations/hashes.

Instead, collect each direct dependency's own module map first then let the 
trace contribute only what that did not cover. The collection happens in the 
order of `MF.Imports`, which is stable across runs, this is important since 
modulemap order is baked into the explicit module's hash. After, collect any 
remaining module maps through the trace order, that is not guaranteed to be 
stable ordering but is so far not observable, and strictly less likely to occur 
now.

The modulemaps are deduped by their `FileEntry`, and reporting is skipped under 
EagerLoadModules.

This patch also adds more test coverage for order sensitive modulemap command 
line tracking.
---
 .../DependencyScanning/ModuleDepCollector.h   |  17 ++-
 .../DependencyScanning/ModuleDepCollector.cpp | 127 ++++++++++--------
 ...modules-direct-dep-modulemap-commandline.m |  62 +++++++++
 .../ClangScanDeps/modules-extern-unrelated.m  |   1 +
 ...dules-module-map-parse-order-commandline.m |  55 ++++++++
 5 files changed, 201 insertions(+), 61 deletions(-)
 create mode 100644 
clang/test/ClangScanDeps/modules-direct-dep-modulemap-commandline.m
 create mode 100644 
clang/test/ClangScanDeps/modules-module-map-parse-order-commandline.m

diff --git a/clang/include/clang/DependencyScanning/ModuleDepCollector.h 
b/clang/include/clang/DependencyScanning/ModuleDepCollector.h
index 6e896bd26f763a7..b5708198e43e947 100644
--- a/clang/include/clang/DependencyScanning/ModuleDepCollector.h
+++ b/clang/include/clang/DependencyScanning/ModuleDepCollector.h
@@ -180,13 +180,18 @@ class ModuleDepCollector final : public 
DependencyCollector {
       const ModuleDeps &Deps,
       llvm::function_ref<void(CowCompilerInvocation &)> Optimize) const;
 
-  /// Collect module map files for given modules.
-  llvm::DenseSet<const FileEntry *>
-  collectModuleMapFiles(ArrayRef<ModuleID> ClangModuleDeps) const;
+  /// Collect the module maps for the module dependencies that are named with
+  /// -fmodule-file.
+  ///
+  /// \param ModuleMapFiles The list of module map files to append for the
+  /// invocation, if needed.
+  /// \param SeenModuleMapFiles The uniqued set of module maps accounted for.
+  /// \param ClangModuleDeps The module dependencies whose module maps to add.
+  void collectModuleMapFiles(
+      std::vector<std::string> &ModuleMapFiles,
+      llvm::SmallDenseSet<const FileEntry *, 16> &SeenModuleMapFiles,
+      ArrayRef<ModuleID> ClangModuleDeps) const;
 
-  /// Add module map files to the invocation, if needed.
-  void addModuleMapFiles(CompilerInvocation &CI,
-                         ArrayRef<ModuleID> ClangModuleDeps) const;
   /// Add module files (pcm) to the invocation, if needed.
   void addModuleFiles(CompilerInvocation &CI,
                       ArrayRef<ModuleID> ClangModuleDeps) const;
diff --git a/clang/lib/DependencyScanning/ModuleDepCollector.cpp 
b/clang/lib/DependencyScanning/ModuleDepCollector.cpp
index 5b38e5105bc315d..36ff99464b70d4b 100644
--- a/clang/lib/DependencyScanning/ModuleDepCollector.cpp
+++ b/clang/lib/DependencyScanning/ModuleDepCollector.cpp
@@ -324,38 +324,12 @@ 
ModuleDepCollector::getInvocationAdjustedForModuleBuildWithoutOutputs(
   CI.getMutFrontendOpts().Inputs.emplace_back(Deps.ClangModuleMapFile,
                                               ModuleMapInputKind);
 
-  auto CurrentModuleMapEntry =
-      
ScanInstance.getFileManager().getOptionalFileRef(Deps.ClangModuleMapFile);
-  assert(CurrentModuleMapEntry && "module map file entry not found");
-
   // Remove directly passed modulemap files. They will get added back if they
   // were actually used.
   CI.getMutFrontendOpts().ModuleMapFiles.clear();
 
-  auto DepModuleMapFiles = collectModuleMapFiles(Deps.ClangModuleDeps);
-  for (StringRef ModuleMapFile : Deps.ModuleMapFileDeps) {
-    // TODO: Track these as `FileEntryRef` to simplify the equality check 
below.
-    auto ModuleMapEntry =
-        ScanInstance.getFileManager().getOptionalFileRef(ModuleMapFile);
-    assert(ModuleMapEntry && "module map file entry not found");
-
-    // Don't report module maps describing eagerly-loaded dependency. This
-    // information will be deserialized from the PCM.
-    // TODO: Verify this works fine when modulemap for module A is eagerly
-    // loaded from A.pcm, and module map passed on the command line contains
-    // definition of a submodule: "explicit module A.Private { ... }".
-    if (Service.getOpts().EagerLoadModules &&
-        DepModuleMapFiles.contains(*ModuleMapEntry))
-      continue;
-
-    // Don't report module map file of the current module unless it also
-    // describes a dependency (for symmetry).
-    if (*ModuleMapEntry == *CurrentModuleMapEntry &&
-        !DepModuleMapFiles.contains(*ModuleMapEntry))
-      continue;
-
+  for (const std::string &ModuleMapFile : Deps.ModuleMapFileDeps)
     CI.getMutFrontendOpts().ModuleMapFiles.emplace_back(ModuleMapFile);
-  }
 
   // Report the prebuilt modules this module uses.
   for (const auto &PrebuiltModule : Deps.PrebuiltModuleDeps)
@@ -378,9 +352,28 @@ 
ModuleDepCollector::getInvocationAdjustedForModuleBuildWithoutOutputs(
   return CI;
 }
 
-llvm::DenseSet<const FileEntry *> ModuleDepCollector::collectModuleMapFiles(
+static void
+collectModuleMap(std::vector<std::string> &ModuleMapFiles,
+                 llvm::SmallDenseSet<const FileEntry *, 16> 
&SeenModuleMapFiles,
+                 FileEntryRef ModuleMapFile, StringRef Path,
+                 const bool Report) {
+  if (!SeenModuleMapFiles.insert(ModuleMapFile).second)
+    return;
+  // Conditionalize reporting for modes like eager module loading.
+  // That mode will have this information deserialized from the PCM instead.
+  // TODO: Verify this works fine when modulemap for module A is eagerly
+  // loaded from A.pcm, and module map passed on the command line contains
+  // definition of a submodule: "explicit module A.Private { ... }".
+  if (Report)
+    ModuleMapFiles.emplace_back(Path);
+}
+
+void ModuleDepCollector::collectModuleMapFiles(
+    std::vector<std::string> &ModuleMapFiles,
+    llvm::SmallDenseSet<const FileEntry *, 16> &SeenModuleMapFiles,
     ArrayRef<ModuleID> ClangModuleDeps) const {
-  llvm::DenseSet<const FileEntry *> ModuleMapFiles;
+  // Add each dependency's module map, in the order the PCM recorded its
+  // imports. This is expected to be a stable order across runs and workers.
   for (const ModuleID &MID : ClangModuleDeps) {
     ModuleDeps *MD = ModuleDepsByID.lookup(MID);
     assert(MD && "Inconsistent dependency info");
@@ -388,20 +381,12 @@ llvm::DenseSet<const FileEntry *> 
ModuleDepCollector::collectModuleMapFiles(
     auto FE = ScanInstance.getFileManager().getOptionalFileRef(
         MD->ClangModuleMapFile);
     assert(FE && "Missing module map file that was previously found");
-    ModuleMapFiles.insert(*FE);
-  }
-  return ModuleMapFiles;
-}
-
-void ModuleDepCollector::addModuleMapFiles(
-    CompilerInvocation &CI, ArrayRef<ModuleID> ClangModuleDeps) const {
-  if (Service.getOpts().EagerLoadModules)
-    return; // Only pcm is needed for eager load.
+    if (!FE)
+      continue;
 
-  for (const ModuleID &MID : ClangModuleDeps) {
-    ModuleDeps *MD = ModuleDepsByID.lookup(MID);
-    assert(MD && "Inconsistent dependency info");
-    CI.getFrontendOpts().ModuleMapFiles.push_back(MD->ClangModuleMapFile);
+    collectModuleMap(ModuleMapFiles, SeenModuleMapFiles, *FE,
+                     MD->ClangModuleMapFile,
+                     /*Report=*/!Service.getOpts().EagerLoadModules);
   }
 }
 
@@ -454,26 +439,29 @@ void 
ModuleDepCollector::applyDiscoveredDependencies(CompilerInvocation &CI) {
 
   if (llvm::any_of(CI.getFrontendOpts().Inputs, needsModules)) {
     Preprocessor &PP = ScanInstance.getPreprocessor();
-    if (Module *CurrentModule = PP.getCurrentModuleImplementation())
-      if (OptionalFileEntryRef CurrentModuleMap =
-              PP.getHeaderSearchInfo()
-                  .getModuleMap()
-                  .getModuleMapFileForUniquing(CurrentModule))
-        CI.getFrontendOpts().ModuleMapFiles.emplace_back(
-            CurrentModuleMap->getNameAsRequested());
 
     SmallVector<ModuleID> DirectDeps;
     for (const auto &KV : ModularDeps)
       if (DirectModularDeps.contains(KV.first))
         DirectDeps.push_back(KV.second->ID);
 
-    // TODO: Report module maps the same way it's done for modular 
dependencies.
-    addModuleMapFiles(CI, DirectDeps);
-
     addModuleFiles(CI, DirectDeps);
 
     for (const auto &KV : DirectPrebuiltModularDeps)
       CI.getFrontendOpts().ModuleFiles.push_back(KV.second.PCMFile);
+
+    llvm::SmallDenseSet<const FileEntry *, 16> SeenModuleMapFiles;
+    const ModuleMap &ModMapInfo = PP.getHeaderSearchInfo().getModuleMap();
+    if (Module *CurrentModule = PP.getCurrentModuleImplementation())
+      if (OptionalFileEntryRef CurrentModuleMap =
+              ModMapInfo.getModuleMapFileForUniquing(CurrentModule))
+        collectModuleMap(CI.getFrontendOpts().ModuleMapFiles,
+                         SeenModuleMapFiles, *CurrentModuleMap,
+                         CurrentModuleMap->getNameAsRequested(),
+                         /*Report=*/true);
+
+    collectModuleMapFiles(CI.getFrontendOpts().ModuleMapFiles,
+                          SeenModuleMapFiles, DirectDeps);
   }
 }
 
@@ -762,8 +750,10 @@ 
ModuleDepCollector::handleTopLevelModule(serialization::ModuleFile *MF) {
   ModuleMap &ModMapInfo =
       MDC.ScanInstance.getPreprocessor().getHeaderSearchInfo().getModuleMap();
 
-  if (auto ModuleMap = ModMapInfo.getModuleMapFileForUniquing(M)) {
-    SmallString<128> Path = ModuleMap->getNameAsRequested();
+  OptionalFileEntryRef CurrentModuleMapEntry =
+      ModMapInfo.getModuleMapFileForUniquing(M);
+  if (CurrentModuleMapEntry) {
+    SmallString<128> Path = CurrentModuleMapEntry->getNameAsRequested();
     ModMapInfo.canonicalizeModuleMapPath(Path);
     MD.ClangModuleMapFile = std::string(Path);
   }
@@ -786,6 +776,11 @@ 
ModuleDepCollector::handleTopLevelModule(serialization::ModuleFile *MF) {
 
   addAllModuleDeps(*MF, MD);
 
+  // Capture module map file dependencies.
+  llvm::SmallDenseSet<const FileEntry *, 16> SeenModuleMapFiles;
+  MDC.collectModuleMapFiles(MD.ModuleMapFileDeps, SeenModuleMapFiles,
+                            MD.ClangModuleDeps);
+
   SmallString<0> PathBuf;
   PathBuf.reserve(256);
   MDC.ScanInstance.getASTReader()->visitInputFileInfos(
@@ -805,7 +800,29 @@ 
ModuleDepCollector::handleTopLevelModule(serialization::ModuleFile *MF) {
         auto ResolvedFilenameAsRequested = ASTReader::ResolveImportedPath(
             PathBuf, IFI.UnresolvedImportedFilenameAsRequested,
             MF->BaseDirectory);
-        MD.ModuleMapFileDeps.emplace_back(*ResolvedFilenameAsRequested);
+
+        // TODO: Track these as `FileEntryRef` to simplify the equality check
+        // below.
+        auto ModuleMapEntry =
+            MDC.ScanInstance.getFileManager().getOptionalFileRef(
+                *ResolvedFilenameAsRequested);
+        assert(ModuleMapEntry && "module map file entry not found");
+        if (!ModuleMapEntry)
+          return;
+
+        // Don't report module map file of the current module unless it also
+        // describes a dependency in ModuleMapFileDeps (for symmetry).
+        if (CurrentModuleMapEntry && *ModuleMapEntry == *CurrentModuleMapEntry)
+          return;
+
+        // Add additional modulemaps for input files the module
+        // dependencies did not already cover. e.g. A modulemap that only
+        // declares a submodule is recorded only as an input file.
+        // FIXME: It is not guaranteed to be in a stable order across runs or
+        // workers.
+        collectModuleMap(MD.ModuleMapFileDeps, SeenModuleMapFiles,
+                         *ModuleMapEntry, *ResolvedFilenameAsRequested,
+                         /*Report=*/true);
       });
 
   bool IgnoreCWD = false;
diff --git 
a/clang/test/ClangScanDeps/modules-direct-dep-modulemap-commandline.m 
b/clang/test/ClangScanDeps/modules-direct-dep-modulemap-commandline.m
new file mode 100644
index 000000000000000..f10b6007160d1cf
--- /dev/null
+++ b/clang/test/ClangScanDeps/modules-direct-dep-modulemap-commandline.m
@@ -0,0 +1,62 @@
+// Check an explicit module's command line names the module map of every
+// dependency it is given with -fmodule-file.
+
+// In this example, EntryPoint imports DirectOnly first, and loading 
DirectOnly's
+//  PCM populates DirectAndTransitive into memory.
+// Then when EntryPoint imports DirectAndTransitive (through @import) itself, 
the
+//  module is already known, so the lookup short-circuits and never searches
+//  DirectAndTransitive's module map in EntryPoint's CompilerInstance as an 
input file.
+// The scan should still register DirectAndTransitive's modulemap as a
+//  direct dependency to ensure the resulting explicit module invocation is 
stable
+//  as additional invalidation or relocation checks can cause EntryPoint's
+//  CompilerInstance to observe that modulemap as an input file dependency.
+
+// RUN: rm -rf %t
+// RUN: split-file %s %t
+
+// RUN: clang-scan-deps -format experimental-full -- \
+// RUN:   %clang -fmodules -fimplicit-module-maps 
-fmodules-cache-path=%t/cache \
+// RUN:     -I %t/entrypoint -I %t/directonly -I %t/directandtransitive -c 
%t/tu.m -o %t/tu.o \
+// RUN:   > %t/result.json
+
+// RUN: %deps-to-rsp %t/result.json --module-name=EntryPoint > 
%t/EntryPoint.rsp
+// RUN: cat %t/EntryPoint.rsp | sed 's:\\\\\?:/:g' | FileCheck %s -DPREFIX=%/t
+
+// CHECK: -fmodule-map-file=[[PREFIX]]/directonly/module.modulemap
+// CHECK: -fmodule-map-file=[[PREFIX]]/directandtransitive/module.modulemap
+// CHECK: -fmodule-file=DirectAndTransitive={{[^"]*}}.pcm
+// CHECK: -fmodule-file=DirectOnly={{[^"]*}}.pcm
+
+// Verify round-trip compilation passes.
+// RUN: %deps-to-rsp %t/result.json --module-name=DirectAndTransitive > 
%t/DirectAndTransitive.rsp
+// RUN: %deps-to-rsp %t/result.json --module-name=DirectOnly > 
%t/DirectOnly.rsp
+// RUN: %deps-to-rsp %t/result.json --tu-index=0 > %t/tu.rsp
+// RUN: %clang @%t/DirectAndTransitive.rsp
+// RUN: %clang @%t/DirectOnly.rsp
+// RUN: %clang @%t/EntryPoint.rsp
+// RUN: %clang @%t/tu.rsp
+
+//--- directandtransitive/module.modulemap
+module DirectAndTransitive { header "direct-and-transitive.h" export * }
+
+//--- directandtransitive/direct-and-transitive.h
+int directAndTransitive(void);
+
+//--- directonly/module.modulemap
+module DirectOnly { header "direct-only.h" export * }
+
+//--- directonly/direct-only.h
+#include "direct-and-transitive.h"
+int directOnly(void);
+
+//--- entrypoint/module.modulemap
+module EntryPoint { header "entry-point.h" export * }
+
+//--- entrypoint/entry-point.h
+#include "direct-only.h"
+@import DirectAndTransitive;
+int entryPoint(void);
+
+//--- tu.m
+#include "entry-point.h"
+int tu(void) { return entryPoint(); }
diff --git a/clang/test/ClangScanDeps/modules-extern-unrelated.m 
b/clang/test/ClangScanDeps/modules-extern-unrelated.m
index 061d0912d086a1d..c0e6fd1f4b2f368 100644
--- a/clang/test/ClangScanDeps/modules-extern-unrelated.m
+++ b/clang/test/ClangScanDeps/modules-extern-unrelated.m
@@ -92,6 +92,7 @@
 // CHECK-NEXT:       "clang-modulemap-file": 
"[[PREFIX]]/zeroth/module.modulemap",
 // CHECK-NEXT:       "command-line": [
 // CHECK-NOT:          "-fmodule-map-file=[[PREFIX]]/second/module.modulemap"
+// CHECK:              "-fmodule-map-file=[[PREFIX]]/second/second.modulemap"
 // CHECK:            ],
 // CHECK-NEXT:       "context-hash": "{{.*}}",
 // CHECK-NEXT:       "file-deps": [
diff --git 
a/clang/test/ClangScanDeps/modules-module-map-parse-order-commandline.m 
b/clang/test/ClangScanDeps/modules-module-map-parse-order-commandline.m
new file mode 100644
index 000000000000000..d7f5c27d5e8f14d
--- /dev/null
+++ b/clang/test/ClangScanDeps/modules-module-map-parse-order-commandline.m
@@ -0,0 +1,55 @@
+// Check that an explicit module's command line orders dependency module maps
+// in the valid parse order.
+
+// In this example, Parent and Parent.Sub are declared in separate directories.
+// For Client's explicit module, the top level Parent module must come before
+// parsing Parent.Sub.
+// The required ordering is the opposite of a lexicographical sort of those
+// directories.
+
+// RUN: rm -rf %t
+// RUN: split-file %s %t
+
+// RUN: clang-scan-deps -format experimental-full -- \
+// RUN:   %clang -fmodules -fimplicit-module-maps 
-fmodules-cache-path=%t/cache \
+// RUN:     -I %t/client -I %t/zzz -I %t/aaa -c %t/tu.m -o %t/tu.o \
+// RUN:   > %t/result.json
+
+// The map declaring Parent must still be named before the one declaring
+// Parent.Sub.
+// RUN: %deps-to-rsp %t/result.json --module-name=Client > %t/Client.rsp
+// RUN: cat %t/Client.rsp | sed 's:\\\\\?:/:g' | FileCheck %s -DPREFIX=%/t
+
+// CHECK:      -fmodule-map-file=[[PREFIX]]/zzz/module.modulemap
+// CHECK-SAME: -fmodule-map-file=[[PREFIX]]/aaa/module.modulemap
+
+// Verify round-trip compilation passes.
+// RUN: %deps-to-rsp %t/result.json --module-name=Parent > %t/Parent.rsp
+// RUN: %deps-to-rsp %t/result.json --tu-index=0 > %t/tu.rsp
+// RUN: %clang @%t/Parent.rsp
+// RUN: %clang @%t/Client.rsp
+// RUN: %clang @%t/tu.rsp
+
+//--- zzz/module.modulemap
+module Parent { header "parent.h" }
+
+//--- zzz/parent.h
+
+//--- aaa/module.modulemap
+explicit module Parent.Sub {
+  header "sub.h"
+  exclude header "sub-excluded.h"
+}
+
+//--- aaa/sub.h
+//--- aaa/sub-excluded.h
+
+//--- client/module.modulemap
+module Client { header "client.h" }
+
+//--- client/client.h
+#include "parent.h"
+#include "sub-excluded.h"
+
+//--- tu.m
+@import Client;

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to