forked from OSchip/llvm-project
832 lines
30 KiB
C++
832 lines
30 KiB
C++
//===--- 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 "Config.h"
|
|
#include "FS.h"
|
|
#include "SourceCode.h"
|
|
#include "support/Logger.h"
|
|
#include "support/Path.h"
|
|
#include "support/Threading.h"
|
|
#include "support/ThreadsafeFS.h"
|
|
#include "clang/Tooling/ArgumentsAdjusters.h"
|
|
#include "clang/Tooling/CompilationDatabase.h"
|
|
#include "clang/Tooling/CompilationDatabasePluginRegistry.h"
|
|
#include "clang/Tooling/JSONCompilationDatabase.h"
|
|
#include "llvm/ADT/None.h"
|
|
#include "llvm/ADT/Optional.h"
|
|
#include "llvm/ADT/PointerIntPair.h"
|
|
#include "llvm/ADT/STLExtras.h"
|
|
#include "llvm/ADT/ScopeExit.h"
|
|
#include "llvm/ADT/SmallString.h"
|
|
#include "llvm/ADT/StringMap.h"
|
|
#include "llvm/Support/Path.h"
|
|
#include "llvm/Support/VirtualFileSystem.h"
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <mutex>
|
|
#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 = absoluteParent(FileName); !Path.empty() && !Action(Path);
|
|
Path = absoluteParent(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
|
|
class DirectoryBasedGlobalCompilationDatabase::DirectoryCache {
|
|
using stopwatch = std::chrono::steady_clock;
|
|
|
|
// CachedFile is used to read a CDB file on disk (e.g. compile_commands.json).
|
|
// It specializes in being able to quickly bail out if the file is unchanged,
|
|
// which is the common case.
|
|
// Internally, it stores file metadata so a stat() can verify it's unchanged.
|
|
// We don't actually cache the content as it's not needed - if the file is
|
|
// unchanged then the previous CDB is valid.
|
|
struct CachedFile {
|
|
CachedFile(llvm::StringRef Parent, llvm::StringRef Rel) {
|
|
llvm::SmallString<256> Path = Parent;
|
|
llvm::sys::path::append(Path, Rel);
|
|
this->Path = Path.str().str();
|
|
}
|
|
std::string Path;
|
|
size_t Size = NoFileCached;
|
|
llvm::sys::TimePoint<> ModifiedTime;
|
|
FileDigest ContentHash;
|
|
|
|
static constexpr size_t NoFileCached = -1;
|
|
|
|
struct LoadResult {
|
|
enum {
|
|
FileNotFound,
|
|
TransientError,
|
|
FoundSameData,
|
|
FoundNewData,
|
|
} Result;
|
|
std::unique_ptr<llvm::MemoryBuffer> Buffer; // Set only if FoundNewData
|
|
};
|
|
|
|
LoadResult load(llvm::vfs::FileSystem &FS, bool HasOldData);
|
|
};
|
|
|
|
// If we've looked for a CDB here and found none, the time when that happened.
|
|
// (Atomics make it possible for get() to return without taking a lock)
|
|
std::atomic<stopwatch::rep> NoCDBAt = {
|
|
stopwatch::time_point::min().time_since_epoch().count()};
|
|
|
|
// Guards the following cache state.
|
|
std::mutex Mu;
|
|
// When was the cache last known to be in sync with disk state?
|
|
stopwatch::time_point CachePopulatedAt = stopwatch::time_point::min();
|
|
// Whether a new CDB has been loaded but not broadcast yet.
|
|
bool NeedsBroadcast = false;
|
|
// Last loaded CDB, meaningful if CachePopulatedAt was ever set.
|
|
// shared_ptr so we can overwrite this when callers are still using the CDB.
|
|
std::shared_ptr<tooling::CompilationDatabase> CDB;
|
|
// File metadata for the CDB files we support tracking directly.
|
|
CachedFile CompileCommandsJson;
|
|
CachedFile BuildCompileCommandsJson;
|
|
CachedFile CompileFlagsTxt;
|
|
// CachedFile member corresponding to CDB.
|
|
// CDB | ACF | Scenario
|
|
// null | null | no CDB found, or initial empty cache
|
|
// set | null | CDB was loaded via generic plugin interface
|
|
// null | set | found known CDB file, but parsing it failed
|
|
// set | set | CDB was parsed from a known file
|
|
CachedFile *ActiveCachedFile = nullptr;
|
|
|
|
public:
|
|
DirectoryCache(llvm::StringRef Path)
|
|
: CompileCommandsJson(Path, "compile_commands.json"),
|
|
BuildCompileCommandsJson(Path, "build/compile_commands.json"),
|
|
CompileFlagsTxt(Path, "compile_flags.txt"), Path(Path) {
|
|
assert(llvm::sys::path::is_absolute(Path));
|
|
}
|
|
|
|
// Absolute canonical path that we're the cache for. (Not case-folded).
|
|
const std::string 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(const ThreadsafeFS &TFS, bool &ShouldBroadcast,
|
|
stopwatch::time_point FreshTime, stopwatch::time_point FreshTimeMissing) {
|
|
// Fast path for common case without taking lock.
|
|
if (stopwatch::time_point(stopwatch::duration(NoCDBAt.load())) >
|
|
FreshTimeMissing) {
|
|
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;
|
|
});
|
|
|
|
// If our cache is valid, serve from it.
|
|
if (CachePopulatedAt > FreshTime)
|
|
return CDB;
|
|
|
|
if (/*MayCache=*/load(*TFS.view(/*CWD=*/llvm::None))) {
|
|
// Use new timestamp, as loading may be slow.
|
|
CachePopulatedAt = stopwatch::now();
|
|
NoCDBAt.store((CDB ? stopwatch::time_point::min() : CachePopulatedAt)
|
|
.time_since_epoch()
|
|
.count());
|
|
}
|
|
|
|
return CDB;
|
|
}
|
|
|
|
private:
|
|
// Updates `CDB` from disk state. Returns false on failure.
|
|
bool load(llvm::vfs::FileSystem &FS);
|
|
};
|
|
|
|
DirectoryBasedGlobalCompilationDatabase::DirectoryCache::CachedFile::LoadResult
|
|
DirectoryBasedGlobalCompilationDatabase::DirectoryCache::CachedFile::load(
|
|
llvm::vfs::FileSystem &FS, bool HasOldData) {
|
|
auto Stat = FS.status(Path);
|
|
if (!Stat || !Stat->isRegularFile()) {
|
|
Size = NoFileCached;
|
|
ContentHash = {};
|
|
return {LoadResult::FileNotFound, nullptr};
|
|
}
|
|
// If both the size and mtime match, presume unchanged without reading.
|
|
if (HasOldData && Stat->getLastModificationTime() == ModifiedTime &&
|
|
Stat->getSize() == Size)
|
|
return {LoadResult::FoundSameData, nullptr};
|
|
auto Buf = FS.getBufferForFile(Path);
|
|
if (!Buf || (*Buf)->getBufferSize() != Stat->getSize()) {
|
|
// Don't clear the cache - possible we're seeing inconsistent size as the
|
|
// file is being recreated. If it ends up identical later, great!
|
|
//
|
|
// This isn't a complete solution: if we see a partial file but stat/read
|
|
// agree on its size, we're ultimately going to have spurious CDB reloads.
|
|
// May be worth fixing if generators don't write atomically (CMake does).
|
|
elog("Failed to read {0}: {1}", Path,
|
|
Buf ? "size changed" : Buf.getError().message());
|
|
return {LoadResult::TransientError, nullptr};
|
|
}
|
|
|
|
FileDigest NewContentHash = digest((*Buf)->getBuffer());
|
|
if (HasOldData && NewContentHash == ContentHash) {
|
|
// mtime changed but data is the same: avoid rebuilding the CDB.
|
|
ModifiedTime = Stat->getLastModificationTime();
|
|
return {LoadResult::FoundSameData, nullptr};
|
|
}
|
|
|
|
Size = (*Buf)->getBufferSize();
|
|
ModifiedTime = Stat->getLastModificationTime();
|
|
ContentHash = NewContentHash;
|
|
return {LoadResult::FoundNewData, std::move(*Buf)};
|
|
}
|
|
|
|
// Adapt CDB-loading functions to a common interface for DirectoryCache::load().
|
|
static std::unique_ptr<tooling::CompilationDatabase>
|
|
parseJSON(PathRef Path, llvm::StringRef Data, std::string &Error) {
|
|
if (auto CDB = tooling::JSONCompilationDatabase::loadFromBuffer(
|
|
Data, Error, tooling::JSONCommandLineSyntax::AutoDetect)) {
|
|
// FS used for expanding response files.
|
|
// FIXME: ExpandResponseFilesDatabase appears not to provide the usual
|
|
// thread-safety guarantees, as the access to FS is not locked!
|
|
// For now, use the real FS, which is known to be threadsafe (if we don't
|
|
// use/change working directory, which ExpandResponseFilesDatabase doesn't).
|
|
auto FS = llvm::vfs::getRealFileSystem();
|
|
return tooling::inferTargetAndDriverMode(
|
|
tooling::inferMissingCompileCommands(
|
|
expandResponseFiles(std::move(CDB), std::move(FS))));
|
|
}
|
|
return nullptr;
|
|
}
|
|
static std::unique_ptr<tooling::CompilationDatabase>
|
|
parseFixed(PathRef Path, llvm::StringRef Data, std::string &Error) {
|
|
return tooling::FixedCompilationDatabase::loadFromBuffer(
|
|
llvm::sys::path::parent_path(Path), Data, Error);
|
|
}
|
|
|
|
bool DirectoryBasedGlobalCompilationDatabase::DirectoryCache::load(
|
|
llvm::vfs::FileSystem &FS) {
|
|
dlog("Probing directory {0}", Path);
|
|
std::string Error;
|
|
|
|
// Load from the specially-supported compilation databases (JSON + Fixed).
|
|
// For these, we know the files they read and cache their metadata so we can
|
|
// cheaply validate whether they've changed, and hot-reload if they have.
|
|
// (As a bonus, these are also VFS-clean)!
|
|
struct CDBFile {
|
|
CachedFile *File;
|
|
// Wrapper for {Fixed,JSON}CompilationDatabase::loadFromBuffer.
|
|
std::unique_ptr<tooling::CompilationDatabase> (*Parser)(
|
|
PathRef,
|
|
/*Data*/ llvm::StringRef,
|
|
/*ErrorMsg*/ std::string &);
|
|
};
|
|
for (const auto &Entry : {CDBFile{&CompileCommandsJson, parseJSON},
|
|
CDBFile{&BuildCompileCommandsJson, parseJSON},
|
|
CDBFile{&CompileFlagsTxt, parseFixed}}) {
|
|
bool Active = ActiveCachedFile == Entry.File;
|
|
auto Loaded = Entry.File->load(FS, Active);
|
|
switch (Loaded.Result) {
|
|
case CachedFile::LoadResult::FileNotFound:
|
|
if (Active) {
|
|
log("Unloaded compilation database from {0}", Entry.File->Path);
|
|
ActiveCachedFile = nullptr;
|
|
CDB = nullptr;
|
|
}
|
|
// Continue looking at other candidates.
|
|
break;
|
|
case CachedFile::LoadResult::TransientError:
|
|
// File existed but we couldn't read it. Reuse the cache, retry later.
|
|
return false; // Load again next time.
|
|
case CachedFile::LoadResult::FoundSameData:
|
|
assert(Active && "CachedFile may not return 'same data' if !HasOldData");
|
|
// This is the critical file, and it hasn't changed.
|
|
return true;
|
|
case CachedFile::LoadResult::FoundNewData:
|
|
// We have a new CDB!
|
|
CDB = Entry.Parser(Entry.File->Path, Loaded.Buffer->getBuffer(), Error);
|
|
if (CDB)
|
|
log("{0} compilation database from {1}", Active ? "Reloaded" : "Loaded",
|
|
Entry.File->Path);
|
|
else
|
|
elog("Failed to load compilation database from {0}: {1}",
|
|
Entry.File->Path, Error);
|
|
ActiveCachedFile = Entry.File;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Fall back to generic handling of compilation databases.
|
|
// We don't know what files they read, so can't efficiently check whether
|
|
// they need to be reloaded. So we never do that.
|
|
// FIXME: the interface doesn't provide a way to virtualize FS access.
|
|
|
|
// Don't try these more than once. If we've scanned before, we're done.
|
|
if (CachePopulatedAt > stopwatch::time_point::min())
|
|
return true;
|
|
for (const auto &Entry :
|
|
tooling::CompilationDatabasePluginRegistry::entries()) {
|
|
// Avoid duplicating the special cases handled above.
|
|
if (Entry.getName() == "fixed-compilation-database" ||
|
|
Entry.getName() == "json-compilation-database")
|
|
continue;
|
|
auto Plugin = Entry.instantiate();
|
|
if (auto CDB = Plugin->loadFromDirectory(Path, Error)) {
|
|
log("Loaded compilation database from {0} with plugin {1}", Path,
|
|
Entry.getName());
|
|
this->CDB = std::move(CDB);
|
|
return true;
|
|
}
|
|
// Don't log Error here, it's usually just "couldn't find <file>".
|
|
}
|
|
dlog("No compilation database at {0}", Path);
|
|
return true;
|
|
}
|
|
|
|
DirectoryBasedGlobalCompilationDatabase::
|
|
DirectoryBasedGlobalCompilationDatabase(const Options &Opts)
|
|
: Opts(Opts), Broadcaster(std::make_unique<BroadcastThread>(*this)) {
|
|
if (!this->Opts.ContextProvider)
|
|
this->Opts.ContextProvider = [](llvm::StringRef) {
|
|
return Context::current().clone();
|
|
};
|
|
}
|
|
|
|
DirectoryBasedGlobalCompilationDatabase::
|
|
~DirectoryBasedGlobalCompilationDatabase() = default;
|
|
|
|
llvm::Optional<tooling::CompileCommand>
|
|
DirectoryBasedGlobalCompilationDatabase::getCompileCommand(PathRef File) const {
|
|
CDBLookupRequest Req;
|
|
Req.FileName = File;
|
|
Req.ShouldBroadcast = true;
|
|
auto Now = std::chrono::steady_clock::now();
|
|
Req.FreshTime = Now - Opts.RevalidateAfter;
|
|
Req.FreshTimeMissing = Now - Opts.RevalidateMissingAfter;
|
|
|
|
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;
|
|
}
|
|
|
|
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) {
|
|
#ifndef NDEBUG
|
|
if (!llvm::sys::path::is_absolute(Dir))
|
|
elog("Trying to cache CDB for relative {0}");
|
|
#endif
|
|
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");
|
|
|
|
std::string Storage;
|
|
std::vector<llvm::StringRef> SearchDirs;
|
|
if (Opts.CompileCommandsDir) // FIXME: unify this case with config.
|
|
SearchDirs = {Opts.CompileCommandsDir.getValue()};
|
|
else {
|
|
WithContext WithProvidedContext(Opts.ContextProvider(Request.FileName));
|
|
const auto &Spec = Config::current().CompileFlags.CDBSearch;
|
|
switch (Spec.Policy) {
|
|
case Config::CDBSearchSpec::NoCDBSearch:
|
|
return llvm::None;
|
|
case Config::CDBSearchSpec::FixedDir:
|
|
Storage = Spec.FixedCDBPath.getValue();
|
|
SearchDirs = {Storage};
|
|
break;
|
|
case Config::CDBSearchSpec::Ancestors:
|
|
// Traverse the canonical version to prevent false positives. i.e.:
|
|
// src/build/../a.cc can detect a CDB in /src/build if not
|
|
// canonicalized.
|
|
Storage = removeDots(Request.FileName);
|
|
actOnAllParentDirectories(Storage, [&](llvm::StringRef Dir) {
|
|
SearchDirs.push_back(Dir);
|
|
return false;
|
|
});
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<const tooling::CompilationDatabase> CDB = nullptr;
|
|
bool ShouldBroadcast = false;
|
|
DirectoryCache *DirCache = nullptr;
|
|
for (DirectoryCache *Candidate : getDirectoryCaches(SearchDirs)) {
|
|
bool CandidateShouldBroadcast = Request.ShouldBroadcast;
|
|
if ((CDB = Candidate->get(Opts.TFS, CandidateShouldBroadcast,
|
|
Request.FreshTime, Request.FreshTimeMissing))) {
|
|
DirCache = Candidate;
|
|
ShouldBroadcast = CandidateShouldBroadcast;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!CDB)
|
|
return llvm::None;
|
|
|
|
CDBLookupResult Result;
|
|
Result.CDB = std::move(CDB);
|
|
Result.PI.SourceRoot = DirCache->Path;
|
|
|
|
if (ShouldBroadcast)
|
|
broadcastCDB(Result);
|
|
return Result;
|
|
}
|
|
|
|
// The broadcast thread announces files with new compile commands to the world.
|
|
// Primarily this is used to enqueue them for background indexing.
|
|
//
|
|
// It's on a separate thread because:
|
|
// - otherwise it would block the first parse of the initial file
|
|
// - we need to enumerate all files in the CDB, of which there are many
|
|
// - we (will) have to evaluate config for every file in the CDB, which is slow
|
|
class DirectoryBasedGlobalCompilationDatabase::BroadcastThread {
|
|
class Filter;
|
|
DirectoryBasedGlobalCompilationDatabase &Parent;
|
|
|
|
std::mutex Mu;
|
|
std::condition_variable CV;
|
|
// Shutdown flag (CV is notified after writing).
|
|
// This is atomic so that broadcasts can also observe it and abort early.
|
|
std::atomic<bool> ShouldStop = {false};
|
|
struct Task {
|
|
CDBLookupResult Lookup;
|
|
Context Ctx;
|
|
};
|
|
std::deque<Task> Queue;
|
|
llvm::Optional<Task> ActiveTask;
|
|
std::thread Thread; // Must be last member.
|
|
|
|
// Thread body: this is just the basic queue procesing boilerplate.
|
|
void run() {
|
|
std::unique_lock<std::mutex> Lock(Mu);
|
|
while (true) {
|
|
bool Stopping = false;
|
|
CV.wait(Lock, [&] {
|
|
return (Stopping = ShouldStop.load(std::memory_order_acquire)) ||
|
|
!Queue.empty();
|
|
});
|
|
if (Stopping) {
|
|
Queue.clear();
|
|
CV.notify_all();
|
|
return;
|
|
}
|
|
ActiveTask = std::move(Queue.front());
|
|
Queue.pop_front();
|
|
|
|
Lock.unlock();
|
|
{
|
|
WithContext WithCtx(std::move(ActiveTask->Ctx));
|
|
process(ActiveTask->Lookup);
|
|
}
|
|
Lock.lock();
|
|
ActiveTask.reset();
|
|
CV.notify_all();
|
|
}
|
|
}
|
|
|
|
// Inspects a new CDB and broadcasts the files it owns.
|
|
void process(const CDBLookupResult &T);
|
|
|
|
public:
|
|
BroadcastThread(DirectoryBasedGlobalCompilationDatabase &Parent)
|
|
: Parent(Parent), Thread([this] { run(); }) {}
|
|
|
|
void enqueue(CDBLookupResult Lookup) {
|
|
{
|
|
assert(!Lookup.PI.SourceRoot.empty());
|
|
std::lock_guard<std::mutex> Lock(Mu);
|
|
// New CDB takes precedence over any queued one for the same directory.
|
|
llvm::erase_if(Queue, [&](const Task &T) {
|
|
return T.Lookup.PI.SourceRoot == Lookup.PI.SourceRoot;
|
|
});
|
|
Queue.push_back({std::move(Lookup), Context::current().clone()});
|
|
}
|
|
CV.notify_all();
|
|
}
|
|
|
|
bool blockUntilIdle(Deadline Timeout) {
|
|
std::unique_lock<std::mutex> Lock(Mu);
|
|
return wait(Lock, CV, Timeout,
|
|
[&] { return Queue.empty() && !ActiveTask.hasValue(); });
|
|
}
|
|
|
|
~BroadcastThread() {
|
|
{
|
|
std::lock_guard<std::mutex> Lock(Mu);
|
|
ShouldStop.store(true, std::memory_order_release);
|
|
}
|
|
CV.notify_all();
|
|
Thread.join();
|
|
}
|
|
};
|
|
|
|
// The DirBasedCDB associates each file with a specific CDB.
|
|
// When a CDB is discovered, it may claim to describe files that we associate
|
|
// with a different CDB. We do not want to broadcast discovery of these, and
|
|
// trigger background indexing of them.
|
|
//
|
|
// We must filter the list, and check whether they are associated with this CDB.
|
|
// This class attempts to do so efficiently.
|
|
//
|
|
// Roughly, it:
|
|
// - loads the config for each file, and determines the relevant search path
|
|
// - gathers all directories that are part of any search path
|
|
// - (lazily) checks for a CDB in each such directory at most once
|
|
// - walks the search path for each file and determines whether to include it.
|
|
class DirectoryBasedGlobalCompilationDatabase::BroadcastThread::Filter {
|
|
llvm::StringRef ThisDir;
|
|
DirectoryBasedGlobalCompilationDatabase &Parent;
|
|
|
|
// Keep track of all directories we might check for CDBs.
|
|
struct DirInfo {
|
|
DirectoryCache *Cache = nullptr;
|
|
enum { Unknown, Missing, TargetCDB, OtherCDB } State = Unknown;
|
|
DirInfo *Parent = nullptr;
|
|
};
|
|
llvm::StringMap<DirInfo> Dirs;
|
|
|
|
// A search path starts at a directory, and either includes ancestors or not.
|
|
using SearchPath = llvm::PointerIntPair<DirInfo *, 1>;
|
|
|
|
// Add all ancestor directories of FilePath to the tracked set.
|
|
// Returns the immediate parent of the file.
|
|
DirInfo *addParents(llvm::StringRef FilePath) {
|
|
DirInfo *Leaf = nullptr;
|
|
DirInfo *Child = nullptr;
|
|
actOnAllParentDirectories(FilePath, [&](llvm::StringRef Dir) {
|
|
auto &Info = Dirs[Dir];
|
|
// If this is the first iteration, then this node is the overall result.
|
|
if (!Leaf)
|
|
Leaf = &Info;
|
|
// Fill in the parent link from the previous iteration to this parent.
|
|
if (Child)
|
|
Child->Parent = &Info;
|
|
// Keep walking, whether we inserted or not, if parent link is missing.
|
|
// (If it's present, parent links must be present up to the root, so stop)
|
|
Child = &Info;
|
|
return Info.Parent != nullptr;
|
|
});
|
|
return Leaf;
|
|
}
|
|
|
|
// Populates DirInfo::Cache (and State, if it is TargetCDB).
|
|
void grabCaches() {
|
|
// Fast path out if there were no files, or CDB loading is off.
|
|
if (Dirs.empty())
|
|
return;
|
|
|
|
std::vector<llvm::StringRef> DirKeys;
|
|
std::vector<DirInfo *> DirValues;
|
|
DirKeys.reserve(Dirs.size() + 1);
|
|
DirValues.reserve(Dirs.size());
|
|
for (auto &E : Dirs) {
|
|
DirKeys.push_back(E.first());
|
|
DirValues.push_back(&E.second);
|
|
}
|
|
|
|
// Also look up the cache entry for the CDB we're broadcasting.
|
|
// Comparing DirectoryCache pointers is more robust than checking string
|
|
// equality, e.g. reuses the case-sensitivity handling.
|
|
DirKeys.push_back(ThisDir);
|
|
auto DirCaches = Parent.getDirectoryCaches(DirKeys);
|
|
const DirectoryCache *ThisCache = DirCaches.back();
|
|
DirCaches.pop_back();
|
|
DirKeys.pop_back();
|
|
|
|
for (unsigned I = 0; I < DirKeys.size(); ++I) {
|
|
DirValues[I]->Cache = DirCaches[I];
|
|
if (DirCaches[I] == ThisCache)
|
|
DirValues[I]->State = DirInfo::TargetCDB;
|
|
}
|
|
}
|
|
|
|
// Should we include a file from this search path?
|
|
bool shouldInclude(SearchPath P) {
|
|
DirInfo *Info = P.getPointer();
|
|
if (!Info)
|
|
return false;
|
|
if (Info->State == DirInfo::Unknown) {
|
|
assert(Info->Cache && "grabCaches() should have filled this");
|
|
// Given that we know that CDBs have been moved/generated, don't trust
|
|
// caches. (This should be rare, so it's OK to add a little latency).
|
|
constexpr auto IgnoreCache = std::chrono::steady_clock::time_point::max();
|
|
// Don't broadcast CDBs discovered while broadcasting!
|
|
bool ShouldBroadcast = false;
|
|
bool Exists =
|
|
nullptr != Info->Cache->get(Parent.Opts.TFS, ShouldBroadcast,
|
|
/*FreshTime=*/IgnoreCache,
|
|
/*FreshTimeMissing=*/IgnoreCache);
|
|
Info->State = Exists ? DirInfo::OtherCDB : DirInfo::Missing;
|
|
}
|
|
// If we have a CDB, include the file if it's the target CDB only.
|
|
if (Info->State != DirInfo::Missing)
|
|
return Info->State == DirInfo::TargetCDB;
|
|
// If we have no CDB and no relevant parent, don't include the file.
|
|
if (!P.getInt() || !Info->Parent)
|
|
return false;
|
|
// Walk up to the next parent.
|
|
return shouldInclude(SearchPath(Info->Parent, 1));
|
|
}
|
|
|
|
public:
|
|
Filter(llvm::StringRef ThisDir,
|
|
DirectoryBasedGlobalCompilationDatabase &Parent)
|
|
: ThisDir(ThisDir), Parent(Parent) {}
|
|
|
|
std::vector<std::string> filter(std::vector<std::string> AllFiles,
|
|
std::atomic<bool> &ShouldStop) {
|
|
std::vector<std::string> Filtered;
|
|
// Allow for clean early-exit of the slow parts.
|
|
auto ExitEarly = [&] {
|
|
if (ShouldStop.load(std::memory_order_acquire)) {
|
|
log("Giving up on broadcasting CDB, as we're shutting down");
|
|
Filtered.clear();
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
// Compute search path for each file.
|
|
std::vector<SearchPath> SearchPaths(AllFiles.size());
|
|
for (unsigned I = 0; I < AllFiles.size(); ++I) {
|
|
if (Parent.Opts.CompileCommandsDir) { // FIXME: unify with config
|
|
SearchPaths[I].setPointer(
|
|
&Dirs[Parent.Opts.CompileCommandsDir.getValue()]);
|
|
continue;
|
|
}
|
|
if (ExitEarly()) // loading config may be slow
|
|
return Filtered;
|
|
WithContext WithProvidedContent(Parent.Opts.ContextProvider(AllFiles[I]));
|
|
const Config::CDBSearchSpec &Spec =
|
|
Config::current().CompileFlags.CDBSearch;
|
|
switch (Spec.Policy) {
|
|
case Config::CDBSearchSpec::NoCDBSearch:
|
|
break;
|
|
case Config::CDBSearchSpec::Ancestors:
|
|
SearchPaths[I].setInt(/*Recursive=*/1);
|
|
SearchPaths[I].setPointer(addParents(AllFiles[I]));
|
|
break;
|
|
case Config::CDBSearchSpec::FixedDir:
|
|
SearchPaths[I].setPointer(&Dirs[Spec.FixedCDBPath.getValue()]);
|
|
break;
|
|
}
|
|
}
|
|
// Get the CDB cache for each dir on the search path, but don't load yet.
|
|
grabCaches();
|
|
// Now work out which files we want to keep, loading CDBs where needed.
|
|
for (unsigned I = 0; I < AllFiles.size(); ++I) {
|
|
if (ExitEarly()) // loading CDBs may be slow
|
|
return Filtered;
|
|
if (shouldInclude(SearchPaths[I]))
|
|
Filtered.push_back(std::move(AllFiles[I]));
|
|
}
|
|
return Filtered;
|
|
}
|
|
};
|
|
|
|
void DirectoryBasedGlobalCompilationDatabase::BroadcastThread::process(
|
|
const CDBLookupResult &T) {
|
|
vlog("Broadcasting compilation database from {0}", T.PI.SourceRoot);
|
|
std::vector<std::string> GovernedFiles =
|
|
Filter(T.PI.SourceRoot, Parent).filter(T.CDB->getAllFiles(), ShouldStop);
|
|
if (!GovernedFiles.empty())
|
|
Parent.OnCommandChanged.broadcast(std::move(GovernedFiles));
|
|
}
|
|
|
|
void DirectoryBasedGlobalCompilationDatabase::broadcastCDB(
|
|
CDBLookupResult Result) const {
|
|
assert(Result.CDB && "Trying to broadcast an invalid CDB!");
|
|
Broadcaster->enqueue(Result);
|
|
}
|
|
|
|
bool DirectoryBasedGlobalCompilationDatabase::blockUntilIdle(
|
|
Deadline Timeout) const {
|
|
return Broadcaster->blockUntilIdle(Timeout);
|
|
}
|
|
|
|
llvm::Optional<ProjectInfo>
|
|
DirectoryBasedGlobalCompilationDatabase::getProjectInfo(PathRef File) const {
|
|
CDBLookupRequest Req;
|
|
Req.FileName = File;
|
|
Req.ShouldBroadcast = false;
|
|
Req.FreshTime = Req.FreshTimeMissing =
|
|
std::chrono::steady_clock::time_point::min();
|
|
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)
|
|
: DelegatingCDB(Base), ArgsAdjuster(std::move(Adjuster)),
|
|
FallbackFlags(std::move(FallbackFlags)) {}
|
|
|
|
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)
|
|
Cmd = DelegatingCDB::getCompileCommand(File);
|
|
if (!Cmd)
|
|
return llvm::None;
|
|
if (ArgsAdjuster)
|
|
Cmd->CommandLine = ArgsAdjuster(Cmd->CommandLine, File);
|
|
return Cmd;
|
|
}
|
|
|
|
tooling::CompileCommand OverlayCDB::getFallbackCommand(PathRef File) const {
|
|
auto Cmd = DelegatingCDB::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, File);
|
|
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});
|
|
}
|
|
|
|
DelegatingCDB::DelegatingCDB(const GlobalCompilationDatabase *Base)
|
|
: Base(Base) {
|
|
if (Base)
|
|
BaseChanged = Base->watch([this](const std::vector<std::string> Changes) {
|
|
OnCommandChanged.broadcast(Changes);
|
|
});
|
|
}
|
|
|
|
DelegatingCDB::DelegatingCDB(std::unique_ptr<GlobalCompilationDatabase> Base)
|
|
: DelegatingCDB(Base.get()) {
|
|
BaseOwner = std::move(Base);
|
|
}
|
|
|
|
llvm::Optional<tooling::CompileCommand>
|
|
DelegatingCDB::getCompileCommand(PathRef File) const {
|
|
if (!Base)
|
|
return llvm::None;
|
|
return Base->getCompileCommand(File);
|
|
}
|
|
|
|
llvm::Optional<ProjectInfo> DelegatingCDB::getProjectInfo(PathRef File) const {
|
|
if (!Base)
|
|
return llvm::None;
|
|
return Base->getProjectInfo(File);
|
|
}
|
|
|
|
tooling::CompileCommand DelegatingCDB::getFallbackCommand(PathRef File) const {
|
|
if (!Base)
|
|
return GlobalCompilationDatabase::getFallbackCommand(File);
|
|
return Base->getFallbackCommand(File);
|
|
}
|
|
|
|
bool DelegatingCDB::blockUntilIdle(Deadline D) const {
|
|
if (!Base)
|
|
return true;
|
|
return Base->blockUntilIdle(D);
|
|
}
|
|
|
|
} // namespace clangd
|
|
} // namespace clang
|