[modulemap] Add -fno-implicit-module-map-file= to exclude from implicit discovery#208191
[modulemap] Add -fno-implicit-module-map-file= to exclude from implicit discovery#208191torarnv wants to merge 1 commit into
Conversation
|
@llvm/pr-subscribers-clang-modules @llvm/pr-subscribers-clang Author: Tor Arne Vestbø (torarnv) Changes…mplicit discovery Add a driver/cc1 flag that excludes a specific module map file from implicit module map discovery (-fimplicit-module-maps), without disabling implicit discovery globally. The excluded map can still be loaded explicitly via -fmodule-map-file=. The exclusion is applied in HeaderSearch::lookupModuleMapFile(), the single entry point for implicit discovery, so all callers (directory scanning and framework inference) honor it. Matching is done by the underlying FileEntry rather than by string, making it robust to path spelling, symlinks, and '.'/'..' components. This lets a project ship a standard-named module.modulemap for consumers that opt in to modules (e.g. Swift's ClangImporter, which relies on implicit discovery) while preventing unrelated C++ consumers from implicitly modularizing the same headers. The party that must opt out (the C++ consumer) is the one that can inject the flag, and the shipped module map stays standard syntax. Full diff: https://github.com/llvm/llvm-project/pull/208191.diff 6 Files Affected:
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index b7142ecb072ff..8e69c9b600c15 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -416,6 +416,13 @@ latest release, please see the [Clang Web Site](https://clang.llvm.org) or the
reproducable builds. These macros can be redefined from the command line if
necessary. `/d1nodatetime-` can be used to turn this feature off if
necessary to override the common build settings.
+- New option `-fno-implicit-module-map-file=<file>` added to exclude a specific
+ module map from *implicit* module map discovery (`-fimplicit-module-maps`),
+ without disabling implicit discovery globally. The excluded map can still be
+ loaded explicitly via `-fmodule-map-file=`. This lets a project ship a
+ standard-named `module.modulemap` for consumers that opt in (e.g. Swift's
+ ClangImporter) while preventing unrelated C++ consumers from implicitly
+ modularizing the same headers.
### Deprecated Compiler Flags
diff --git a/clang/include/clang/Lex/HeaderSearch.h b/clang/include/clang/Lex/HeaderSearch.h
index 962787cc48a17..5ad8facb60b73 100644
--- a/clang/include/clang/Lex/HeaderSearch.h
+++ b/clang/include/clang/Lex/HeaderSearch.h
@@ -21,6 +21,7 @@
#include "clang/Lex/ModuleMap.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/MapVector.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringMap.h"
@@ -30,6 +31,7 @@
#include <cassert>
#include <cstddef>
#include <memory>
+#include <optional>
#include <string>
#include <utility>
#include <vector>
@@ -277,6 +279,11 @@ class HeaderSearch {
/// a system header.
std::vector<std::pair<std::string, bool>> SystemHeaderPrefixes;
+ /// Lazily-computed set of module map files, resolved to their underlying
+ /// \c FileEntry, that should be excluded from implicit module map discovery.
+ /// Derived from \c HeaderSearchOptions::ExcludedImplicitModuleMapFiles.
+ std::optional<llvm::DenseSet<const FileEntry *>> ExcludedImplicitModuleMaps;
+
/// The context hash used in SpecificModuleCachePath (unless suppressed).
std::string ContextHash;
@@ -731,6 +738,10 @@ class HeaderSearch {
OptionalFileEntryRef lookupModuleMapFile(DirectoryEntryRef Dir,
bool IsFramework);
+ /// Determine whether the given module map file has been excluded from
+ /// implicit module map discovery via \c -fno-implicit-module-map-file=.
+ bool isModuleMapFileExcludedFromImplicitDiscovery(FileEntryRef File);
+
/// Determine whether there is a module map that may map the header
/// with the given file name to a (sub)module.
/// Always returns false if modules are disabled.
diff --git a/clang/include/clang/Lex/HeaderSearchOptions.h b/clang/include/clang/Lex/HeaderSearchOptions.h
index fd46ea223bae1..2c7bf65451879 100644
--- a/clang/include/clang/Lex/HeaderSearchOptions.h
+++ b/clang/include/clang/Lex/HeaderSearchOptions.h
@@ -187,6 +187,12 @@ class HeaderSearchOptions {
/// The set of user-provided virtual filesystem overlay files.
std::vector<std::string> VFSOverlayFiles;
+ /// Module map files that should be excluded from implicit module map
+ /// discovery (i.e. the ones that would otherwise be found by name while
+ /// walking header search directories). They can still be loaded explicitly
+ /// via \c -fmodule-map-file=.
+ std::vector<std::string> ExcludedImplicitModuleMapFiles;
+
/// Include the compiler builtin includes.
LLVM_PREFERRED_TYPE(bool)
unsigned UseBuiltinIncludes : 1;
diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td
index 35b0390bd2fc0..3d8e76c6c18f7 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -3802,6 +3802,13 @@ def fmodule_map_file : Joined<["-"], "fmodule-map-file=">,
MetaVarName<"<file>">,
HelpText<"Load this module map file">,
MarshallingInfoStringVector<FrontendOpts<"ModuleMapFiles">>;
+def fno_implicit_module_map_file : Joined<["-"], "fno-implicit-module-map-file=">,
+ Group<f_Group>,
+ Visibility<[ClangOption, CC1Option, CLOption]>,
+ MetaVarName<"<file>">,
+ HelpText<"Exclude this module map file from implicit module map discovery. "
+ "It can still be loaded explicitly via -fmodule-map-file=.">,
+ MarshallingInfoStringVector<HeaderSearchOpts<"ExcludedImplicitModuleMapFiles">>;
def fmodule_file : Joined<["-"], "fmodule-file=">,
Group<i_Group>,
Visibility<[ClangOption, CC1Option, CLOption]>,
diff --git a/clang/lib/Lex/HeaderSearch.cpp b/clang/lib/Lex/HeaderSearch.cpp
index ecd80db10f2bd..8abec291de1ae 100644
--- a/clang/lib/Lex/HeaderSearch.cpp
+++ b/clang/lib/Lex/HeaderSearch.cpp
@@ -2171,10 +2171,39 @@ HeaderSearch::parseModuleMapFileImpl(FileEntryRef File, bool IsSystem,
return MMR_NewlyProcessed;
}
+bool HeaderSearch::isModuleMapFileExcludedFromImplicitDiscovery(
+ FileEntryRef File) {
+ if (HSOpts.ExcludedImplicitModuleMapFiles.empty())
+ return false;
+
+ // Resolve the excluded paths to their underlying files once, so that
+ // matching is robust to path spelling (relative vs absolute, symlinks, ...)
+ // rather than a brittle string comparison.
+ if (!ExcludedImplicitModuleMaps) {
+ ExcludedImplicitModuleMaps.emplace();
+ for (StringRef Path : HSOpts.ExcludedImplicitModuleMapFiles)
+ if (auto Excluded = FileMgr.getOptionalFileRef(Path))
+ ExcludedImplicitModuleMaps->insert(&Excluded->getFileEntry());
+ }
+
+ return ExcludedImplicitModuleMaps->contains(&File.getFileEntry());
+}
+
OptionalFileEntryRef
HeaderSearch::lookupModuleMapFile(DirectoryEntryRef Dir, bool IsFramework) {
if (!HSOpts.ImplicitModuleMaps)
return std::nullopt;
+
+ // Return the given implicitly-discovered module map file unless it has been
+ // excluded via -fno-implicit-module-map-file=, in which case discovery
+ // continues as if the file were not present.
+ auto ReturnUnlessExcluded =
+ [&](FileEntryRef F) -> OptionalFileEntryRef {
+ if (isModuleMapFileExcludedFromImplicitDiscovery(F))
+ return std::nullopt;
+ return F;
+ };
+
// For frameworks, the preferred spelling is Modules/module.modulemap, but
// module.map at the framework root is also accepted.
SmallString<128> ModuleMapFileName(Dir.getName());
@@ -2182,12 +2211,14 @@ HeaderSearch::lookupModuleMapFile(DirectoryEntryRef Dir, bool IsFramework) {
llvm::sys::path::append(ModuleMapFileName, "Modules");
llvm::sys::path::append(ModuleMapFileName, "module.modulemap");
if (auto F = FileMgr.getOptionalFileRef(ModuleMapFileName))
- return *F;
+ return ReturnUnlessExcluded(*F);
// Continue to allow module.map, but warn it's deprecated.
ModuleMapFileName = Dir.getName();
llvm::sys::path::append(ModuleMapFileName, "module.map");
if (auto F = FileMgr.getOptionalFileRef(ModuleMapFileName)) {
+ if (isModuleMapFileExcludedFromImplicitDiscovery(*F))
+ return std::nullopt;
Diags.Report(diag::warn_deprecated_module_dot_map)
<< ModuleMapFileName << 0 << IsFramework;
return *F;
@@ -2200,7 +2231,7 @@ HeaderSearch::lookupModuleMapFile(DirectoryEntryRef Dir, bool IsFramework) {
llvm::sys::path::append(ModuleMapFileName, "Modules",
"module.private.modulemap");
if (auto F = FileMgr.getOptionalFileRef(ModuleMapFileName))
- return *F;
+ return ReturnUnlessExcluded(*F);
}
return std::nullopt;
}
diff --git a/clang/test/Modules/no-implicit-module-map-file.c b/clang/test/Modules/no-implicit-module-map-file.c
new file mode 100644
index 0000000000000..5e91f51a3fc19
--- /dev/null
+++ b/clang/test/Modules/no-implicit-module-map-file.c
@@ -0,0 +1,67 @@
+// Test -fno-implicit-module-map-file=: exclude a specific module map from
+// *implicit* module-map discovery, without disabling implicit discovery
+// globally, and without affecting explicit -fmodule-map-file= loading or other
+// implicitly-discovered maps.
+
+// RUN: rm -rf %t
+// RUN: split-file %s %t
+
+// Baseline: the module map is discovered implicitly and the #include is
+// translated into an import of module 'Foo'.
+// RUN: %clang_cc1 -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN: -I %t/include -Rmodule-include-translation -verify=translated %t/use-foo.c
+
+// Excluded: the same map is skipped for implicit discovery, so the #include
+// stays textual (no translation) and compiles as an ordinary header.
+// RUN: %clang_cc1 -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN: -I %t/include -Rmodule-include-translation \
+// RUN: -fno-implicit-module-map-file=%t/include/module.modulemap \
+// RUN: -verify=textual %t/use-foo.c
+
+// Path robustness: a different spelling of the same file (extra "/./" and a
+// "/../") still matches, because matching is by underlying file, not string.
+// RUN: %clang_cc1 -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN: -I %t/include -Rmodule-include-translation \
+// RUN: -fno-implicit-module-map-file=%t/include/./sub/../module.modulemap \
+// RUN: -verify=textual %t/use-foo.c
+
+// Explicit still works: even though the map is on the exclusion list, loading
+// it explicitly via -fmodule-map-file= makes 'Foo' modular again, so the
+// include is translated.
+// RUN: %clang_cc1 -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN: -I %t/include -Rmodule-include-translation \
+// RUN: -fno-implicit-module-map-file=%t/include/module.modulemap \
+// RUN: -fmodule-map-file=%t/include/module.modulemap \
+// RUN: -verify=translated %t/use-foo.c
+
+// Other maps unaffected: excluding Foo's map does not stop a different,
+// non-excluded map from being discovered and used.
+// RUN: %clang_cc1 -fmodules -fimplicit-module-maps -fmodules-cache-path=%t/cache \
+// RUN: -I %t/include -I %t/other -Rmodule-include-translation \
+// RUN: -fno-implicit-module-map-file=%t/include/module.modulemap \
+// RUN: -verify=other %t/use-bar.c
+
+//--- include/module.modulemap
+module Foo { header "foo.h" export * }
+
+//--- include/foo.h
+void foo(void);
+
+//--- include/sub/.keep
+
+//--- other/module.modulemap
+module Bar { header "bar.h" export * }
+
+//--- other/bar.h
+void bar(void);
+
+//--- use-foo.c
+// translated-remark@+2 {{treating #include as an import of module 'Foo'}}
+// textual-no-diagnostics
+#include "foo.h"
+void test(void) { foo(); }
+
+//--- use-bar.c
+// other-remark@+1 {{treating #include as an import of module 'Bar'}}
+#include "bar.h"
+void test(void) { bar(); }
|
|
✅ With the latest revision this PR passed the C/C++ code formatter. |
…mplicit discovery Add a driver/cc1 flag that excludes a specific module map file from *implicit* module map discovery (-fimplicit-module-maps), without disabling implicit discovery globally. The excluded map can still be loaded explicitly via -fmodule-map-file=. The exclusion is applied in HeaderSearch::lookupModuleMapFile(), the single entry point for implicit discovery, so all callers (directory scanning and framework inference) honor it. Matching is done by the underlying FileEntry rather than by string, making it robust to path spelling, symlinks, and '.'/'..' components. This lets a project ship a standard-named module.modulemap for consumers that opt in to modules (e.g. Swift's ClangImporter, which relies on implicit discovery) while preventing unrelated C++ consumers from implicitly modularizing the same headers. The party that must opt out (the C++ consumer) is the one that can inject the flag, and the shipped module map stays standard syntax.
8106ad4 to
e81b0d4
Compare
Add a driver/cc1 flag that excludes a specific module map file from implicit module map discovery (-fimplicit-module-maps), without disabling implicit discovery globally. The excluded map can still be loaded explicitly via -fmodule-map-file=.
The exclusion is applied in HeaderSearch::lookupModuleMapFile(), the single entry point for implicit discovery, so all callers (directory scanning and framework inference) honor it. Matching is done by the underlying FileEntry rather than by string, making it robust to path spelling, symlinks, and '.'/'..' components.
This lets a project ship a standard-named module.modulemap for consumers that opt in to modules (e.g. Swift's ClangImporter, which relies on implicit discovery) while preventing unrelated C++ consumers from implicitly modularizing the same headers. The party that must opt out (the C++ consumer) is the one that can inject the flag, and the shipped module map stays standard syntax.