| //===--- GlobalCompilationDatabase.cpp ---------------------------*- C++-*-===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "GlobalCompilationDatabase.h" |
| #include "FS.h" |
| #include "support/Logger.h" |
| #include "support/Path.h" |
| #include "clang/Frontend/CompilerInvocation.h" |
| #include "clang/Tooling/ArgumentsAdjusters.h" |
| #include "clang/Tooling/CompilationDatabase.h" |
| #include "llvm/ADT/None.h" |
| #include "llvm/ADT/Optional.h" |
| #include "llvm/ADT/STLExtras.h" |
| #include "llvm/ADT/ScopeExit.h" |
| #include "llvm/ADT/SmallString.h" |
| #include "llvm/Support/FileSystem.h" |
| #include "llvm/Support/FileUtilities.h" |
| #include "llvm/Support/Path.h" |
| #include "llvm/Support/Program.h" |
| #include <chrono> |
| #include <string> |
| #include <tuple> |
| #include <vector> |
| |
| namespace clang { |
| namespace clangd { |
| namespace { |
| |
| // Runs the given action on all parent directories of filename, starting from |
| // deepest directory and going up to root. Stops whenever action succeeds. |
| void actOnAllParentDirectories(PathRef FileName, |
| llvm::function_ref<bool(PathRef)> Action) { |
| for (auto Path = llvm::sys::path::parent_path(FileName); |
| !Path.empty() && !Action(Path); |
| Path = llvm::sys::path::parent_path(Path)) |
| ; |
| } |
| |
| } // namespace |
| |
| tooling::CompileCommand |
| GlobalCompilationDatabase::getFallbackCommand(PathRef File) const { |
| std::vector<std::string> Argv = {"clang"}; |
| // Clang treats .h files as C by default and files without extension as linker |
| // input, resulting in unhelpful diagnostics. |
| // Parsing as Objective C++ is friendly to more cases. |
| auto FileExtension = llvm::sys::path::extension(File); |
| if (FileExtension.empty() || FileExtension == ".h") |
| Argv.push_back("-xobjective-c++-header"); |
| Argv.push_back(std::string(File)); |
| tooling::CompileCommand Cmd(llvm::sys::path::parent_path(File), |
| llvm::sys::path::filename(File), std::move(Argv), |
| /*Output=*/""); |
| Cmd.Heuristic = "clangd fallback"; |
| return Cmd; |
| } |
| |
| // Loads and caches the CDB from a single directory. |
| // |
| // This class is threadsafe, which is to say we have independent locks for each |
| // directory we're searching for a CDB. |
| // Loading is deferred until first access. |
| // |
| // The DirectoryBasedCDB keeps a map from path => DirectoryCache. |
| // Typical usage is to: |
| // - 1) determine all the paths that might be searched |
| // - 2) acquire the map lock and get-or-create all the DirectoryCache entries |
| // - 3) release the map lock and query the caches as desired |
| // |
| // FIXME: this should revalidate the cache sometimes |
| // FIXME: IO should go through a VFS |
| class DirectoryBasedGlobalCompilationDatabase::DirectoryCache { |
| // Absolute canonical path that we're the cache for. (Not case-folded). |
| const std::string Path; |
| |
| // True if we've looked for a CDB here and found none. |
| // (This makes it possible for get() to return without taking a lock) |
| // FIXME: this should have an expiry time instead of lasting forever. |
| std::atomic<bool> FinalizedNoCDB = {false}; |
| |
| // Guards following cache state. |
| std::mutex Mu; |
| // Has cache been filled from disk? FIXME: this should be an expiry time. |
| bool CachePopulated = false; |
| // Whether a new CDB has been loaded but not broadcast yet. |
| bool NeedsBroadcast = false; |
| // Last loaded CDB, meaningful if CachePopulated is set. |
| // shared_ptr so we can overwrite this when callers are still using the CDB. |
| std::shared_ptr<tooling::CompilationDatabase> CDB; |
| |
| public: |
| DirectoryCache(llvm::StringRef Path) : Path(Path) { |
| assert(llvm::sys::path::is_absolute(Path)); |
| } |
| |
| // Get the CDB associated with this directory. |
| // ShouldBroadcast: |
| // - as input, signals whether the caller is willing to broadcast a |
| // newly-discovered CDB. (e.g. to trigger background indexing) |
| // - as output, signals whether the caller should do so. |
| // (If a new CDB is discovered and ShouldBroadcast is false, we mark the |
| // CDB as needing broadcast, and broadcast it next time we can). |
| std::shared_ptr<const tooling::CompilationDatabase> |
| get(bool &ShouldBroadcast) { |
| // Fast path for common case without taking lock. |
| if (FinalizedNoCDB.load()) { |
| ShouldBroadcast = false; |
| return nullptr; |
| } |
| std::lock_guard<std::mutex> Lock(Mu); |
| auto RequestBroadcast = llvm::make_scope_exit([&, OldCDB(CDB.get())] { |
| // If we loaded a new CDB, it should be broadcast at some point. |
| if (CDB != nullptr && CDB.get() != OldCDB) |
| NeedsBroadcast = true; |
| else if (CDB == nullptr) // nothing to broadcast anymore! |
| NeedsBroadcast = false; |
| // If we have something to broadcast, then do so iff allowed. |
| if (!ShouldBroadcast) |
| return; |
| ShouldBroadcast = NeedsBroadcast; |
| NeedsBroadcast = false; |
| }); |
| |
| // For now, we never actually attempt to revalidate a populated cache. |
| if (CachePopulated) |
| return CDB; |
| assert(CDB == nullptr); |
| |
| load(); |
| CachePopulated = true; |
| |
| if (!CDB) |
| FinalizedNoCDB.store(true); |
| return CDB; |
| } |
| |
| llvm::StringRef path() const { return Path; } |
| |
| private: |
| // Updates `CDB` from disk state. |
| void load() { |
| std::string Error; // ignored, because it's often "didn't find anything". |
| CDB = tooling::CompilationDatabase::loadFromDirectory(Path, Error); |
| if (!CDB) { |
| // Fallback: check for $src/build, the conventional CMake build root. |
| // Probe existence first to avoid each plugin doing IO if it doesn't |
| // exist. |
| llvm::SmallString<256> BuildDir(Path); |
| llvm::sys::path::append(BuildDir, "build"); |
| if (llvm::sys::fs::is_directory(BuildDir)) { |
| vlog("Found candidate build directory {0}", BuildDir); |
| CDB = tooling::CompilationDatabase::loadFromDirectory(BuildDir, Error); |
| } |
| } |
| if (CDB) { |
| log("Loaded compilation database from {0}", Path); |
| } else { |
| vlog("No compilation database at {0}", Path); |
| } |
| } |
| }; |
| |
| DirectoryBasedGlobalCompilationDatabase:: |
| DirectoryBasedGlobalCompilationDatabase( |
| llvm::Optional<Path> CompileCommandsDir) { |
| if (CompileCommandsDir) |
| OnlyDirCache = std::make_unique<DirectoryCache>(*CompileCommandsDir); |
| } |
| |
| DirectoryBasedGlobalCompilationDatabase:: |
| ~DirectoryBasedGlobalCompilationDatabase() = default; |
| |
| llvm::Optional<tooling::CompileCommand> |
| DirectoryBasedGlobalCompilationDatabase::getCompileCommand(PathRef File) const { |
| CDBLookupRequest Req; |
| Req.FileName = File; |
| Req.ShouldBroadcast = true; |
| |
| auto Res = lookupCDB(Req); |
| if (!Res) { |
| log("Failed to find compilation database for {0}", File); |
| return llvm::None; |
| } |
| |
| auto Candidates = Res->CDB->getCompileCommands(File); |
| if (!Candidates.empty()) |
| return std::move(Candidates.front()); |
| |
| return None; |
| } |
| |
| // For platforms where paths are case-insensitive (but case-preserving), |
| // we need to do case-insensitive comparisons and use lowercase keys. |
| // FIXME: Make Path a real class with desired semantics instead. |
| // This class is not the only place this problem exists. |
| // FIXME: Mac filesystems default to case-insensitive, but may be sensitive. |
| |
| static std::string maybeCaseFoldPath(PathRef Path) { |
| #if defined(_WIN32) || defined(__APPLE__) |
| return Path.lower(); |
| #else |
| return std::string(Path); |
| #endif |
| } |
| |
| static bool pathEqual(PathRef A, PathRef B) { |
| #if defined(_WIN32) || defined(__APPLE__) |
| return A.equals_lower(B); |
| #else |
| return A == B; |
| #endif |
| } |
| |
| std::vector<DirectoryBasedGlobalCompilationDatabase::DirectoryCache *> |
| DirectoryBasedGlobalCompilationDatabase::getDirectoryCaches( |
| llvm::ArrayRef<llvm::StringRef> Dirs) const { |
| std::vector<std::string> FoldedDirs; |
| FoldedDirs.reserve(Dirs.size()); |
| for (const auto &Dir : Dirs) |
| FoldedDirs.push_back(maybeCaseFoldPath(Dir)); |
| |
| std::vector<DirectoryCache *> Ret; |
| Ret.reserve(Dirs.size()); |
| |
| std::lock_guard<std::mutex> Lock(DirCachesMutex); |
| for (unsigned I = 0; I < Dirs.size(); ++I) |
| Ret.push_back(&DirCaches.try_emplace(FoldedDirs[I], Dirs[I]).first->second); |
| return Ret; |
| } |
| |
| llvm::Optional<DirectoryBasedGlobalCompilationDatabase::CDBLookupResult> |
| DirectoryBasedGlobalCompilationDatabase::lookupCDB( |
| CDBLookupRequest Request) const { |
| assert(llvm::sys::path::is_absolute(Request.FileName) && |
| "path must be absolute"); |
| |
| bool ShouldBroadcast = false; |
| DirectoryCache *DirCache = nullptr; |
| std::shared_ptr<const tooling::CompilationDatabase> CDB = nullptr; |
| if (OnlyDirCache) { |
| DirCache = OnlyDirCache.get(); |
| ShouldBroadcast = Request.ShouldBroadcast; |
| CDB = DirCache->get(ShouldBroadcast); |
| } else { |
| // Traverse the canonical version to prevent false positives. i.e.: |
| // src/build/../a.cc can detect a CDB in /src/build if not canonicalized. |
| std::string CanonicalPath = removeDots(Request.FileName); |
| std::vector<llvm::StringRef> SearchDirs; |
| actOnAllParentDirectories(CanonicalPath, [&](PathRef Path) { |
| SearchDirs.push_back(Path); |
| return false; |
| }); |
| for (DirectoryCache *Candidate : getDirectoryCaches(SearchDirs)) { |
| bool CandidateShouldBroadcast = Request.ShouldBroadcast; |
| if ((CDB = Candidate->get(CandidateShouldBroadcast))) { |
| DirCache = Candidate; |
| ShouldBroadcast = CandidateShouldBroadcast; |
| break; |
| } |
| } |
| } |
| |
| if (!CDB) |
| return llvm::None; |
| |
| CDBLookupResult Result; |
| Result.CDB = std::move(CDB); |
| Result.PI.SourceRoot = DirCache->path().str(); |
| |
| // FIXME: Maybe make the following part async, since this can block |
| // retrieval of compile commands. |
| if (ShouldBroadcast) |
| broadcastCDB(Result); |
| return Result; |
| } |
| |
| void DirectoryBasedGlobalCompilationDatabase::broadcastCDB( |
| CDBLookupResult Result) const { |
| assert(Result.CDB && "Trying to broadcast an invalid CDB!"); |
| |
| std::vector<std::string> AllFiles = Result.CDB->getAllFiles(); |
| // We assume CDB in CompileCommandsDir owns all of its entries, since we don't |
| // perform any search in parent paths whenever it is set. |
| if (OnlyDirCache) { |
| assert(OnlyDirCache->path() == Result.PI.SourceRoot && |
| "Trying to broadcast a CDB outside of CompileCommandsDir!"); |
| OnCommandChanged.broadcast(std::move(AllFiles)); |
| return; |
| } |
| |
| // Uniquify all parent directories of all files. |
| llvm::StringMap<bool> DirectoryHasCDB; |
| std::vector<llvm::StringRef> FileAncestors; |
| for (llvm::StringRef File : AllFiles) { |
| actOnAllParentDirectories(File, [&](PathRef Path) { |
| auto It = DirectoryHasCDB.try_emplace(Path); |
| // Already seen this path, and all of its parents. |
| if (!It.second) |
| return true; |
| |
| FileAncestors.push_back(It.first->getKey()); |
| return pathEqual(Path, Result.PI.SourceRoot); |
| }); |
| } |
| // Work out which ones have CDBs in them. |
| for (DirectoryCache *Dir : getDirectoryCaches(FileAncestors)) { |
| bool ShouldBroadcast = false; |
| if (Dir->get(ShouldBroadcast)) |
| DirectoryHasCDB.find(Dir->path())->setValue(true); |
| } |
| |
| std::vector<std::string> GovernedFiles; |
| for (llvm::StringRef File : AllFiles) { |
| // A file is governed by this CDB if lookup for the file would find it. |
| // Independent of whether it has an entry for that file or not. |
| actOnAllParentDirectories(File, [&](PathRef Path) { |
| if (DirectoryHasCDB.lookup(Path)) { |
| if (pathEqual(Path, Result.PI.SourceRoot)) |
| // Make sure listeners always get a canonical path for the file. |
| GovernedFiles.push_back(removeDots(File)); |
| // Stop as soon as we hit a CDB. |
| return true; |
| } |
| return false; |
| }); |
| } |
| |
| OnCommandChanged.broadcast(std::move(GovernedFiles)); |
| } |
| |
| llvm::Optional<ProjectInfo> |
| DirectoryBasedGlobalCompilationDatabase::getProjectInfo(PathRef File) const { |
| CDBLookupRequest Req; |
| Req.FileName = File; |
| Req.ShouldBroadcast = false; |
| auto Res = lookupCDB(Req); |
| if (!Res) |
| return llvm::None; |
| return Res->PI; |
| } |
| |
| OverlayCDB::OverlayCDB(const GlobalCompilationDatabase *Base, |
| std::vector<std::string> FallbackFlags, |
| tooling::ArgumentsAdjuster Adjuster) |
| : Base(Base), ArgsAdjuster(std::move(Adjuster)), |
| FallbackFlags(std::move(FallbackFlags)) { |
| if (Base) |
| BaseChanged = Base->watch([this](const std::vector<std::string> Changes) { |
| OnCommandChanged.broadcast(Changes); |
| }); |
| } |
| |
| llvm::Optional<tooling::CompileCommand> |
| OverlayCDB::getCompileCommand(PathRef File) const { |
| llvm::Optional<tooling::CompileCommand> Cmd; |
| { |
| std::lock_guard<std::mutex> Lock(Mutex); |
| auto It = Commands.find(removeDots(File)); |
| if (It != Commands.end()) |
| Cmd = It->second; |
| } |
| if (!Cmd && Base) |
| Cmd = Base->getCompileCommand(File); |
| if (!Cmd) |
| return llvm::None; |
| if (ArgsAdjuster) |
| Cmd->CommandLine = ArgsAdjuster(Cmd->CommandLine, Cmd->Filename); |
| return Cmd; |
| } |
| |
| tooling::CompileCommand OverlayCDB::getFallbackCommand(PathRef File) const { |
| auto Cmd = Base ? Base->getFallbackCommand(File) |
| : GlobalCompilationDatabase::getFallbackCommand(File); |
| std::lock_guard<std::mutex> Lock(Mutex); |
| Cmd.CommandLine.insert(Cmd.CommandLine.end(), FallbackFlags.begin(), |
| FallbackFlags.end()); |
| if (ArgsAdjuster) |
| Cmd.CommandLine = ArgsAdjuster(Cmd.CommandLine, Cmd.Filename); |
| return Cmd; |
| } |
| |
| void OverlayCDB::setCompileCommand( |
| PathRef File, llvm::Optional<tooling::CompileCommand> Cmd) { |
| // We store a canonical version internally to prevent mismatches between set |
| // and get compile commands. Also it assures clients listening to broadcasts |
| // doesn't receive different names for the same file. |
| std::string CanonPath = removeDots(File); |
| { |
| std::unique_lock<std::mutex> Lock(Mutex); |
| if (Cmd) |
| Commands[CanonPath] = std::move(*Cmd); |
| else |
| Commands.erase(CanonPath); |
| } |
| OnCommandChanged.broadcast({CanonPath}); |
| } |
| |
| llvm::Optional<ProjectInfo> OverlayCDB::getProjectInfo(PathRef File) const { |
| // It wouldn't make much sense to treat files with overridden commands |
| // specially when we can't do the same for the (unknown) local headers they |
| // include or changing behavior mid-air after receiving an override. |
| if (Base) |
| return Base->getProjectInfo(File); |
| return llvm::None; |
| } |
| } // namespace clangd |
| } // namespace clang |