1 //===--- GlobalCompilationDatabase.cpp ---------------------------*- C++-*-===// 2 // 3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 4 // See https://llvm.org/LICENSE.txt for license information. 5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 6 // 7 //===----------------------------------------------------------------------===// 8 9 #include "GlobalCompilationDatabase.h" 10 #include "FS.h" 11 #include "Logger.h" 12 #include "Path.h" 13 #include "clang/Frontend/CompilerInvocation.h" 14 #include "clang/Tooling/ArgumentsAdjusters.h" 15 #include "clang/Tooling/CompilationDatabase.h" 16 #include "llvm/ADT/None.h" 17 #include "llvm/ADT/Optional.h" 18 #include "llvm/ADT/STLExtras.h" 19 #include "llvm/ADT/SmallString.h" 20 #include "llvm/Support/FileSystem.h" 21 #include "llvm/Support/Path.h" 22 #include <string> 23 #include <tuple> 24 #include <vector> 25 26 namespace clang { 27 namespace clangd { 28 namespace { 29 30 void adjustArguments(tooling::CompileCommand &Cmd, 31 llvm::StringRef ResourceDir) { 32 tooling::ArgumentsAdjuster ArgsAdjuster = tooling::combineAdjusters( 33 // clangd should not write files to disk, including dependency files 34 // requested on the command line. 35 tooling::getClangStripDependencyFileAdjuster(), 36 // Strip plugin related command line arguments. Clangd does 37 // not support plugins currently. Therefore it breaks if 38 // compiler tries to load plugins. 39 tooling::combineAdjusters(tooling::getStripPluginsAdjuster(), 40 tooling::getClangSyntaxOnlyAdjuster())); 41 42 Cmd.CommandLine = ArgsAdjuster(Cmd.CommandLine, Cmd.Filename); 43 // Inject the resource dir. 44 // FIXME: Don't overwrite it if it's already there. 45 if (!ResourceDir.empty()) 46 Cmd.CommandLine.push_back(("-resource-dir=" + ResourceDir).str()); 47 } 48 49 std::string getStandardResourceDir() { 50 static int Dummy; // Just an address in this process. 51 return CompilerInvocation::GetResourcesPath("clangd", (void *)&Dummy); 52 } 53 54 // Runs the given action on all parent directories of filename, starting from 55 // deepest directory and going up to root. Stops whenever action succeeds. 56 void actOnAllParentDirectories(PathRef FileName, 57 llvm::function_ref<bool(PathRef)> Action) { 58 for (auto Path = llvm::sys::path::parent_path(FileName); 59 !Path.empty() && !Action(Path); 60 Path = llvm::sys::path::parent_path(Path)) 61 ; 62 } 63 64 } // namespace 65 66 static std::string getFallbackClangPath() { 67 static int Dummy; 68 std::string ClangdExecutable = 69 llvm::sys::fs::getMainExecutable("clangd", (void *)&Dummy); 70 SmallString<128> ClangPath; 71 ClangPath = llvm::sys::path::parent_path(ClangdExecutable); 72 llvm::sys::path::append(ClangPath, "clang"); 73 return ClangPath.str(); 74 } 75 76 tooling::CompileCommand 77 GlobalCompilationDatabase::getFallbackCommand(PathRef File) const { 78 std::vector<std::string> Argv = {getFallbackClangPath()}; 79 // Clang treats .h files as C by default and files without extension as linker 80 // input, resulting in unhelpful diagnostics. 81 // Parsing as Objective C++ is friendly to more cases. 82 auto FileExtension = llvm::sys::path::extension(File); 83 if (FileExtension.empty() || FileExtension == ".h") 84 Argv.push_back("-xobjective-c++-header"); 85 Argv.push_back(File); 86 tooling::CompileCommand Cmd(llvm::sys::path::parent_path(File), 87 llvm::sys::path::filename(File), std::move(Argv), 88 /*Output=*/""); 89 Cmd.Heuristic = "clangd fallback"; 90 return Cmd; 91 } 92 93 DirectoryBasedGlobalCompilationDatabase:: 94 DirectoryBasedGlobalCompilationDatabase( 95 llvm::Optional<Path> CompileCommandsDir) 96 : CompileCommandsDir(std::move(CompileCommandsDir)) {} 97 98 DirectoryBasedGlobalCompilationDatabase:: 99 ~DirectoryBasedGlobalCompilationDatabase() = default; 100 101 llvm::Optional<tooling::CompileCommand> 102 DirectoryBasedGlobalCompilationDatabase::getCompileCommand(PathRef File) const { 103 CDBLookupRequest Req; 104 Req.FileName = File; 105 Req.ShouldBroadcast = true; 106 107 auto Res = lookupCDB(Req); 108 if (!Res) { 109 log("Failed to find compilation database for {0}", File); 110 return llvm::None; 111 } 112 113 auto Candidates = Res->CDB->getCompileCommands(File); 114 if (!Candidates.empty()) 115 return std::move(Candidates.front()); 116 117 return None; 118 } 119 120 // For platforms where paths are case-insensitive (but case-preserving), 121 // we need to do case-insensitive comparisons and use lowercase keys. 122 // FIXME: Make Path a real class with desired semantics instead. 123 // This class is not the only place this problem exists. 124 // FIXME: Mac filesystems default to case-insensitive, but may be sensitive. 125 126 static std::string maybeCaseFoldPath(PathRef Path) { 127 #if defined(_WIN32) || defined(__APPLE__) 128 return Path.lower(); 129 #else 130 return Path; 131 #endif 132 } 133 134 static bool pathEqual(PathRef A, PathRef B) { 135 #if defined(_WIN32) || defined(__APPLE__) 136 return A.equals_lower(B); 137 #else 138 return A == B; 139 #endif 140 } 141 142 DirectoryBasedGlobalCompilationDatabase::CachedCDB & 143 DirectoryBasedGlobalCompilationDatabase::getCDBInDirLocked(PathRef Dir) const { 144 // FIXME(ibiryukov): Invalidate cached compilation databases on changes 145 // FIXME(sammccall): this function hot, avoid copying key when hitting cache. 146 auto Key = maybeCaseFoldPath(Dir); 147 auto R = CompilationDatabases.try_emplace(Key); 148 if (R.second) { // Cache miss, try to load CDB. 149 CachedCDB &Entry = R.first->second; 150 std::string Error = ""; 151 Entry.CDB = tooling::CompilationDatabase::loadFromDirectory(Dir, Error); 152 Entry.Path = Dir; 153 } 154 return R.first->second; 155 } 156 157 llvm::Optional<DirectoryBasedGlobalCompilationDatabase::CDBLookupResult> 158 DirectoryBasedGlobalCompilationDatabase::lookupCDB( 159 CDBLookupRequest Request) const { 160 assert(llvm::sys::path::is_absolute(Request.FileName) && 161 "path must be absolute"); 162 163 bool ShouldBroadcast = false; 164 CDBLookupResult Result; 165 166 { 167 std::lock_guard<std::mutex> Lock(Mutex); 168 CachedCDB *Entry = nullptr; 169 if (CompileCommandsDir) { 170 Entry = &getCDBInDirLocked(*CompileCommandsDir); 171 } else { 172 // Traverse the canonical version to prevent false positives. i.e.: 173 // src/build/../a.cc can detect a CDB in /src/build if not canonicalized. 174 // FIXME(sammccall): this loop is hot, use a union-find-like structure. 175 actOnAllParentDirectories(removeDots(Request.FileName), 176 [&](PathRef Path) { 177 Entry = &getCDBInDirLocked(Path); 178 return Entry->CDB != nullptr; 179 }); 180 } 181 182 if (!Entry || !Entry->CDB) 183 return llvm::None; 184 185 // Mark CDB as broadcasted to make sure discovery is performed once. 186 if (Request.ShouldBroadcast && !Entry->SentBroadcast) { 187 Entry->SentBroadcast = true; 188 ShouldBroadcast = true; 189 } 190 191 Result.CDB = Entry->CDB.get(); 192 Result.PI.SourceRoot = Entry->Path; 193 } 194 195 // FIXME: Maybe make the following part async, since this can block retrieval 196 // of compile commands. 197 if (ShouldBroadcast) 198 broadcastCDB(Result); 199 return Result; 200 } 201 202 void DirectoryBasedGlobalCompilationDatabase::broadcastCDB( 203 CDBLookupResult Result) const { 204 assert(Result.CDB && "Trying to broadcast an invalid CDB!"); 205 206 std::vector<std::string> AllFiles = Result.CDB->getAllFiles(); 207 // We assume CDB in CompileCommandsDir owns all of its entries, since we don't 208 // perform any search in parent paths whenever it is set. 209 if (CompileCommandsDir) { 210 assert(*CompileCommandsDir == Result.PI.SourceRoot && 211 "Trying to broadcast a CDB outside of CompileCommandsDir!"); 212 OnCommandChanged.broadcast(std::move(AllFiles)); 213 return; 214 } 215 216 llvm::StringMap<bool> DirectoryHasCDB; 217 { 218 std::lock_guard<std::mutex> Lock(Mutex); 219 // Uniquify all parent directories of all files. 220 for (llvm::StringRef File : AllFiles) { 221 actOnAllParentDirectories(File, [&](PathRef Path) { 222 auto It = DirectoryHasCDB.try_emplace(Path); 223 // Already seen this path, and all of its parents. 224 if (!It.second) 225 return true; 226 227 CachedCDB &Entry = getCDBInDirLocked(Path); 228 It.first->second = Entry.CDB != nullptr; 229 return pathEqual(Path, Result.PI.SourceRoot); 230 }); 231 } 232 } 233 234 std::vector<std::string> GovernedFiles; 235 for (llvm::StringRef File : AllFiles) { 236 // A file is governed by this CDB if lookup for the file would find it. 237 // Independent of whether it has an entry for that file or not. 238 actOnAllParentDirectories(File, [&](PathRef Path) { 239 if (DirectoryHasCDB.lookup(Path)) { 240 if (pathEqual(Path, Result.PI.SourceRoot)) 241 // Make sure listeners always get a canonical path for the file. 242 GovernedFiles.push_back(removeDots(File)); 243 // Stop as soon as we hit a CDB. 244 return true; 245 } 246 return false; 247 }); 248 } 249 250 OnCommandChanged.broadcast(std::move(GovernedFiles)); 251 } 252 253 llvm::Optional<ProjectInfo> 254 DirectoryBasedGlobalCompilationDatabase::getProjectInfo(PathRef File) const { 255 CDBLookupRequest Req; 256 Req.FileName = File; 257 Req.ShouldBroadcast = false; 258 auto Res = lookupCDB(Req); 259 if (!Res) 260 return llvm::None; 261 return Res->PI; 262 } 263 264 OverlayCDB::OverlayCDB(const GlobalCompilationDatabase *Base, 265 std::vector<std::string> FallbackFlags, 266 llvm::Optional<std::string> ResourceDir) 267 : Base(Base), ResourceDir(ResourceDir ? std::move(*ResourceDir) 268 : getStandardResourceDir()), 269 FallbackFlags(std::move(FallbackFlags)) { 270 if (Base) 271 BaseChanged = Base->watch([this](const std::vector<std::string> Changes) { 272 OnCommandChanged.broadcast(Changes); 273 }); 274 } 275 276 llvm::Optional<tooling::CompileCommand> 277 OverlayCDB::getCompileCommand(PathRef File) const { 278 llvm::Optional<tooling::CompileCommand> Cmd; 279 { 280 std::lock_guard<std::mutex> Lock(Mutex); 281 auto It = Commands.find(removeDots(File)); 282 if (It != Commands.end()) 283 Cmd = It->second; 284 } 285 if (!Cmd && Base) 286 Cmd = Base->getCompileCommand(File); 287 if (!Cmd) 288 return llvm::None; 289 adjustArguments(*Cmd, ResourceDir); 290 return Cmd; 291 } 292 293 tooling::CompileCommand OverlayCDB::getFallbackCommand(PathRef File) const { 294 auto Cmd = Base ? Base->getFallbackCommand(File) 295 : GlobalCompilationDatabase::getFallbackCommand(File); 296 std::lock_guard<std::mutex> Lock(Mutex); 297 Cmd.CommandLine.insert(Cmd.CommandLine.end(), FallbackFlags.begin(), 298 FallbackFlags.end()); 299 adjustArguments(Cmd, ResourceDir); 300 return Cmd; 301 } 302 303 void OverlayCDB::setCompileCommand( 304 PathRef File, llvm::Optional<tooling::CompileCommand> Cmd) { 305 // We store a canonical version internally to prevent mismatches between set 306 // and get compile commands. Also it assures clients listening to broadcasts 307 // doesn't receive different names for the same file. 308 std::string CanonPath = removeDots(File); 309 { 310 std::unique_lock<std::mutex> Lock(Mutex); 311 if (Cmd) 312 Commands[CanonPath] = std::move(*Cmd); 313 else 314 Commands.erase(CanonPath); 315 } 316 OnCommandChanged.broadcast({CanonPath}); 317 } 318 319 llvm::Optional<ProjectInfo> OverlayCDB::getProjectInfo(PathRef File) const { 320 { 321 std::lock_guard<std::mutex> Lock(Mutex); 322 auto It = Commands.find(removeDots(File)); 323 if (It != Commands.end()) 324 return ProjectInfo{}; 325 } 326 if (Base) 327 return Base->getProjectInfo(File); 328 329 return llvm::None; 330 } 331 } // namespace clangd 332 } // namespace clang 333