//===--- 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 #include #include #include 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 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 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 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 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 get(bool &ShouldBroadcast) { // Fast path for common case without taking lock. if (FinalizedNoCDB.load()) { ShouldBroadcast = false; return nullptr; } std::lock_guard 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 CompileCommandsDir) { if (CompileCommandsDir) OnlyDirCache = std::make_unique(*CompileCommandsDir); } DirectoryBasedGlobalCompilationDatabase:: ~DirectoryBasedGlobalCompilationDatabase() = default; llvm::Optional 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::getDirectoryCaches( llvm::ArrayRef Dirs) const { std::vector FoldedDirs; FoldedDirs.reserve(Dirs.size()); for (const auto &Dir : Dirs) FoldedDirs.push_back(maybeCaseFoldPath(Dir)); std::vector Ret; Ret.reserve(Dirs.size()); std::lock_guard 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::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 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 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 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 DirectoryHasCDB; std::vector 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 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 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 FallbackFlags, tooling::ArgumentsAdjuster Adjuster) : Base(Base), ArgsAdjuster(std::move(Adjuster)), FallbackFlags(std::move(FallbackFlags)) { if (Base) BaseChanged = Base->watch([this](const std::vector Changes) { OnCommandChanged.broadcast(Changes); }); } llvm::Optional OverlayCDB::getCompileCommand(PathRef File) const { llvm::Optional Cmd; { std::lock_guard 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 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 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 Lock(Mutex); if (Cmd) Commands[CanonPath] = std::move(*Cmd); else Commands.erase(CanonPath); } OnCommandChanged.broadcast({CanonPath}); } llvm::Optional 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