ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
tool.cpp File Reference

Go to the source code of this file.

Functions

std::mutex & outputRegistryMutex ()
 
std::unordered_set< std::string > & outputRegistry ()
 
bool registerOutputPath (const std::string &path)
 
void unregisterOutputPath (const std::string &path)
 
int main (int argc, const char **argv)
 

Function Documentation

◆ main()

int main ( int  argc,
const char **  argv 
)

Definition at line 813 of file defer/tool.cpp.

813 {
814 InitLLVM InitLLVM(argc, argv);
815
816 // Capture CWD before anything else can change it
817 g_originalCwd = fs::current_path();
818
819 // Hide all the LLVM internal options that aren't relevant to our tool
820 cl::HideUnrelatedOptions(ToolCategory);
821
822 cl::ParseCommandLineOptions(argc, argv, "ascii-defer transformation tool\n");
823
824 fs::path outputDir = fs::path(OutputDirectoryOption.getValue());
825 // Make output directory absolute relative to original CWD
826 if (!outputDir.is_absolute()) {
827 outputDir = g_originalCwd / outputDir;
828 }
829 fs::path inputRoot;
830 if (!InputRootOption.getValue().empty()) {
831 inputRoot = fs::path(InputRootOption.getValue());
832 } else {
833 inputRoot = fs::current_path();
834 }
835
836 // Make input root absolute for reliable path computation
837 if (!inputRoot.is_absolute()) {
838 std::error_code ec;
839 inputRoot = fs::absolute(inputRoot, ec);
840 if (ec) {
841 llvm::errs() << "Failed to resolve input root path: " << ec.message() << "\n";
842 return 1;
843 }
844 }
845
846 std::vector<std::string> sourcePaths;
847 for (const auto &path : SourcePaths) {
848 if (!path.empty()) {
849 sourcePaths.push_back(path);
850 }
851 }
852
853 if (sourcePaths.empty()) {
854 llvm::errs() << "No translation units specified for transformation. Provide positional source paths.\n";
855 return 1;
856 }
857
858 if (fs::exists(outputDir)) {
859 if (!fs::is_directory(outputDir)) {
860 llvm::errs() << "Output path exists and is not a directory: " << outputDir.c_str() << "\n";
861 return 1;
862 }
863 } else {
864 std::error_code errorCode;
865 fs::create_directories(outputDir, errorCode);
866 if (errorCode) {
867 llvm::errs() << "Failed to create output directory: " << outputDir.c_str() << " - " << errorCode.message()
868 << "\n";
869 return 1;
870 }
871 }
872
873 // Load compilation database
874 std::string buildPath = BuildPath.getValue();
875 if (buildPath.empty()) {
876 buildPath = ".";
877 }
878 std::string errorMessage;
879 std::unique_ptr<tooling::CompilationDatabase> compilations =
880 tooling::CompilationDatabase::loadFromDirectory(buildPath, errorMessage);
881 if (!compilations) {
882 llvm::errs() << "Error loading compilation database from '" << buildPath << "': " << errorMessage << "\n";
883 return 1;
884 }
885
886 tooling::ClangTool tool(*compilations, sourcePaths);
887
888 // Build the list of arguments to prepend for system header resolution
889 // LibTooling uses CC1 mode internally which has different include path handling than
890 // the clang driver. We use -Xclang to pass CC1-level flags that properly configure
891 // system include paths for LibTooling's CompilerInvocation.
892 std::vector<std::string> prependArgs;
893
894 // Try to find clang resource directory at runtime
895 // Priority: 1) CLANG_RESOURCE_DIR compile-time path, 2) Runtime detection via common paths
896 std::string resourceDir;
897
898#ifdef CLANG_RESOURCE_DIR
899 if (llvm::sys::fs::exists(CLANG_RESOURCE_DIR)) {
900 resourceDir = CLANG_RESOURCE_DIR;
901 // llvm::errs() << "Using embedded clang resource directory: " << resourceDir << "\n";
902 } else {
903 if (!QuietMode) {
904 llvm::errs() << "Embedded clang resource directory not found: " << CLANG_RESOURCE_DIR << "\n";
905 }
906 }
907#endif
908
909 // Runtime detection if embedded path doesn't work
910 if (resourceDir.empty()) {
911 // Common locations for clang resource directories
912 std::vector<std::string> searchPaths;
913
914#ifdef __APPLE__
915 // Homebrew LLVM on Apple Silicon
916 searchPaths.push_back("/opt/homebrew/opt/llvm/lib/clang");
917 // Homebrew LLVM on Intel Mac
918 searchPaths.push_back("/usr/local/opt/llvm/lib/clang");
919 // Xcode's clang
920 searchPaths.push_back(
921 "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang");
922 // CommandLineTools clang
923 searchPaths.push_back("/Library/Developer/CommandLineTools/usr/lib/clang");
924#endif
925#ifdef __linux__
926 // System LLVM installations
927 searchPaths.push_back("/usr/lib/llvm-22/lib/clang");
928 searchPaths.push_back("/usr/lib/llvm-21/lib/clang");
929 searchPaths.push_back("/usr/lib/llvm-20/lib/clang");
930 searchPaths.push_back("/usr/lib/clang");
931 searchPaths.push_back("/usr/local/lib/clang");
932#endif
933 // Universal fallback
934 searchPaths.push_back("/usr/local/lib/clang");
935
936 for (const auto &basePath : searchPaths) {
937 if (!llvm::sys::fs::exists(basePath)) {
938 continue;
939 }
940
941 // Find the highest version subdirectory
942 std::error_code ec;
943 std::string bestVersion;
944 int bestMajor = 0;
945
946 for (llvm::sys::fs::directory_iterator dir(basePath, ec), dirEnd; !ec && dir != dirEnd; dir.increment(ec)) {
947 std::string name = llvm::sys::path::filename(dir->path()).str();
948 // Parse version number (e.g., "22", "21.1.0", "21")
949 int major = 0;
950 if (sscanf(name.c_str(), "%d", &major) == 1 && major > bestMajor) {
951 bestMajor = major;
952 bestVersion = dir->path();
953 }
954 }
955
956 if (!bestVersion.empty()) {
957 resourceDir = bestVersion;
958 if (!QuietMode) {
959 llvm::errs() << "Found clang resource directory at runtime: " << resourceDir << "\n";
960 }
961 break;
962 }
963 }
964
965 if (resourceDir.empty() && !QuietMode) {
966 llvm::errs() << "Warning: Could not find clang resource directory\n";
967 }
968 }
969
970 if (!resourceDir.empty()) {
971 prependArgs.push_back(std::string("-resource-dir=") + resourceDir);
972 }
973
974 // Add target triple - LibTooling needs this to validate architecture-specific flags
975 // Without a target, flags like -mavx2 cause "unsupported option for target ''" errors
976#ifdef __APPLE__
977#ifdef __arm64__
978 prependArgs.push_back("-target");
979 prependArgs.push_back("arm64-apple-darwin");
980 // llvm::errs() << "Using target: arm64-apple-darwin\n";
981#else
982 prependArgs.push_back("-target");
983 prependArgs.push_back("x86_64-apple-darwin");
984 // llvm::errs() << "Using target: x86_64-apple-darwin\n";
985#endif
986#elif defined(__linux__)
987#ifdef __aarch64__
988 prependArgs.push_back("-target");
989 prependArgs.push_back("aarch64-linux-gnu");
990 // llvm::errs() << "Using target: aarch64-linux-gnu\n";
991#else
992 prependArgs.push_back("-target");
993 prependArgs.push_back("x86_64-linux-gnu");
994 // llvm::errs() << "Using target: x86_64-linux-gnu\n";
995#endif
996#endif
997
998 // Override the sysroot for macOS. Homebrew's LLVM config file sets -isysroot
999 // to CommandLineTools SDK, but we strip that from compile_commands.json and
1000 // set our own explicitly to ensure consistent behavior.
1001 std::string selectedSDK;
1002#ifdef __APPLE__
1003 {
1004 const char *sdkPaths[] = {
1005 // Xcode SDK (preferred - most complete headers and frameworks)
1006 "/Applications/Xcode.app/Contents/Developer/Platforms/"
1007 "MacOSX.platform/Developer/SDKs/MacOSX.sdk",
1008 // CommandLineTools SDK (fallback for users without Xcode)
1009 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk",
1010 };
1011
1012 for (const char *sdk : sdkPaths) {
1013 if (llvm::sys::fs::exists(sdk)) {
1014 selectedSDK = sdk;
1015 break;
1016 }
1017 }
1018
1019 if (!selectedSDK.empty()) {
1020 prependArgs.push_back("-isysroot");
1021 prependArgs.push_back(selectedSDK);
1022 // llvm::errs() << "Using macOS SDK: " << selectedSDK << "\n";
1023 } else {
1024 llvm::errs() << "Warning: No macOS SDK found, system headers may not be available\n";
1025 }
1026 }
1027#endif
1028
1029 // Build list of system include paths to add as -isystem paths.
1030 // LibTooling (cc1 mode) doesn't automatically add system include paths like
1031 // the clang driver does, so we add them explicitly:
1032 // 1. Clang's builtin headers (stdbool.h, stddef.h, etc.) - FIRST so they shadow SDK builtins
1033 // 2. SDK's usr/include (stdio.h, stdlib.h, etc.) - for system headers
1034 std::vector<std::string> appendArgs;
1035 if (!resourceDir.empty()) {
1036 std::string builtinInclude = resourceDir + "/include";
1037 if (llvm::sys::fs::exists(builtinInclude)) {
1038 appendArgs.push_back("-isystem");
1039 appendArgs.push_back(builtinInclude);
1040 // llvm::errs() << "Added clang builtin -isystem: " << builtinInclude << "\n";
1041 }
1042 }
1043 // NOTE: We intentionally do NOT add SDK's usr/include to the -isystem path.
1044 // LibTooling on LLVM 21 has a bug where __has_include_next() evaluates even
1045 // in non-taken preprocessor branches and creates VFS entries that can't be opened.
1046 // The clang builtin stdbool.h has:
1047 // #if defined(__MVS__) && __has_include_next(<stdbool.h>)
1048 // Even though __MVS__ is not defined on macOS, LLVM 21's LibTooling still tries
1049 // to resolve __has_include_next(<stdbool.h>), looks in SDK/usr/include, and fails
1050 // because stdbool.h doesn't exist there (it's a builtin header).
1051 //
1052 // By not adding SDK/usr/include, the __has_include_next won't find any entry
1053 // and will return false without trying to open a non-existent file.
1054 // The -isysroot flag still provides access to SDK headers through framework paths.
1055
1056 // Single consolidated argument adjuster that:
1057 // 1. Preserves compiler path (first arg)
1058 // 2. Inserts prepend args immediately after compiler (-nostdlibinc, -resource-dir, -isysroot)
1059 // 3. Strips unnecessary flags from remaining args
1060 // 4. Adds system include paths as -isystem at the end (after project -I paths)
1061 // 5. Adds defer tool parsing define BEFORE the "--" separator
1062 std::string inputRootStr = inputRoot.string();
1063 auto consolidatedAdjuster = [prependArgs, appendArgs, inputRootStr](const tooling::CommandLineArguments &args,
1064 StringRef) {
1065 // Helper to check if a path is under the project root
1066 auto isProjectPath = [&inputRootStr](const std::string &path) -> bool {
1067 if (inputRootStr.empty())
1068 return false;
1069 // Normalize both paths for comparison
1070 std::error_code ec;
1071 auto normalizedPath = fs::canonical(path, ec);
1072 if (ec)
1073 return false; // Path doesn't exist or can't be resolved
1074 auto normalizedRoot = fs::canonical(inputRootStr, ec);
1075 if (ec)
1076 return false;
1077 // Check if the path starts with the root
1078 std::string pathStr = normalizedPath.string();
1079 std::string rootStr = normalizedRoot.string();
1080 if (pathStr.find(rootStr) != 0)
1081 return false;
1082 // Exclude .deps-cache directory - these are cached dependencies that use
1083 // angled includes (<header.h>) and need -isystem, not -iquote
1084 if (pathStr.find("/.deps-cache/") != std::string::npos)
1085 return false;
1086 return true;
1087 };
1088 tooling::CommandLineArguments result;
1089
1090 if (args.empty()) {
1091 return result;
1092 }
1093
1094 // First: preserve the compiler path (first argument)
1095 result.push_back(args[0]);
1096
1097 // Second: add the prepend args right after the compiler
1098 for (const auto &arg : prependArgs) {
1099 result.push_back(arg);
1100 }
1101
1102 // Third: process remaining arguments, stripping unnecessary flags
1103 // IMPORTANT: Convert -I to -iquote for project include paths.
1104 // Clang include search order for <header.h>: -I paths -> -isystem paths -> system paths
1105 // Clang include search order for "header.h": -iquote paths -> -I paths -> -isystem paths -> system paths
1106 //
1107 // The problem: Project -I paths are searched BEFORE -isystem paths for <stdio.h>.
1108 // This causes LibTooling to look for <stdio.h> in the project's lib/ directory first.
1109 //
1110 // The fix: Convert project -I to -iquote. Then <stdio.h> skips project paths entirely
1111 // and finds system headers in -isystem paths instead.
1112 //
1113 // ALSO: Collect all -isystem paths and reorder them so clang builtins come FIRST.
1114 // LibTooling on LLVM 21 has a VFS bug where it creates phantom entries for headers
1115 // in -isystem directories even when the header doesn't exist there.
1116 std::vector<std::string> collectedIsystemPaths;
1117 bool foundSeparator = false;
1118 size_t separatorIndex = 0;
1119 for (size_t i = 1; i < args.size(); ++i) {
1120 const std::string &arg = args[i];
1121
1122 // When we hit "--", note its position and break
1123 if (arg == "--") {
1124 foundSeparator = true;
1125 separatorIndex = i;
1126 break;
1127 }
1128
1129 // Skip sanitizer flags
1130 if (arg.find("-fsanitize") != std::string::npos)
1131 continue;
1132 if (arg.find("-fno-sanitize") != std::string::npos)
1133 continue;
1134 // Skip debug info flags (not needed for AST parsing)
1135 if (arg == "-g" || arg == "-g2" || arg == "-g3")
1136 continue;
1137 if (arg == "-fno-eliminate-unused-debug-types")
1138 continue;
1139 if (arg == "-fno-inline")
1140 continue;
1141 // Strip -resource-dir flags and their arguments - we added our embedded path
1142 if (arg == "-resource-dir") {
1143 ++i;
1144 continue;
1145 }
1146 if (arg.find("-resource-dir=") == 0)
1147 continue;
1148 // Strip -isysroot flags and their arguments - we added our embedded SDK path
1149 if (arg == "-isysroot") {
1150 ++i;
1151 continue;
1152 }
1153 if (arg.find("-isysroot=") == 0 || (arg.find("-isysroot") == 0 && arg.length() > 9))
1154 continue;
1155
1156 // Collect -isystem paths instead of passing them through
1157 // We'll add them at the end in the correct order (clang builtins first)
1158 if (arg == "-isystem" && i + 1 < args.size()) {
1159 collectedIsystemPaths.push_back(args[++i]);
1160 continue;
1161 }
1162 if (arg.find("-isystem") == 0 && arg.length() > 8) {
1163 collectedIsystemPaths.push_back(arg.substr(8));
1164 continue;
1165 }
1166
1167 // Convert -I to -iquote for project include paths
1168 // Convert -I to -isystem for dependency paths (so they come after our clang builtins)
1169 // This prevents <stdbool.h> from being searched in dependency directories
1170 // before our clang builtin path
1171 // EXCEPTION: Keep -I for the public "include/" directory since it contains
1172 // public API headers meant to be included with angle brackets like <ascii-chat/header.h>
1173 if (arg == "-I" && i + 1 < args.size()) {
1174 // -I /path/to/dir (separate argument)
1175 const std::string &includePath = args[++i];
1176 if (isProjectPath(includePath) && includePath.find("/include") == std::string::npos) {
1177 // Project path but NOT the public include directory - convert to -iquote
1178 result.push_back("-iquote");
1179 result.push_back(includePath);
1180 } else if (!isProjectPath(includePath)) {
1181 // Collect dependency -I paths to add as -isystem after our builtins
1182 collectedIsystemPaths.push_back(includePath);
1183 } else {
1184 // Public include directory - keep as -I for angle-bracket includes
1185 result.push_back("-I");
1186 result.push_back(includePath);
1187 }
1188 continue;
1189 }
1190 if (arg.find("-I") == 0 && arg.length() > 2) {
1191 // -I/path/to/dir (combined form)
1192 std::string includePath = arg.substr(2);
1193 if (isProjectPath(includePath) && includePath.find("/include") == std::string::npos) {
1194 // Project path but NOT the public include directory - convert to -iquote
1195 result.push_back("-iquote");
1196 result.push_back(includePath);
1197 } else if (!isProjectPath(includePath)) {
1198 // Collect dependency -I paths to add as -isystem after our builtins
1199 collectedIsystemPaths.push_back(includePath);
1200 } else {
1201 // Public include directory - keep as -I for angle-bracket includes
1202 result.push_back("-I");
1203 result.push_back(includePath);
1204 }
1205 continue;
1206 }
1207
1208 result.push_back(arg);
1209 }
1210
1211 // Fourth: add system include paths in the correct order
1212 // Order matters for LibTooling on LLVM 21 - clang builtins MUST come first
1213 // to shadow any phantom VFS entries that might be created for other paths
1214 for (const auto &arg : appendArgs) {
1215 result.push_back(arg);
1216 }
1217 // Then add the collected -isystem paths from the compilation database
1218 for (const auto &path : collectedIsystemPaths) {
1219 result.push_back("-isystem");
1220 result.push_back(path);
1221 }
1222
1223 // Fifth: add the defer tool define and separator
1224 result.push_back("-DASCIICHAT_DEFER_TOOL_PARSING");
1225 if (foundSeparator) {
1226 result.push_back("--");
1227 // Copy any remaining args after "--"
1228 for (size_t i = separatorIndex + 1; i < args.size(); ++i) {
1229 result.push_back(args[i]);
1230 }
1231 }
1232
1233 return result;
1234 };
1235 tool.appendArgumentsAdjuster(consolidatedAdjuster);
1236
1237 DeferActionFactory actionFactory(outputDir, inputRoot);
1238 const int executionResult = tool.run(&actionFactory);
1239 if (executionResult != 0) {
1240 llvm::errs() << "Defer transformation failed with code " << executionResult << "\n";
1241 }
1242 return executionResult;
1243}
action_args_t args

