File indexing completed on 2024-05-19 05:41:59

0001 // ct_lvtclp_compilerutil.cpp                                         -*-C++-*-
0002 
0003 /*
0004 // Copyright 2023 Codethink Ltd <codethink@codethink.co.uk>
0005 // SPDX-License-Identifier: Apache-2.0
0006 //
0007 // Licensed under the Apache License, Version 2.0 (the "License");
0008 // you may not use this file except in compliance with the License.
0009 // You may obtain a copy of the License at
0010 //
0011 //     http://www.apache.org/licenses/LICENSE-2.0
0012 //
0013 // Unless required by applicable law or agreed to in writing, software
0014 // distributed under the License is distributed on an "AS IS" BASIS,
0015 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0016 // See the License for the specific language governing permissions and
0017 // limitations under the License.
0018 */
0019 
0020 #include <ct_lvtclp_compilerutil.h>
0021 #include <ct_lvtshr_stringhelpers.h>
0022 
0023 #include <filesystem>
0024 #include <initializer_list>
0025 #include <iostream>
0026 #include <istream>
0027 #include <optional>
0028 
0029 #include <QDebug>
0030 
0031 #include <clang/Frontend/FrontendActions.h>
0032 #include <clang/Tooling/Tooling.h>
0033 
0034 namespace {
0035 
0036 #ifdef __linux__
0037 std::string trimLeadingSpaces(const std::string& str)
0038 // remove leading spaces from str
0039 {
0040     std::size_t loc = str.find_first_not_of(' ');
0041     return str.substr(loc);
0042 }
0043 
0044 std::optional<std::vector<std::string>> tryCompiler(const std::string& compiler)
0045 {
0046     std::optional<std::string> compilerOut = Codethink::lvtclp::CompilerUtil::runCompiler(compiler);
0047     if (!compilerOut) {
0048         return {};
0049     }
0050 
0051     bool inIncludeSearchBlock = false;
0052     const char *blockStart = "#include <...> search starts here:";
0053     const char *blockEnd = "End of search list.";
0054 
0055     std::vector<std::string> includes;
0056 
0057     // now collect each line between blockStart and blockEnd
0058     std::stringstream ss(*compilerOut);
0059     std::string line;
0060     while (std::getline(ss, line)) {
0061         if (line == blockEnd) {
0062             break;
0063         }
0064 
0065         if (inIncludeSearchBlock) {
0066             includes.push_back(trimLeadingSpaces(line));
0067         }
0068 
0069         if (line == blockStart) {
0070             inIncludeSearchBlock = true;
0071         }
0072     }
0073 
0074     return includes;
0075 }
0076 
0077 std::vector<std::string> findLinuxIncludes()
0078 // run tryCompiler on clang++ or g++ with various version suffixes,
0079 // returning the first successful result
0080 {
0081     // lower versions first because hopefully things are backwards compatible
0082     std::initializer_list<std::string> suffixes = {
0083         "-9",
0084         "-10",
0085         "-11",
0086         "-12",
0087         "-13",
0088         "-14",
0089         "-15",
0090         "",
0091     };
0092 
0093     for (const char *compiler : {"clang++", "g++"}) {
0094         for (const std::string& suffix : suffixes) {
0095             const std::string fullName = compiler + suffix;
0096             std::optional<std::vector<std::string>> res = tryCompiler(fullName);
0097             if (res) {
0098                 return std::move(*res);
0099             }
0100         }
0101     }
0102 
0103     return {};
0104 }
0105 #endif // __linux__
0106 
0107 #ifdef __APPLE__
0108 static std::vector<std::string> findMacIncludes()
0109 {
0110     // Apple's clang has some include paths built in, which we don't get
0111     // from our open source libclang. We need to add this include path.
0112     // The path should look something like this, but we need to get the
0113     // right version number
0114     // /Library/Developer/CommandLineTools/usr/lib/clang/12.0.5/include
0115     // Asking apple's clang (e.g. using findLinuxIncludes) yields unbuildable
0116     // headers
0117 
0118     std::filesystem::path base("/Library/Developer/CommandLineTools/usr/lib/clang");
0119     if (!std::filesystem::is_directory(base)) {
0120         std::cerr << "Cannot find XCode CommandLineTools include directory: " << base << std::endl;
0121         return {};
0122     }
0123 
0124     // find version number by trying each subdirectory of base
0125     for (const auto& subdir : std::filesystem::directory_iterator(base)) {
0126         const std::filesystem::path& versionDir = subdir;
0127         std::filesystem::path includeDir = versionDir / "include";
0128 
0129         if (std::filesystem::is_directory(includeDir)) {
0130             return {includeDir.string()};
0131         }
0132     }
0133 
0134     std::cerr << "Cannot find XCode CommandLineTools include directory" << std::endl;
0135     return {};
0136 }
0137 #endif // __APPLE__
0138 
0139 } // unnamed namespace
0140 
0141 namespace Codethink::lvtclp {
0142 
0143 std::vector<std::string> CompilerUtil::findSystemIncludes()
0144 {
0145 #ifdef __APPLE__
0146     return findMacIncludes();
0147 #endif
0148 #ifdef __linux__
0149     return findLinuxIncludes();
0150 #endif
0151     return {};
0152 }
0153 
0154 #ifdef __linux__
0155 std::optional<std::string> CompilerUtil::runCompiler(const std::string& compiler)
0156 // attempt to run something like
0157 //      compiler -E -v - </dev/null 1>/dev/null 2>out
0158 // if compiler isn't in path, return {}
0159 // if compiler doesn't return EXIT_SUCCESS, return {}
0160 // otherwise return the contents of stderr
0161 {
0162     // The popen() function shall execute the command specified by the string command. It shall create a pipe between
0163     // the calling program and the executed command, and shall return a pointer to a stream that can be used to either
0164     // read from or write to the pipe.
0165     // https://pubs.opengroup.org/onlinepubs/009696699/functions/popen.html
0166     auto fp = popen("g++ -E -v -x c++ - </dev/null 2>&1", "r");
0167     if (fp == nullptr) {
0168         return {};
0169     }
0170 
0171     auto constexpr READ_SIZE = 4096;
0172     char output[READ_SIZE];
0173     std::string result;
0174     while (fgets(output, READ_SIZE, fp) != nullptr) {
0175         result += output;
0176     }
0177 
0178     // The pclose() function shall close a stream that was opened by popen(), wait for the command to terminate, and
0179     // return the termination status of the process that was running the command language interpreter. However, if a
0180     // call caused the termination status to be unavailable to pclose(), then pclose() shall return -1 with errno set
0181     // to [ECHILD] to report this situation.
0182     // https://pubs.opengroup.org/onlinepubs/009696699/functions/pclose.html
0183     auto status = pclose(fp);
0184     if (status != 0) {
0185         return {};
0186     }
0187 
0188     return result;
0189 }
0190 #endif
0191 
0192 std::optional<std::string> CompilerUtil::findBundledHeaders(bool silent)
0193 {
0194 #ifdef __APPLE__
0195     // the bundled headers are broken on MacOS. Fortunately we have working
0196     // system includes
0197     return {};
0198 #endif
0199 
0200 #ifdef CT_CLANG_HEADERS_RELATIVE_DIR
0201     // CT_LVT_BINDIR should contain the directory in which argv[0] is located.
0202     // CT_CLANG_HEADERS_RELATIVE_DIR should contain a relative path from that
0203     // directory to the installed copy of the clang tooling headers
0204     const char *env_p = std::getenv("CT_LVT_BINDIR");
0205     if (env_p) {
0206         auto env = std::string{env_p};
0207         const std::filesystem::path binDir(env);
0208         std::filesystem::path headersDir = binDir / CT_CLANG_HEADERS_RELATIVE_DIR;
0209         if (std::filesystem::is_regular_file(headersDir / "stddef.h")) {
0210             if (!silent) {
0211                 qDebug() << "Found clang header dir: " << headersDir.string();
0212             }
0213             return std::filesystem::canonical(headersDir).string();
0214         }
0215 
0216         // try appimage path
0217         headersDir = binDir / "lib" / "clang" / "include";
0218         if (std::filesystem::is_regular_file(headersDir / "stddef.h")) {
0219             if (!silent) {
0220                 qDebug() << "Found clang header dir: " << headersDir.string();
0221             }
0222             return std::filesystem::canonical(headersDir).string();
0223         }
0224         if (!silent) {
0225             qDebug() << "WARNING: broken installation: cannot find clang headers at " << headersDir.string();
0226         }
0227     }
0228     if (!silent) {
0229         qDebug() << "Finding headers with heuristics - expect compilation failures";
0230     }
0231 #endif // CT_CLANG_HEADERS_RELATIVE_DIR
0232 
0233     return {};
0234 }
0235 
0236 bool CompilerUtil::weNeedSystemHeaders()
0237 // Figure out if we can already find system headers without modifying anything
0238 {
0239     std::initializer_list<const char *> sources = {
0240         // stddef.h is the header mentioned in clang::tooling documentation
0241         "#include <stddef.h>",
0242         // weirdly MacOS finds stddef.h but then fails on stdarg.h so check
0243         // that too
0244         "#include <stdarg.h>",
0245     };
0246 
0247     for (const char *source : sources) {
0248         auto action = std::make_unique<clang::PreprocessOnlyAction>();
0249 
0250         // the tool will fail if it can't find the include
0251         bool ret = clang::tooling::runToolOnCode(std::move(action), source, "dummy.cpp");
0252         if (!ret) {
0253             return true;
0254         }
0255     }
0256 
0257     return false;
0258 }
0259 
0260 } // namespace Codethink::lvtclp