https://github.com/Bigcheese created https://github.com/llvm/llvm-project/pull/230713
Adding a header to a directory that is searched before the one an include previously resolved in changes the build without changing any file the previous build depended on. This is the same class of negative dependency as adding a header to an umbrella directory, so report these directories the same way. Only the translation unit reports them, because the search paths it uses are pruned from the command lines of the modules it imports, and many translation units share one module. Because of this, invalidation of these search paths is handled by checking once per compiler instance instead of per module load. This is off by default and enabled in clang-scan-deps via `-track-search-directories`. Assisted-by: Claude Code: opus-5.5 >From 78d84ec74926e6f29fed7db85e781553b024ff0e Mon Sep 17 00:00:00 2001 From: Michael Spencer <[email protected]> Date: Wed, 7 Oct 2026 23:17:28 -0700 Subject: [PATCH] [clang][deps] Report search directories as directory dependencies Adding a header to a directory that is searched before the one an include previously resolved in changes the build without changing any file the previous build depended on. This is the same class of negative dependency as adding a header to an umbrella directory, so report these directories the same way. Only the translation unit reports them, because the search paths it uses are pruned from the command lines of the modules it imports, and many translation units share one module. Because of this, invalidation of these search paths is handled by checking once per compiler instance instead of per module load. This is off by default and enabled in clang-scan-deps via `-track-search-directories`. Assisted-by: Claude Code: opus-5.5 --- clang/docs/ReleaseNotes.md | 5 ++ clang/include/clang/Basic/FileManager.h | 6 ++ .../DependencyScanning/DependencyConsumer.h | 2 + .../DependencyScanning/DependencyGraph.h | 7 ++ .../DependencyScanningService.h | 8 ++ .../DependencyScanningUtils.h | 5 ++ clang/include/clang/Lex/HeaderSearch.h | 5 ++ clang/include/clang/Lex/HeaderSearchOptions.h | 8 ++ clang/include/clang/Serialization/ASTReader.h | 13 +++ clang/lib/Basic/FileManager.cpp | 12 +++ .../DependencyScanningUtils.cpp | 1 + .../DependencyScanningWorker.cpp | 2 + .../DependencyScanning/ModuleDepCollector.cpp | 7 ++ clang/lib/Lex/HeaderSearch.cpp | 23 +++++ clang/lib/Lex/ModuleMap.cpp | 13 +-- clang/lib/Serialization/ASTReader.cpp | 57 ++++++++++++ clang/test/ClangScanDeps/tu-directory-deps.c | 44 ++++++++++ .../tu-search-directory-shadow.c | 87 +++++++++++++++++++ clang/tools/clang-scan-deps/ClangScanDeps.cpp | 12 +++ clang/tools/clang-scan-deps/Opts.td | 5 ++ 20 files changed, 313 insertions(+), 9 deletions(-) create mode 100644 clang/test/ClangScanDeps/tu-directory-deps.c create mode 100644 clang/test/ClangScanDeps/tu-search-directory-shadow.c diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md index 96d5e630e80dac..b2b3924bc127b8 100644 --- a/clang/docs/ReleaseNotes.md +++ b/clang/docs/ReleaseNotes.md @@ -273,6 +273,11 @@ features cannot lower the translation-unit ABI level; be detected by watching the directories or by comparing their modification times. +- Added `clang-scan-deps -track-search-directories`, which extends + `directory-deps` to the non-system directories searched for headers. Adding a + header there can shadow one found in a later directory, which changes the + build without changing any file it previously depended on. + ### New Compiler Flags - New option `-fmodules-validate-directory-dependencies` makes an implicitly diff --git a/clang/include/clang/Basic/FileManager.h b/clang/include/clang/Basic/FileManager.h index 1df52394fc93ce..3f122fcfa56ffe 100644 --- a/clang/include/clang/Basic/FileManager.h +++ b/clang/include/clang/Basic/FileManager.h @@ -310,6 +310,12 @@ class FileManager : public RefCountedBase<FileManager> { bool makeAbsolutePath(SmallVectorImpl<char> &Path, bool Canonicalize = false) const; + /// Append the directories the contents of \p Dir come from, as by + /// \c llvm::vfs::FileSystem::getDirectoryContentRealSources, made absolute + /// and with dots removed. + void getDirectoryContentRealSources(StringRef Dir, + SmallVectorImpl<std::string> &Out) const; + /// Retrieve the canonical name for a given directory. /// /// This is a very expensive operation, despite its results being cached, diff --git a/clang/include/clang/DependencyScanning/DependencyConsumer.h b/clang/include/clang/DependencyScanning/DependencyConsumer.h index 54152635a32537..d6bbf41a24602b 100644 --- a/clang/include/clang/DependencyScanning/DependencyConsumer.h +++ b/clang/include/clang/DependencyScanning/DependencyConsumer.h @@ -32,6 +32,8 @@ class DependencyConsumer { virtual void handleFileDependency(StringRef Filename) = 0; + virtual void handleDirectoryDependency(StringRef Dirname) {} + virtual void handlePrebuiltModuleDependency(PrebuiltModuleDep PMD) = 0; virtual void handleModuleDependency(ModuleDeps MD) = 0; diff --git a/clang/include/clang/DependencyScanning/DependencyGraph.h b/clang/include/clang/DependencyScanning/DependencyGraph.h index fc5ee5901be18f..b941a7651b8b86 100644 --- a/clang/include/clang/DependencyScanning/DependencyGraph.h +++ b/clang/include/clang/DependencyScanning/DependencyGraph.h @@ -175,6 +175,13 @@ struct TranslationUnitDeps { /// directly depends on, not including transitive dependencies. std::vector<std::string> FileDeps; + /// Absolute paths to directories this module depends on the listing of, + /// including their subdirectories. When one of those listings changes, for + /// example as seen by comparing directory modification times, report the + /// path as spelled here via \c DependencyScanningService::addInvalidatedPath + /// on the next scan. + std::vector<std::string> DirectoryDeps; + /// A collection of prebuilt modules this translation unit directly depends /// on, not including transitive dependencies. std::vector<PrebuiltModuleDep> PrebuiltModuleDeps; diff --git a/clang/include/clang/DependencyScanning/DependencyScanningService.h b/clang/include/clang/DependencyScanning/DependencyScanningService.h index d1fc26490423cd..75a4f05f11f62d 100644 --- a/clang/include/clang/DependencyScanning/DependencyScanningService.h +++ b/clang/include/clang/DependencyScanning/DependencyScanningService.h @@ -88,6 +88,10 @@ struct DependencyScanningServiceOptions { bool ReportVisibleModules = false; /// Whether the resulting command lines should load explicit PCMs eagerly. bool EagerLoadModules = false; + /// Whether to report the non-system directories searched for headers, and to + /// rebuild scanning modules when the build system reports one of them as + /// changed. Requires \c ValidateAgainstInvalidatedPaths. + bool TrackSearchDirectories = false; /// Whether to trace VFS accesses during the scan. bool TraceVFS = false; /// Whether to scan modules asynchronously. @@ -114,6 +118,10 @@ class DependencyScanningService { public: explicit DependencyScanningService(DependencyScanningServiceOptions Opts) : Opts(std::move(Opts)), Logger(this->Opts.LogPath) { + assert((!this->Opts.TrackSearchDirectories || + this->Opts.ValidateAgainstInvalidatedPaths) && + "search directories are only checked when validating against " + "invalidated paths"); ModCacheEntries.ValidateAgainstInvalidatedPaths = this->Opts.ValidateAgainstInvalidatedPaths; } diff --git a/clang/include/clang/DependencyScanning/DependencyScanningUtils.h b/clang/include/clang/DependencyScanning/DependencyScanningUtils.h index 952342d2a322d4..46492e916c7f76 100644 --- a/clang/include/clang/DependencyScanning/DependencyScanningUtils.h +++ b/clang/include/clang/DependencyScanning/DependencyScanningUtils.h @@ -35,6 +35,10 @@ class FullDependencyConsumer : public DependencyConsumer { Dependencies.push_back(std::string(File)); } + void handleDirectoryDependency(StringRef Dir) override { + DirectoryDeps.push_back(std::string(Dir)); + } + void handlePrebuiltModuleDependency(PrebuiltModuleDep PMD) override { PrebuiltModuleDeps.emplace_back(std::move(PMD)); } @@ -67,6 +71,7 @@ class FullDependencyConsumer : public DependencyConsumer { private: std::vector<std::string> Dependencies; + std::vector<std::string> DirectoryDeps; std::vector<PrebuiltModuleDep> PrebuiltModuleDeps; llvm::MapVector<ModuleID, ModuleDeps> ClangModuleDeps; std::string ModuleName; diff --git a/clang/include/clang/Lex/HeaderSearch.h b/clang/include/clang/Lex/HeaderSearch.h index adaa126024c3ca..92a0639bb877e6 100644 --- a/clang/include/clang/Lex/HeaderSearch.h +++ b/clang/include/clang/Lex/HeaderSearch.h @@ -1057,6 +1057,11 @@ class HeaderSearch { bool IsFramework); }; +/// The non-system directories \p HSOpts searches. Header maps are skipped. +std::vector<std::string> +getNonSystemSearchDirs(const HeaderSearchOptions &HSOpts, + const FileManager &FileMgr); + /// Apply the header search options to get given HeaderSearch object. void ApplyHeaderSearchOptions(HeaderSearch &HS, const HeaderSearchOptions &HSOpts, diff --git a/clang/include/clang/Lex/HeaderSearchOptions.h b/clang/include/clang/Lex/HeaderSearchOptions.h index a27af3278b8cff..51735e32bcea5f 100644 --- a/clang/include/clang/Lex/HeaderSearchOptions.h +++ b/clang/include/clang/Lex/HeaderSearchOptions.h @@ -285,6 +285,13 @@ class HeaderSearchOptions { LLVM_PREFERRED_TYPE(bool) unsigned ModulesIncludeVFSUsage : 1; + /// Whether validating the directory dependencies of a module also checks the + /// non-system directories searched for headers, which decide where its + /// includes resolved. Only sound with \c ModulesStrictContextHash, which + /// makes them the ones the module was built with. + LLVM_PREFERRED_TYPE(bool) + unsigned ModulesValidateSearchDirectories : 1; + /// Whether we should look for a module in module maps only in provided /// header search paths or if we are allowed to look for module maps in /// subdirectories of provided paths too. @@ -315,6 +322,7 @@ class HeaderSearchOptions { ModulesPruneNonAffectingModuleMaps(true), ModulesHashContent(false), ModulesSerializeOnlyPreprocessor(false), ModulesStrictContextHash(false), ModulesIncludeVFSUsage(false), + ModulesValidateSearchDirectories(false), AllowModuleMapSubdirectorySearch(true), LazyLoadModuleMaps(false) {} /// AddPath - Add the \p Path path to the specified \p Group list. diff --git a/clang/include/clang/Serialization/ASTReader.h b/clang/include/clang/Serialization/ASTReader.h index 275383d40cf808..fa122948da66e0 100644 --- a/clang/include/clang/Serialization/ASTReader.h +++ b/clang/include/clang/Serialization/ASTReader.h @@ -1585,6 +1585,19 @@ class ASTReader : public ExternalPreprocessorSource, const ModuleFile *ImportedBy, unsigned ClientLoadCapabilities); + /// The latest change to the search directories that every module is + /// validated against, and the directory it was in. + struct SearchDirectoriesChange { + time_t Time = 0; + std::string Dir; + /// Whether the build system reported \c Dir as changed, rather than its + /// modification time being \c Time. + bool Invalidated = false; + }; + /// Computed on first use. + std::optional<SearchDirectoriesChange> SearchDirsChange; + const SearchDirectoriesChange &getSearchDirectoriesChange(); + void ReadDirectoryDependencies(const RecordData &Record, ModuleFile &F); bool isDirectoryDependencyOutOfDate(ModuleFile &F, bool Complain); diff --git a/clang/lib/Basic/FileManager.cpp b/clang/lib/Basic/FileManager.cpp index fe80ffbebd2bfc..40b0e1b2d095e0 100644 --- a/clang/lib/Basic/FileManager.cpp +++ b/clang/lib/Basic/FileManager.cpp @@ -522,6 +522,18 @@ bool FileManager::makeAbsolutePath(SmallVectorImpl<char> &Path, return Changed; } +void FileManager::getDirectoryContentRealSources( + StringRef Dir, SmallVectorImpl<std::string> &Out) const { + size_t Begin = Out.size(); + FS->getDirectoryContentRealSources(Dir, Out); + for (std::string &Path : llvm::drop_begin(Out, Begin)) { + SmallString<256> Canonical(Path); + makeAbsolutePath(Canonical); + llvm::sys::path::remove_dots(Canonical, /*remove_dot_dot=*/true); + Path = std::string(Canonical); + } +} + void FileManager::fillRealPathName(FileEntry *UFE, llvm::StringRef FileName) { llvm::SmallString<128> AbsPath(FileName); // This is not the same as `VFS::getRealPath()`, which resolves symlinks diff --git a/clang/lib/DependencyScanning/DependencyScanningUtils.cpp b/clang/lib/DependencyScanning/DependencyScanningUtils.cpp index e27c597a14fccf..a08bb749d6ab38 100644 --- a/clang/lib/DependencyScanning/DependencyScanningUtils.cpp +++ b/clang/lib/DependencyScanning/DependencyScanningUtils.cpp @@ -18,6 +18,7 @@ TranslationUnitDeps FullDependencyConsumer::takeTranslationUnitDeps() { TU.ID.ModuleName = std::move(ModuleName); TU.NamedModuleDeps = std::move(NamedModuleDeps); TU.FileDeps = std::move(Dependencies); + TU.DirectoryDeps = std::move(DirectoryDeps); TU.PrebuiltModuleDeps = std::move(PrebuiltModuleDeps); TU.VisibleModules = std::move(VisibleModules); TU.Commands = std::move(Commands); diff --git a/clang/lib/DependencyScanning/DependencyScanningWorker.cpp b/clang/lib/DependencyScanning/DependencyScanningWorker.cpp index 8ab67e150bb5d1..2741c5b9910dcf 100644 --- a/clang/lib/DependencyScanning/DependencyScanningWorker.cpp +++ b/clang/lib/DependencyScanning/DependencyScanningWorker.cpp @@ -418,6 +418,8 @@ createScanCompilerInvocation(const CompilerInvocation &Invocation, ScanInvocation->getHeaderSearchOpts().ModuleFormat = "raw"; ScanInvocation->getHeaderSearchOpts().ModulesIncludeVFSUsage = any(Service.getOpts().OptimizeArgs & ScanningOptimizations::VFS); + ScanInvocation->getHeaderSearchOpts().ModulesValidateSearchDirectories = + Service.getOpts().TrackSearchDirectories; // Consider different header search and diagnostic options to create // different modules. This avoids the unsound aliasing of module PCMs. diff --git a/clang/lib/DependencyScanning/ModuleDepCollector.cpp b/clang/lib/DependencyScanning/ModuleDepCollector.cpp index 68905285267cac..dc0b00a0c2c7fb 100644 --- a/clang/lib/DependencyScanning/ModuleDepCollector.cpp +++ b/clang/lib/DependencyScanning/ModuleDepCollector.cpp @@ -714,6 +714,13 @@ void ModuleDepCollector::run(DependencyConsumer &Consumer) { for (auto &&I : MDC.FileDeps) Consumer.handleFileDependency(I); + // The translation unit depends on every non system serach directory. + if (MDC.Service.getOpts().TrackSearchDirectories) + for (const std::string &Dir : + getNonSystemSearchDirs(MDC.ScanInstance.getHeaderSearchOpts(), + MDC.ScanInstance.getFileManager())) + Consumer.handleDirectoryDependency(Dir); + for (auto &&I : MDC.DirectPrebuiltModularDeps) Consumer.handlePrebuiltModuleDependency(I.second); } diff --git a/clang/lib/Lex/HeaderSearch.cpp b/clang/lib/Lex/HeaderSearch.cpp index 906ce07b96a797..5e78334363fcff 100644 --- a/clang/lib/Lex/HeaderSearch.cpp +++ b/clang/lib/Lex/HeaderSearch.cpp @@ -80,6 +80,29 @@ HeaderFileInfo::getControllingMacro(ExternalPreprocessorSource *External) { ExternalHeaderFileInfoSource::~ExternalHeaderFileInfoSource() = default; +std::vector<std::string> +clang::getNonSystemSearchDirs(const HeaderSearchOptions &HSOpts, + const FileManager &FileMgr) { + std::vector<std::string> Dirs; + for (const HeaderSearchOptions::Entry &Entry : HSOpts.UserEntries) { + if (Entry.Group != frontend::Quoted && Entry.Group != frontend::Angled) + continue; + SmallString<256> Path(Entry.Path); + FileMgr.makeAbsolutePath(Path); + // A header map is a file. + llvm::ErrorOr<llvm::vfs::Status> Status = + FileMgr.getVirtualFileSystem().status(Path); + if (Status && !Status->isDirectory()) + continue; + SmallVector<std::string, 2> Paths; + FileMgr.getDirectoryContentRealSources(Path, Paths); + llvm::append_range(Dirs, Paths); + } + llvm::sort(Dirs); + Dirs.erase(llvm::unique(Dirs), Dirs.end()); + return Dirs; +} + HeaderSearch::HeaderSearch(const HeaderSearchOptions &HSOpts, SourceManager &SourceMgr, DiagnosticsEngine &Diags, const LangOptions &LangOpts, diff --git a/clang/lib/Lex/ModuleMap.cpp b/clang/lib/Lex/ModuleMap.cpp index 3187a23fe6c098..2ed197192c7b74 100644 --- a/clang/lib/Lex/ModuleMap.cpp +++ b/clang/lib/Lex/ModuleMap.cpp @@ -1352,15 +1352,10 @@ void ModuleMap::setUmbrellaHeaderAsWritten( } void ModuleMap::recordDirectoryDependencies(Module *Mod, StringRef Dir) { - FileManager &FileMgr = SourceMgr.getFileManager(); - SmallVector<std::string, 2> Sources; - FileMgr.getVirtualFileSystem().getDirectoryContentRealSources(Dir, Sources); - for (StringRef Source : Sources) { - SmallString<256> Canonical(Source); - FileMgr.makeAbsolutePath(Canonical); - llvm::sys::path::remove_dots(Canonical, /*remove_dot_dot=*/true); - Mod->addDirectoryDependency(Canonical); - } + SmallVector<std::string, 2> Paths; + SourceMgr.getFileManager().getDirectoryContentRealSources(Dir, Paths); + for (StringRef Path : Paths) + Mod->addDirectoryDependency(Path); } void ModuleMap::setUmbrellaDirAsWritten( diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp index 0a897a49d1165c..8d86aefb231929 100644 --- a/clang/lib/Serialization/ASTReader.cpp +++ b/clang/lib/Serialization/ASTReader.cpp @@ -4900,6 +4900,20 @@ bool ASTReader::isDirectoryDependencyOutOfDate(ModuleFile &F, bool Complain) { ModuleCache &ModCache = getModuleManager().getModuleCache(); const HeaderSearchOptions &HSOpts = PP.getHeaderSearchInfo().getHeaderSearchOpts(); + if (HSOpts.ModulesValidateSearchDirectories) { + const SearchDirectoriesChange &Change = getSearchDirectoriesChange(); + if (Change.Time > F.ModTime) { + Diag(Change.Invalidated ? diag::remark_module_path_invalidated + : diag::remark_module_directory_dep_changed) + << F.ModuleName << Change.Dir; + if (Complain) + Diag(Change.Invalidated ? diag::err_module_path_invalidated + : diag::err_module_directory_dep_changed) + << F.ModuleName << Change.Dir; + return true; + } + } + for (StringRef Dir : F.DirectoryDependencies) { if (std::optional<bool> Invalidated = ModCache.isDirectoryInvalidated(Dir)) { @@ -4938,6 +4952,49 @@ bool ASTReader::isDirectoryDependencyOutOfDate(ModuleFile &F, bool Complain) { return false; } +const ASTReader::SearchDirectoriesChange & +ASTReader::getSearchDirectoriesChange() { + if (SearchDirsChange) + return *SearchDirsChange; + + SearchDirectoriesChange &Change = SearchDirsChange.emplace(); + auto Update = [&](time_t Time, StringRef Dir, bool Invalidated) { + if (Time > Change.Time) + Change = {Time, Dir.str(), Invalidated}; + }; + + llvm::vfs::FileSystem &FS = PP.getFileManager().getVirtualFileSystem(); + auto UpdateFromModTime = [&](StringRef Dir) { + llvm::ErrorOr<llvm::vfs::Status> Status = FS.status(Dir); + if (Status && Status->isDirectory()) + Update(llvm::sys::toTimeT(Status->getLastModificationTime()), Dir, + /*Invalidated=*/false); + }; + + ModuleCache &ModCache = getModuleManager().getModuleCache(); + const HeaderSearchOptions &HSOpts = + PP.getHeaderSearchInfo().getHeaderSearchOpts(); + for (const std::string &Dir : + getNonSystemSearchDirs(HSOpts, PP.getFileManager())) { + if (std::optional<bool> Invalidated = + ModCache.isDirectoryInvalidated(Dir)) { + // As for a directory dependency, a module built during the build session + // is newer than a reported change. + if (*Invalidated) + Update(HSOpts.BuildSessionTimestamp, Dir, /*Invalidated=*/true); + continue; + } + // As for a directory dependency, the whole tree's listing matters. + UpdateFromModTime(Dir); + std::error_code EC; + for (llvm::vfs::recursive_directory_iterator I(FS, Dir, EC), E; + I != E && !EC; I.increment(EC)) + if (I->type() == llvm::sys::fs::file_type::directory_file) + UpdateFromModTime(I->path()); + } + return Change; +} + /// Move the given method to the back of the global list of methods. static void moveMethodToBackOfGlobalList(Sema &S, ObjCMethodDecl *Method) { // Find the entry for this selector in the method pool. diff --git a/clang/test/ClangScanDeps/tu-directory-deps.c b/clang/test/ClangScanDeps/tu-directory-deps.c new file mode 100644 index 00000000000000..ed053bf4f82ab7 --- /dev/null +++ b/clang/test/ClangScanDeps/tu-directory-deps.c @@ -0,0 +1,44 @@ +// Test that a translation unit reports the directories it searches for headers. + +// RUN: rm -rf %t +// RUN: split-file %s %t +// RUN: sed "s|DIR|%/t|g" %t/cdb.json.template > %t/cdb.json + +// RUN: clang-scan-deps -compilation-database %t/cdb.json \ +// RUN: -format experimental-full -track-search-directories 2>&1 \ +// RUN: | sed 's:\\\\\?:/:g' | FileCheck %s -DPREFIX=%/t --check-prefix=ON + +// ON: "directory-deps": [ +// ON-NEXT: "[[PREFIX]]/angled", +// ON-NEXT: "[[PREFIX]]/fw", +// ON-NEXT: "[[PREFIX]]/missing", +// ON-NEXT: "[[PREFIX]]/quoted" +// ON-NEXT: ] + +// RUN: clang-scan-deps -compilation-database %t/cdb.json \ +// RUN: -format experimental-full 2>&1 \ +// RUN: | sed 's:\\\\\?:/:g' | FileCheck %s --check-prefix=OFF + +// OFF-NOT: "directory-deps" + +//--- fw/F.framework/Headers/F.h +//--- sysfw/SF.framework/Headers/SF.h +//--- quoted/Q.h +//--- angled/A.h +//--- sys/S.h +//--- notadir + +//--- tu.c +#include "Q.h" +#include "A.h" +#include "S.h" +#include <F/F.h> +#include <SF/SF.h> + +//--- cdb.json.template +[{ + "file": "DIR/tu.c", + "directory": "DIR", + "command": "clang -iquote DIR/quoted -I angled -isystem DIR/sys -F DIR/fw -iframework DIR/sysfw -I DIR/notadir -I DIR/missing -c DIR/tu.c -o DIR/tu.o" +}] + diff --git a/clang/test/ClangScanDeps/tu-search-directory-shadow.c b/clang/test/ClangScanDeps/tu-search-directory-shadow.c new file mode 100644 index 00000000000000..d7426dec4eea26 --- /dev/null +++ b/clang/test/ClangScanDeps/tu-search-directory-shadow.c @@ -0,0 +1,87 @@ +// Test the negative dependency of adding a header to a directory that is +// searched before the one an include previously resolved in. Nothing the first +// scan recorded as an input changes, only the listing of that directory. + +// RUN: rm -rf %t +// RUN: split-file %s %t +// RUN: sed "s|DIR|%/t|g" %t/cdb.json.template > %t/cdb.json + +// 0) The search directories are checked when the scanning module is loaded, +// so the tracking scans below can reuse one built by a scan that doesn't +// track them. +// RUN: clang-scan-deps -compilation-database %t/cdb.json \ +// RUN: -format experimental-full > /dev/null + +// 1) A clean scan resolves both headers in inc/B. Only the translation unit +// reports the search directories. +// RUN: clang-scan-deps -compilation-database %t/cdb.json \ +// RUN: -format experimental-full -track-search-directories 2>&1 \ +// RUN: | sed 's:\\\\\?:/:g' | FileCheck %s -DPREFIX=%/t --check-prefix=CLEAN + +// CLEAN-NOT: "directory-deps" +// CLEAN: "file-deps": [ +// CLEAN-NEXT: "[[PREFIX]]/inc/Mod/module.modulemap", +// CLEAN-NEXT: "[[PREFIX]]/inc/Mod/Mod.h", +// CLEAN-NEXT: "[[PREFIX]]/inc/B/Value.h", +// CLEAN-NEXT: "[[PREFIX]]/inc/B/Other.h" +// CLEAN-NEXT: ] +// CLEAN: "name": "Mod" +// CLEAN: "directory-deps": [ +// CLEAN-NEXT: "[[PREFIX]]/inc/A", +// CLEAN-NEXT: "[[PREFIX]]/inc/B", +// CLEAN-NEXT: "[[PREFIX]]/inc/Mod" +// CLEAN-NEXT: ] + +// 2) Shadow one of the headers from the directory that is searched first. +// Module files written in the second a scan starts count as up to date with +// it, so leave a second between builds. +// RUN: sleep 1 +// RUN: touch %t/inc/A/Value.h + +// 3) Re-scan without -invalidated-path: the cached module is reused, so +// the stale resolution is reported again. This is the negative dependency +// the build system has to close. +// RUN: clang-scan-deps -compilation-database %t/cdb.json \ +// RUN: -format experimental-full -track-search-directories 2>&1 \ +// RUN: | sed 's:\\\\\?:/:g' | FileCheck %s -DPREFIX=%/t --check-prefix=STALE + +// STALE: "[[PREFIX]]/inc/B/Value.h" +// STALE-NOT: "[[PREFIX]]/inc/A/Value.h" + +// 4) Re-scan with -invalidated-path: the scanning module shares the +// translation unit's header search, so it is rebuilt and picks up the +// shadowing header. +// RUN: clang-scan-deps -compilation-database %t/cdb.json \ +// RUN: -format experimental-full -track-search-directories \ +// RUN: -invalidated-path=%/t/inc/A 2>&1 \ +// RUN: | sed 's:\\\\\?:/:g' | FileCheck %s -DPREFIX=%/t --check-prefix=FRESH + +// FRESH: "file-deps": [ +// FRESH-NEXT: "[[PREFIX]]/inc/Mod/module.modulemap", +// FRESH-NEXT: "[[PREFIX]]/inc/Mod/Mod.h", +// FRESH-NEXT: "[[PREFIX]]/inc/A/Value.h", +// FRESH-NEXT: "[[PREFIX]]/inc/B/Other.h" +// FRESH-NEXT: ] +// FRESH: "name": "Mod" + +//--- inc/A/Placeholder.h +//--- inc/B/Value.h +//--- inc/B/Other.h + +//--- inc/Mod/module.modulemap +module Mod { header "Mod.h" } + +//--- inc/Mod/Mod.h +#include "Value.h" +#include "Other.h" + +//--- tu.c +#include "Mod.h" + +//--- cdb.json.template +[{ + "file": "DIR/tu.c", + "directory": "DIR", + "command": "clang -fmodules -fmodules-cache-path=DIR/cache -I DIR/inc/A -I DIR/inc/B -I DIR/inc/Mod -c DIR/tu.c -o DIR/tu.o" +}] + diff --git a/clang/tools/clang-scan-deps/ClangScanDeps.cpp b/clang/tools/clang-scan-deps/ClangScanDeps.cpp index 4c6ff23de71df3..17b6451607bab4 100644 --- a/clang/tools/clang-scan-deps/ClangScanDeps.cpp +++ b/clang/tools/clang-scan-deps/ClangScanDeps.cpp @@ -93,6 +93,7 @@ static std::string ModuleFilesDir; static bool EagerLoadModules; static bool CacheNegativeStats; static std::vector<std::string> InvalidatedPaths; +static bool TrackSearchDirectories; static unsigned NumThreads = 0; static std::string CompilationDB; static std::optional<std::string> ModuleNames; @@ -217,6 +218,8 @@ static void ParseArgs(int argc, char **argv) { InvalidatedPaths.emplace_back(Path); } + TrackSearchDirectories = Args.hasArg(OPT_track_search_directories); + if (const llvm::opt::Arg *A = Args.getLastArg(OPT_j)) { StringRef S{A->getValue()}; if (!llvm::to_integer(S, NumThreads, 0)) { @@ -424,6 +427,7 @@ class FullDeps { ID.FileName = std::string(Input); ID.ContextHash = std::move(TUDeps.ID.ContextHash); ID.FileDeps = std::move(TUDeps.FileDeps); + ID.DirectoryDeps = std::move(TUDeps.DirectoryDeps); ID.NamedModule = std::move(TUDeps.ID.ModuleName); ID.NamedModuleDeps = std::move(TUDeps.NamedModuleDeps); ID.ClangModuleDeps = std::move(TUDeps.ClangModuleDeps); @@ -556,6 +560,9 @@ class FullDeps { toJSONSorted(JOS, I.ClangModuleDeps)); JOS.attributeArray("command-line", toJSONStrings(JOS, Cmd.Arguments)); + if (!I.DirectoryDeps.empty()) + JOS.attributeArray("directory-deps", + toJSONStrings(JOS, I.DirectoryDeps)); JOS.attribute("executable", StringRef(Cmd.Executable)); JOS.attributeArray("file-deps", toJSONStrings(JOS, I.FileDeps)); @@ -579,6 +586,9 @@ class FullDeps { toJSONSorted(JOS, I.ClangModuleDeps)); JOS.attributeArray("command-line", toJSONStrings(JOS, I.DriverCommandLine)); + if (!I.DirectoryDeps.empty()) + JOS.attributeArray("directory-deps", + toJSONStrings(JOS, I.DirectoryDeps)); JOS.attribute("executable", "clang"); JOS.attributeArray("file-deps", toJSONStrings(JOS, I.FileDeps)); @@ -634,6 +644,7 @@ class FullDeps { std::string FileName; std::string ContextHash; std::vector<std::string> FileDeps; + std::vector<std::string> DirectoryDeps; std::string NamedModule; std::vector<std::string> NamedModuleDeps; std::vector<ModuleID> ClangModuleDeps; @@ -1180,6 +1191,7 @@ int clang_scan_deps_main(int argc, char **argv, const llvm::ToolContext &) { Opts.FlushModuleCache = !NoFlushModuleCache; Opts.CacheNegativeStats = CacheNegativeStats; Opts.ValidateAgainstInvalidatedPaths = true; + Opts.TrackSearchDirectories = TrackSearchDirectories; Opts.LogPath = LogPath; llvm::Timer T; diff --git a/clang/tools/clang-scan-deps/Opts.td b/clang/tools/clang-scan-deps/Opts.td index 663d8468847f39..5f355c54356295 100644 --- a/clang/tools/clang-scan-deps/Opts.td +++ b/clang/tools/clang-scan-deps/Opts.td @@ -31,6 +31,11 @@ defm invalidated_path "are resolved against the current directory. May be given more than " "once">; +def track_search_directories + : F<"track-search-directories", + "Also report the non-system directories searched for headers in " + "'directory-deps'">; + def j : Arg<"j", "Number of worker threads to use (default: use all concurrent threads)">; defm compilation_database : Eq<"compilation-database", "Compilation database">; _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