References args.

◆ outputRegistry()

std::unordered_set< std::string > & outputRegistry ( )

Definition at line 39 of file defer/tool.cpp.

39 {
40 static std::unordered_set<std::string> registry;
41 return registry;
42}

Referenced by registerOutputPath(), and unregisterOutputPath().

◆ outputRegistryMutex()

std::mutex & outputRegistryMutex ( )

Definition at line 34 of file defer/tool.cpp.

34 {
35 static std::mutex mutex;
36 return mutex;
37}

Referenced by registerOutputPath(), and unregisterOutputPath().

◆ registerOutputPath()

bool registerOutputPath ( const std::string &  path)

Definition at line 44 of file defer/tool.cpp.

44 {
45 std::lock_guard<std::mutex> guard(outputRegistryMutex());
46 auto &registry = outputRegistry();
47 bool inserted = registry.insert(path).second;
48 return inserted;
49}
std::mutex & outputRegistryMutex()
std::unordered_set< std::string > & outputRegistry()

References outputRegistry(), and outputRegistryMutex().

◆ unregisterOutputPath()

void unregisterOutputPath ( const std::string &  path)

Definition at line 51 of file defer/tool.cpp.

51 {
52 std::lock_guard<std::mutex> guard(outputRegistryMutex());
53 outputRegistry().erase(path);
54}

References outputRegistry(), and outputRegistryMutex().