31#include <clang/Basic/DiagnosticIDs.h>
32#include <clang/Basic/DiagnosticOptions.h>
33#include <clang/Basic/SourceLocation.h>
34#include <clang/Basic/SourceManager.h>
36#if LLVM_VERSION_MAJOR >= 22
37#include <clang/Driver/CreateASTUnitFromArgs.h>
39#include <clang/Frontend/ASTUnit.h>
40#include <clang/Frontend/PCHContainerOperations.h>
41#include <clang/Lex/HeaderSearchOptions.h>
42#include <clang/Tooling/CompilationDatabase.h>
43#include <clang/Tooling/JSONCompilationDatabase.h>
44#include <clang/Tooling/Tooling.h>
45#include <llvm/ADT/ScopeExit.h>
46#include <llvm/ADT/StringExtras.h>
47#include <llvm/Config/llvm-config.h>
48#include <llvm/Support/FileSystem.h>
49#include <llvm/Support/MemoryBuffer.h>
50#include <llvm/Support/Path.h>
51#include <llvm/Support/Program.h>
52#include <llvm/Support/raw_ostream.h>
57namespace ct = clang::tooling;
63void emitStructuredMessage(
const std::string &level,
64 const std::string &message) {
65 llvm::errs() << acav::logfmt::formatDiagnosticLine(level,
"", 0, 0, message)
71std::string getExecutableDir() {
74 std::string execPath = llvm::sys::fs::getMainExecutable(
75 "acav",
reinterpret_cast<void *
>(&getExecutableDir));
77 if (execPath.empty()) {
81 llvm::SmallString<256> execDir(execPath);
82 llvm::sys::path::remove_filename(execDir);
83 return std::string(execDir);
89std::optional<std::string> findBundledResourceDir() {
90 std::string execDir = getExecutableDir();
91 if (execDir.empty()) {
96 llvm::SmallString<256> resourcePath(execDir);
97 llvm::sys::path::append(resourcePath,
"..",
"lib",
"clang",
98 std::to_string(LLVM_VERSION_MAJOR));
101 llvm::SmallString<256> normalizedPath;
102 if (std::error_code ec =
103 llvm::sys::fs::real_path(resourcePath, normalizedPath)) {
108 return std::string(normalizedPath);
111bool isValidClangResourceDir(llvm::StringRef resourceDir) {
112 if (resourceDir.empty() || !llvm::sys::fs::exists(resourceDir)) {
116 llvm::SmallString<256> includeDir(resourceDir);
117 llvm::sys::path::append(includeDir,
"include");
118 if (!llvm::sys::fs::exists(includeDir)) {
122 llvm::SmallString<256> stddefPath(includeDir);
123 llvm::sys::path::append(stddefPath,
"stddef.h");
124 return llvm::sys::fs::exists(stddefPath);
127std::optional<std::string> readFileToString(
const std::string &path) {
128 auto bufferOrErr = llvm::MemoryBuffer::getFile(path);
132 return (*bufferOrErr)->getBuffer().str();
135std::optional<std::string>
136runProgramCaptureStdout(llvm::StringRef program,
137 llvm::ArrayRef<llvm::StringRef> args) {
138 llvm::SmallString<256> stdoutPath;
139 if (llvm::sys::fs::createTemporaryFile(
"acav",
"stdout", stdoutPath)) {
144 bool executionFailed =
false;
145 std::array<std::optional<llvm::StringRef>, 3> redirects = {
146 std::nullopt, llvm::StringRef(stdoutPath),
150 int rc = llvm::sys::ExecuteAndWait(program, args, std::nullopt, redirects, 0,
151 0, &errMsg, &executionFailed);
152#if LLVM_VERSION_MAJOR >= 22
153 llvm::scope_exit cleanup([&]() { (void)llvm::sys::fs::remove(stdoutPath); });
156 llvm::make_scope_exit([&]() { (void)llvm::sys::fs::remove(stdoutPath); });
159 if (executionFailed || rc != 0) {
163 auto output = readFileToString(stdoutPath.str().str());
170std::optional<std::string> runClangWithArg(llvm::StringRef programPath,
171 llvm::StringRef arg) {
172 llvm::StringRef progName = llvm::sys::path::filename(programPath);
173 std::array<llvm::StringRef, 2> args = {progName, arg};
174 return runProgramCaptureStdout(programPath, args);
177int parseClangMajorVersion(
const std::string &versionOutput) {
182 constexpr llvm::StringLiteral versionToken(
"version");
183 std::size_t pos = versionOutput.find(versionToken.data());
184 if (pos == std::string::npos) {
187 pos += versionToken.size();
188 while (pos < versionOutput.size() &&
189 (versionOutput[pos] ==
' ' || versionOutput[pos] ==
'\t')) {
192 std::size_t start = pos;
193 while (pos < versionOutput.size() && std::isdigit(versionOutput[pos])) {
199 return std::atoi(versionOutput.substr(start, pos - start).c_str());
202std::optional<std::string>
203getClangResourceDirFromProgram(llvm::StringRef programPath) {
204 auto output = runClangWithArg(programPath,
"-print-resource-dir");
208 std::string resourceDir =
209 llvm::StringRef(*output).split(
'\n').first.trim().str();
210 if (!isValidClangResourceDir(resourceDir)) {
216std::optional<int> getClangProgramMajorVersion(llvm::StringRef programPath) {
217 auto output = runClangWithArg(programPath,
"--version");
221 int major = parseClangMajorVersion(*output);
228std::vector<std::string>
229stripResourceDirArgs(
const std::vector<std::string> &commandLine) {
230 std::vector<std::string> stripped;
231 stripped.reserve(commandLine.size());
233 for (std::size_t i = 0; i < commandLine.size(); ++i) {
234 const std::string &arg = commandLine[i];
235 if (arg ==
"-resource-dir") {
236 if (i + 1 < commandLine.size()) {
241 if (llvm::StringRef(arg).starts_with(
"-resource-dir=")) {
244 stripped.push_back(arg);
250class CallbackDiagnosticConsumer :
public clang::DiagnosticConsumer {
252 explicit CallbackDiagnosticConsumer(DiagnosticCallback callback)
253 : callback_(std::move(callback)) {}
255 void HandleDiagnostic(clang::DiagnosticsEngine::Level level,
256 const clang::Diagnostic &info)
override {
261 llvm::SmallString<256> message;
262 info.FormatDiagnostic(message);
264 DiagnosticMessage diag;
266 diag.message = message.str().str();
268 clang::SourceLocation loc = info.getLocation();
270 const clang::SourceManager &sm = info.getSourceManager();
271 clang::FullSourceLoc fullLoc(loc, sm);
272 if (fullLoc.isValid()) {
273 const llvm::StringRef filename =
274 sm.getFilename(fullLoc.getSpellingLoc());
275 if (!filename.empty()) {
276 diag.file = filename.str();
278 diag.line = fullLoc.getSpellingLineNumber();
279 diag.column = fullLoc.getSpellingColumnNumber();
287 DiagnosticCallback callback_;
291clang::DiagnosticConsumer *
292createDiagnosticConsumer(
const DiagnosticCallback &callback) {
294 return new CallbackDiagnosticConsumer(callback);
297 llvm::errs() << acav::logfmt::formatDiagnosticLine(diag.level, diag.file,
298 diag.line, diag.column,
302 return new CallbackDiagnosticConsumer(structuredFallback);
308static std::map<std::string, std::string>
309extractModuleFileMappings(
const std::string &compilationDb,
310 const std::string &sourcePath);
314 if (!overrideResourceDir.empty()) {
315 if (!isValidClangResourceDir(overrideResourceDir)) {
316 emitStructuredMessage(
"error",
"[clang] Invalid override resource dir: " +
317 overrideResourceDir);
320 emitStructuredMessage(
"info",
"[clang] Using override resource dir: " +
321 overrideResourceDir);
322 return overrideResourceDir;
328 if (
auto bundledDir = findBundledResourceDir()) {
329 if (isValidClangResourceDir(*bundledDir)) {
330 emitStructuredMessage(
"info",
"[clang] Using bundled resource dir: " +
334 emitStructuredMessage(
"debug",
335 "[clang] Bundled resource dir found but invalid: " +
341 constexpr int requiredMajor = LLVM_VERSION_MAJOR;
344 [&](llvm::StringRef programPath) -> std::optional<std::string> {
345 auto major = getClangProgramMajorVersion(programPath);
346 if (!major || *major != requiredMajor) {
347 emitStructuredMessage(
349 "[clang] Skipping clang binary (major mismatch): " +
350 programPath.str() +
" (found " +
351 (major ? std::to_string(*major) : std::string(
"unknown")) +
352 ", need " + std::to_string(requiredMajor) +
")");
355 return getClangResourceDirFromProgram(programPath);
358 const std::vector<std::string> candidateNames = {
359 "clang++-" + std::to_string(requiredMajor),
360 "clang-" + std::to_string(requiredMajor) +
"++",
364 for (
const std::string &name : candidateNames) {
365 auto programOrErr = llvm::sys::findProgramByName(name);
369 if (
auto dir = tryProgram(*programOrErr)) {
370 emitStructuredMessage(
"info",
371 "[clang] Resource dir (via clang++): " + *dir);
376 emitStructuredMessage(
377 "error",
"[clang] Failed to locate clang resource dir. Checked:\n"
378 " 1. Bundled: ../lib/clang/" +
379 std::to_string(requiredMajor) +
380 "/ (not found or invalid)\n"
381 " 2. System clang++ -print-resource-dir (not found or "
382 "version mismatch)");
386std::vector<std::string>
388 const std::string &clangResourceDir,
389 std::string &diagnostic) {
391 std::vector<std::string> adjusted = stripResourceDirArgs(commandLine);
394 bool hasSysroot =
false;
395 bool useMacOSSDK =
true;
396 for (std::size_t i = 0; i < adjusted.size(); ++i) {
397 const std::string &arg = adjusted[i];
398 const llvm::StringRef argRef(arg);
400 hasSysroot |= argRef.starts_with(
"-isysroot") || arg ==
"--sysroot" ||
401 argRef.starts_with(
"--sysroot=");
403 llvm::StringRef target;
404 if ((arg ==
"-target" || arg ==
"--target") && i + 1 < adjusted.size()) {
405 target = adjusted[i + 1];
406 }
else if (argRef.starts_with(
"--target=")) {
407 target = argRef.drop_front(llvm::StringRef(
"--target=").size());
408 }
else if (argRef.starts_with(
"-target=")) {
409 target = argRef.drop_front(llvm::StringRef(
"-target=").size());
412 if (!target.empty()) {
413 const std::string lowerTarget = target.lower();
414 const llvm::StringRef lowerTargetRef(lowerTarget);
415 useMacOSSDK = lowerTargetRef.contains(
"-apple-macos") ||
416 lowerTargetRef.contains(
"-apple-darwin");
420 if (!hasSysroot && useMacOSSDK) {
421 auto xcrunOrErr = llvm::sys::findProgramByName(
"xcrun");
422 const std::string xcrun = xcrunOrErr ? *xcrunOrErr :
"/usr/bin/xcrun";
423 if (!llvm::sys::fs::exists(xcrun)) {
424 diagnostic =
"unable to find xcrun to discover the active macOS SDK";
426 std::array<llvm::StringRef, 4> args = {
"xcrun",
"--sdk",
"macosx",
428 auto output = runProgramCaptureStdout(xcrun, args);
430 diagnostic =
"xcrun failed while discovering the active macOS SDK";
432 std::string sdkPath =
433 llvm::StringRef(*output).split(
'\n').first.trim().str();
434 if (sdkPath.empty() || !llvm::sys::fs::is_directory(sdkPath)) {
435 diagnostic =
"xcrun returned an invalid macOS SDK path: " + sdkPath;
437 adjusted.push_back(
"-isysroot");
438 adjusted.push_back(sdkPath);
445 if (!clangResourceDir.empty()) {
446 adjusted.push_back(
"-resource-dir");
447 adjusted.push_back(clangResourceDir);
453std::unique_ptr<clang::ASTUnit>
455 const std::string &sourcePath, std::string &errorMessage,
456 const DiagnosticCallback &diagnosticCallback,
457 const std::string &clangResourceDirOverride) {
459 std::string loadError;
460 std::unique_ptr<ct::CompilationDatabase> compdb =
461 ct::JSONCompilationDatabase::loadFromFile(
462 compilationDatabase, loadError,
463 clang::tooling::JSONCommandLineSyntax::AutoDetect);
466 errorMessage =
"Failed to load compilation database: " + loadError;
472 compdb = ct::inferToolLocation(std::move(compdb));
473 compdb = ct::inferTargetAndDriverMode(std::move(compdb));
476 compdb = ct::expandResponseFiles(std::move(compdb),
477 llvm::vfs::getRealFileSystem());
480 std::vector<clang::tooling::CompileCommand> commands =
481 compdb->getCompileCommands(sourcePath);
483 if (commands.empty()) {
484 errorMessage =
"No compile command found for source file: " + sourcePath;
490 if (resourceDir.empty()) {
491 errorMessage =
"Get clang resource dir failed";
496 const clang::tooling::CompileCommand &cmd = commands[0];
502 llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> vfs =
503 llvm::vfs::getRealFileSystem();
504 if (std::error_code ec = vfs->setCurrentWorkingDirectory(cmd.Directory)) {
505 errorMessage =
"Failed to set working directory to '" + cmd.Directory +
506 "': " + ec.message();
511#if LLVM_VERSION_MAJOR >= 21
512 auto diagOpts = std::make_shared<clang::DiagnosticOptions>();
513 clang::IntrusiveRefCntPtr<clang::DiagnosticsEngine> diags(
514 new clang::DiagnosticsEngine(
515 clang::IntrusiveRefCntPtr<clang::DiagnosticIDs>(
516 new clang::DiagnosticIDs()),
517 *diagOpts, createDiagnosticConsumer(diagnosticCallback)));
519 clang::IntrusiveRefCntPtr<clang::DiagnosticOptions> diagOpts(
520 new clang::DiagnosticOptions());
521 clang::IntrusiveRefCntPtr<clang::DiagnosticsEngine> diags(
522 new clang::DiagnosticsEngine(
523 clang::IntrusiveRefCntPtr<clang::DiagnosticIDs>(
524 new clang::DiagnosticIDs()),
525 diagOpts, createDiagnosticConsumer(diagnosticCallback)));
528 std::string toolchainDiagnostic;
529 std::vector<std::string> adjustedCommandLine =
531 toolchainDiagnostic);
532 if (!toolchainDiagnostic.empty()) {
533 emitStructuredMessage(
"warning",
"[toolchain] " + toolchainDiagnostic);
537 std::vector<const char *> args;
538 for (
const auto &arg : adjustedCommandLine) {
539 args.push_back(arg.c_str());
541 if (std::find(adjustedCommandLine.begin(), adjustedCommandLine.end(),
542 "-fparse-all-comments") == adjustedCommandLine.end()) {
543 args.push_back(
"-fparse-all-comments");
547 std::string expandedCommand =
"[make-ast] Expanded command:";
548 for (
const auto *arg : args) {
549 expandedCommand +=
" ";
550 expandedCommand += arg;
552 emitStructuredMessage(
"debug", expandedCommand);
555 auto pchOps = std::make_shared<clang::PCHContainerOperations>();
560#if LLVM_VERSION_MAJOR >= 22
561 std::unique_ptr<clang::ASTUnit> astUnit = clang::CreateASTUnitFromCommandLine(
562 args.data(), args.data() + args.size(), pchOps, diagOpts, diags,
566 false, clang::CaptureDiagsKind::None,
569 0, clang::TU_Complete,
573 clang::SkipFunctionBodiesScope::None,
581#elif LLVM_VERSION_MAJOR >= 21
583 std::unique_ptr<clang::ASTUnit> astUnit = clang::ASTUnit::LoadFromCommandLine(
584 args.data(), args.data() + args.size(), pchOps, diagOpts, diags,
588 false, clang::CaptureDiagsKind::None,
591 0, clang::TU_Complete,
595 clang::SkipFunctionBodiesScope::None,
605 std::unique_ptr<clang::ASTUnit> astUnit = clang::ASTUnit::LoadFromCommandLine(
606 args.data(), args.data() + args.size(), pchOps, diags, resourceDir,
609 false, clang::CaptureDiagsKind::None,
612 0, clang::TU_Complete,
616 clang::SkipFunctionBodiesScope::None,
627 errorMessage =
"Failed to create AST for source file: " + sourcePath;
631 return std::move(astUnit);
634bool saveAst(clang::ASTUnit &astUnit,
const std::string &outputPath,
635 std::string &errorMessage) {
637 if (astUnit.Save(outputPath)) {
638 errorMessage =
"Failed to save AST to: " + outputPath;
644std::unique_ptr<clang::ASTUnit>
646 const std::string &compilationDbPath,
647 const std::string &sourcePath,
648 const DiagnosticCallback &diagnosticCallback) {
650 emitStructuredMessage(
"debug",
"[loadAstFromFile] AST file: " + astFilePath);
651 emitStructuredMessage(
652 "debug",
"[loadAstFromFile] Compilation DB: " +
653 (compilationDbPath.empty() ?
"(none)" : compilationDbPath));
656 std::map<std::string, std::string> moduleFileMappings;
657 std::string workingDir;
659 if (!compilationDbPath.empty() && !sourcePath.empty()) {
661 extractModuleFileMappings(compilationDbPath, sourcePath);
664 llvm::SmallString<256> compDbDir(compilationDbPath);
665 llvm::sys::path::remove_filename(compDbDir);
666 workingDir = std::string(compDbDir);
669 emitStructuredMessage(
"debug",
670 "[loadAstFromFile] Working dir: " +
671 (workingDir.empty() ?
"(empty)" : workingDir));
672 emitStructuredMessage(
"debug",
"[loadAstFromFile] Module mappings: " +
673 std::to_string(moduleFileMappings.size()) +
677#if LLVM_VERSION_MAJOR >= 21
678 auto diagOpts = std::make_shared<clang::DiagnosticOptions>();
679 clang::IntrusiveRefCntPtr<clang::DiagnosticsEngine> diags(
680 new clang::DiagnosticsEngine(
681 clang::IntrusiveRefCntPtr<clang::DiagnosticIDs>(
682 new clang::DiagnosticIDs()),
683 *diagOpts, createDiagnosticConsumer(diagnosticCallback)));
685 clang::IntrusiveRefCntPtr<clang::DiagnosticOptions> diagOpts(
686 new clang::DiagnosticOptions());
687 clang::IntrusiveRefCntPtr<clang::DiagnosticsEngine> diags(
688 new clang::DiagnosticsEngine(
689 clang::IntrusiveRefCntPtr<clang::DiagnosticIDs>(
690 new clang::DiagnosticIDs()),
691 diagOpts, createDiagnosticConsumer(diagnosticCallback)));
695 auto pchContainerOps = std::make_shared<clang::PCHContainerOperations>();
700 llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> vfs =
701 llvm::vfs::getRealFileSystem();
702 if (!workingDir.empty()) {
703 if (std::error_code ec = vfs->setCurrentWorkingDirectory(workingDir)) {
705 emitStructuredMessage(
"warning",
"[loadAstFromFile] Warning: Failed to "
706 "set VFS working directory to '" +
707 workingDir +
"': " + ec.message());
712 clang::FileSystemOptions fsOpts;
713 if (!workingDir.empty()) {
714 fsOpts.WorkingDir = workingDir;
720 clang::HeaderSearchOptions hsOpts;
721 for (
const auto &[moduleName, pcmPath] : moduleFileMappings) {
722 hsOpts.PrebuiltModuleFiles[moduleName] = pcmPath;
723 emitStructuredMessage(
"debug",
"[loadAstFromFile] Added module mapping: " +
724 moduleName +
" -> " + pcmPath);
728 if (!workingDir.empty()) {
729 hsOpts.PrebuiltModulePaths.push_back(workingDir);
737#if LLVM_VERSION_MAJOR >= 22
739 std::unique_ptr<clang::ASTUnit> astUnit = clang::ASTUnit::LoadFromASTFile(
740 astFilePath, pchContainerOps->getRawReader(),
741 clang::ASTUnit::LoadEverything, vfs, diagOpts, diags, fsOpts,
744 false, clang::CaptureDiagsKind::None,
747#elif LLVM_VERSION_MAJOR >= 21
749 std::unique_ptr<clang::ASTUnit> astUnit = clang::ASTUnit::LoadFromASTFile(
750 astFilePath, pchContainerOps->getRawReader(),
751 clang::ASTUnit::LoadEverything, diagOpts, diags, fsOpts, hsOpts,
753 false, clang::CaptureDiagsKind::None,
759 auto hsOptsPtr = std::make_shared<clang::HeaderSearchOptions>(hsOpts);
760 std::unique_ptr<clang::ASTUnit> astUnit = clang::ASTUnit::LoadFromASTFile(
761 astFilePath, pchContainerOps->getRawReader(),
762 clang::ASTUnit::LoadEverything, diags, fsOpts, hsOptsPtr,
764 false, clang::CaptureDiagsKind::None,
771 errorMessage =
"Failed to load AST from file: " + astFilePath;
775 return std::move(astUnit);
778std::vector<std::string>
780 std::string &errorMessage) {
783 std::string loadError;
784 std::unique_ptr<ct::CompilationDatabase> compilationDatabase =
785 ct::JSONCompilationDatabase::loadFromFile(
786 compDbPath, loadError, ct::JSONCommandLineSyntax::AutoDetect);
788 if (!compilationDatabase) {
789 errorMessage =
"Failed to load compilation database: " + loadError;
794 std::vector<std::string> sourceFiles = compilationDatabase->getAllFiles();
796 if (sourceFiles.empty()) {
797 errorMessage =
"No source files found in compilation database";
806static std::map<std::string, std::string>
807extractModuleFileMappings(
const std::string &compilationDb,
808 const std::string &sourcePath) {
809 std::map<std::string, std::string> mappings;
812 std::string loadError;
813 std::unique_ptr<ct::CompilationDatabase> compdb =
814 ct::JSONCompilationDatabase::loadFromFile(
815 compilationDb, loadError, ct::JSONCommandLineSyntax::AutoDetect);
818 emitStructuredMessage(
820 "[extractModuleFileMappings] Failed to load compilation database: " +
826 compdb = ct::expandResponseFiles(std::move(compdb),
827 llvm::vfs::getRealFileSystem());
830 std::vector<ct::CompileCommand> commands =
831 compdb->getCompileCommands(sourcePath);
833 if (commands.empty()) {
834 emitStructuredMessage(
836 "[extractModuleFileMappings] No compile command found for: " +
841 const ct::CompileCommand &cmd = commands.front();
842 std::string workingDir = cmd.Directory;
845 for (
const std::string &arg : cmd.CommandLine) {
847 if (arg.rfind(
"-fmodule-file=", 0) == 0) {
849 std::string nameAndPath = arg.substr(14);
852 size_t eqPos = nameAndPath.find(
'=');
853 if (eqPos != std::string::npos) {
854 std::string moduleName = nameAndPath.substr(0, eqPos);
855 std::string pcmPath = nameAndPath.substr(eqPos + 1);
858 if (!pcmPath.empty() && pcmPath[0] !=
'/') {
859 pcmPath = workingDir +
"/" + pcmPath;
862 mappings[moduleName] = pcmPath;
863 emitStructuredMessage(
"debug",
864 "[extractModuleFileMappings] Found mapping: " +
865 moduleName +
" -> " + pcmPath);
Utilities for interacting with Clang at runtime This includes runtime detection of Clang paths and AS...
std::vector< std::string > getSourceFilesFromCompilationDatabase(const std::string &compDbPath, std::string &errorMessage)
Extract source file paths from a compilation database.
std::unique_ptr< clang::ASTUnit > createAstFromCDB(const std::string &compilationDatabase, const std::string &sourcePath, std::string &errorMessage, const DiagnosticCallback &diagnosticCallback=nullptr, const std::string &clangResourceDirOverride="")
Create AST from a given compilation database This function provides a easy way to generate clang AST ...
bool saveAst(clang::ASTUnit &astUnit, const std::string &outputPath, std::string &errorMessage)
Save ast to local file.
std::string getClangResourceDir(const std::string &overrideResourceDir="")
Get clang resource directory.
std::unique_ptr< clang::ASTUnit > loadAstFromFile(const std::string &astFilePath, std::string &errorMessage, const std::string &compilationDbPath="", const std::string &sourcePath="", const DiagnosticCallback &diagnosticCallback=nullptr)
Load AST from local file.
std::vector< std::string > buildToolchainAdjustedCommandLine(const std::vector< std::string > &commandLine, const std::string &clangResourceDir, std::string &diagnostic)
Normalize a Clang command line for ACAV's embedded Clang.