diff options
Diffstat (limited to 'sources/shiboken6/ApiExtractor/clangparser/compilersupport.cpp')
-rw-r--r-- | sources/shiboken6/ApiExtractor/clangparser/compilersupport.cpp | 396 |
1 files changed, 396 insertions, 0 deletions
diff --git a/sources/shiboken6/ApiExtractor/clangparser/compilersupport.cpp b/sources/shiboken6/ApiExtractor/clangparser/compilersupport.cpp new file mode 100644 index 000000000..659324f8c --- /dev/null +++ b/sources/shiboken6/ApiExtractor/clangparser/compilersupport.cpp @@ -0,0 +1,396 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt for Python. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "compilersupport.h" +#include "header_paths.h" + +#include <reporthandler.h> + +#include <QtCore/QDebug> +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QFileInfo> +#include <QtCore/QProcess> +#include <QtCore/QStandardPaths> +#include <QtCore/QStringList> +#include <QtCore/QVersionNumber> + +#include <clang-c/Index.h> + +#include <string.h> +#include <algorithm> +#include <iterator> + +namespace clang { + +QVersionNumber libClangVersion() +{ + return QVersionNumber(CINDEX_VERSION_MAJOR, CINDEX_VERSION_MINOR); +} + +static bool runProcess(const QString &program, const QStringList &arguments, + QByteArray *stdOutIn = nullptr, QByteArray *stdErrIn = nullptr) +{ + QProcess process; + process.start(program, arguments, QProcess::ReadWrite); + if (!process.waitForStarted()) { + qWarning().noquote().nospace() << "Unable to start " + << process.program() << ": " << process.errorString(); + return false; + } + process.closeWriteChannel(); + const bool finished = process.waitForFinished(); + const QByteArray stdErr = process.readAllStandardError(); + if (stdErrIn) + *stdErrIn = stdErr; + if (stdOutIn) + *stdOutIn = process.readAllStandardOutput(); + + if (!finished) { + qWarning().noquote().nospace() << process.program() << " timed out: " << stdErr; + process.kill(); + return false; + } + + if (process.exitStatus() != QProcess::NormalExit) { + qWarning().noquote().nospace() << process.program() << " crashed: " << stdErr; + return false; + } + + if (process.exitCode() != 0) { + qWarning().noquote().nospace() << process.program() << " exited " + << process.exitCode() << ": " << stdErr; + return false; + } + + return true; +} + +#if defined(Q_CC_GNU) + +static QByteArray frameworkPath() { return QByteArrayLiteral(" (framework directory)"); } + +#if defined(Q_OS_MACOS) +static void filterHomebrewHeaderPaths(HeaderPaths &headerPaths) +{ + QByteArray homebrewPrefix = qgetenv("HOMEBREW_OPT"); + + // If HOMEBREW_OPT is found we assume that the build is happening + // inside a brew environment, which means we need to filter out + // the -isystem flags added by the brew clang shim. This is needed + // because brew passes the Qt include paths as system include paths + // and because our parser ignores system headers, Qt classes won't + // be found and thus compilation errors will occur. + if (homebrewPrefix.isEmpty()) + return; + + qCInfo(lcShiboken) << "Found HOMEBREW_OPT with value:" << homebrewPrefix + << "Assuming homebrew build environment."; + + HeaderPaths::iterator it = headerPaths.begin(); + while (it != headerPaths.end()) { + if (it->path.startsWith(homebrewPrefix)) { + qCInfo(lcShiboken) << "Filtering out homebrew include path: " + << it->path; + it = headerPaths.erase(it); + } else { + ++it; + } + } +} +#endif + +// Determine g++'s internal include paths from the output of +// g++ -E -x c++ - -v </dev/null +// Output looks like: +// #include <...> search starts here: +// /usr/local/include +// /System/Library/Frameworks (framework directory) +// End of search list. +static HeaderPaths gppInternalIncludePaths(const QString &compiler) +{ + HeaderPaths result; + QStringList arguments; + arguments << QStringLiteral("-E") << QStringLiteral("-x") << QStringLiteral("c++") + << QStringLiteral("-") << QStringLiteral("-v"); + QByteArray stdOut; + QByteArray stdErr; + if (!runProcess(compiler, arguments, &stdOut, &stdErr)) + return result; + const QByteArrayList stdErrLines = stdErr.split('\n'); + bool isIncludeDir = false; + for (const QByteArray &line : stdErrLines) { + if (isIncludeDir) { + if (line.startsWith(QByteArrayLiteral("End of search list"))) { + isIncludeDir = false; + } else { + HeaderPath headerPath{line.trimmed(), HeaderType::System}; + if (headerPath.path.endsWith(frameworkPath())) { + headerPath.type = HeaderType::FrameworkSystem; + headerPath.path.truncate(headerPath.path.size() - frameworkPath().size()); + } + result.append(headerPath); + } + } else if (line.startsWith(QByteArrayLiteral("#include <...> search starts here"))) { + isIncludeDir = true; + } + } + +#if defined(Q_OS_MACOS) + filterHomebrewHeaderPaths(result); +#endif + return result; +} +#endif // Q_CC_MSVC + +// Detect Vulkan as supported from Qt 5.10 by checking the environment variables. +static void detectVulkan(HeaderPaths *headerPaths) +{ + static const char *vulkanVariables[] = {"VULKAN_SDK", "VK_SDK_PATH"}; + for (const char *vulkanVariable : vulkanVariables) { + if (qEnvironmentVariableIsSet(vulkanVariable)) { + const QByteArray path = qgetenv(vulkanVariable) + QByteArrayLiteral("/include"); + headerPaths->append(HeaderPath{path, HeaderType::System}); + break; + } + } +} + +#if defined(Q_CC_GNU) +enum class LinuxDistribution { RedHat, CentOs, Other }; + +static LinuxDistribution linuxDistribution() +{ + const QString &productType = QSysInfo::productType(); + if (productType == QLatin1String("rhel")) + return LinuxDistribution::RedHat; + if (productType.compare(QLatin1String("centos"), Qt::CaseInsensitive) == 0) + return LinuxDistribution::CentOs; + return LinuxDistribution::Other; +} + +static bool checkProductVersion(const QVersionNumber &minimum, + const QVersionNumber &excludedMaximum) +{ + const QVersionNumber osVersion = QVersionNumber::fromString(QSysInfo::productVersion()); + return osVersion.isNull() || (osVersion >= minimum && osVersion < excludedMaximum); +} + +static inline bool needsGppInternalHeaders() +{ + const LinuxDistribution distro = linuxDistribution(); + switch (distro) { + case LinuxDistribution::RedHat: + case LinuxDistribution::CentOs: + return checkProductVersion(QVersionNumber(6, 10), QVersionNumber(8)); + case LinuxDistribution::Other: + break; + } + return false; +} +#endif // Q_CC_GNU + +// For MSVC, we set the MS compatibility version and let Clang figure out its own +// options and include paths. +// For the others, we pass "-nostdinc" since libclang tries to add it's own system +// include paths, which together with the clang compiler paths causes some clash +// which causes std types not being found and construct -I/-F options from the +// include paths of the host compiler. + +#ifdef Q_CC_CLANG +static QByteArray noStandardIncludeOption() { return QByteArrayLiteral("-nostdinc"); } +#endif + +// The clang builtin includes directory is used to find the definitions for +// intrinsic functions and builtin types. It is necessary to use the clang +// includes to prevent redefinition errors. The default toolchain includes +// should be picked up automatically by clang without specifying +// them implicitly. + +#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) +# define NEED_CLANG_BUILTIN_INCLUDES 1 +#else +# define NEED_CLANG_BUILTIN_INCLUDES 0 +#endif + +#if NEED_CLANG_BUILTIN_INCLUDES +static QString findClangLibDir() +{ + for (const char *envVar : {"LLVM_INSTALL_DIR", "CLANG_INSTALL_DIR"}) { + if (qEnvironmentVariableIsSet(envVar)) { + const QString path = QFile::decodeName(qgetenv(envVar)) + QLatin1String("/lib"); + if (QFileInfo::exists(path)) + return path; + } + } + const QString llvmConfig = + QStandardPaths::findExecutable(QLatin1String("llvm-config")); + if (!llvmConfig.isEmpty()) { + QByteArray stdOut; + if (runProcess(llvmConfig, QStringList{QLatin1String("--libdir")}, &stdOut)) { + const QString path = QFile::decodeName(stdOut.trimmed()); + if (QFileInfo::exists(path)) + return path; + } + } + return QString(); +} + +static QString findClangBuiltInIncludesDir() +{ + // Find the include directory of the highest version. + const QString clangPathLibDir = findClangLibDir(); + if (!clangPathLibDir.isEmpty()) { + QString candidate; + QVersionNumber lastVersionNumber(1, 0, 0); + QDir clangDir(clangPathLibDir + QLatin1String("/clang")); + const QFileInfoList versionDirs = + clangDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QFileInfo &fi : versionDirs) { + const QString fileName = fi.fileName(); + if (fileName.at(0).isDigit()) { + const QVersionNumber versionNumber = QVersionNumber::fromString(fileName); + if (!versionNumber.isNull() && versionNumber > lastVersionNumber) { + candidate = fi.absoluteFilePath(); + lastVersionNumber = versionNumber; + } + } + } + if (!candidate.isEmpty()) + return candidate + QStringLiteral("/include"); + } + return QString(); +} +#endif // NEED_CLANG_BUILTIN_INCLUDES + +#if defined(Q_CC_CLANG) || defined(Q_CC_GNU) +static QString compilerFromCMake(const QString &defaultCompiler) +{ +// Added !defined(Q_OS_DARWIN) due to PYSIDE-1032 +# if defined(CMAKE_CXX_COMPILER) && !defined(Q_OS_DARWIN) + Q_UNUSED(defaultCompiler) + return QString::fromLocal8Bit(CMAKE_CXX_COMPILER); +# else + return defaultCompiler; +# endif +} +#endif // Q_CC_CLANG, Q_CC_GNU + +// Returns clang options needed for emulating the host compiler +QByteArrayList emulatedCompilerOptions() +{ + QByteArrayList result; +#if defined(Q_CC_MSVC) + HeaderPaths headerPaths; + result.append(QByteArrayLiteral("-fms-compatibility-version=19.26.28806")); + result.append(QByteArrayLiteral("-fdelayed-template-parsing")); + result.append(QByteArrayLiteral("-Wno-microsoft-enum-value")); + // Fix yvals_core.h: STL1000: Unexpected compiler version, expected Clang 7 or newer (MSVC2017 update) + result.append(QByteArrayLiteral("-D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH")); +#elif defined(Q_CC_CLANG) + HeaderPaths headerPaths = gppInternalIncludePaths(compilerFromCMake(QStringLiteral("clang++"))); + result.append(noStandardIncludeOption()); +#elif defined(Q_CC_GNU) + HeaderPaths headerPaths; + +#if NEED_CLANG_BUILTIN_INCLUDES + const QString clangBuiltinIncludesDir = + QDir::toNativeSeparators(findClangBuiltInIncludesDir()); + if (clangBuiltinIncludesDir.isEmpty()) { + qCWarning(lcShiboken, "Unable to locate Clang's built-in include directory " + "(neither by checking the environment variables LLVM_INSTALL_DIR, CLANG_INSTALL_DIR " + " nor running llvm-config). This may lead to parse errors."); + } else { + qCInfo(lcShiboken, "CLANG builtins includes directory: %s", + qPrintable(clangBuiltinIncludesDir)); + headerPaths.append(HeaderPath{QFile::encodeName(clangBuiltinIncludesDir), + HeaderType::System}); + } +#endif // NEED_CLANG_BUILTIN_INCLUDES + + // Append the c++ include paths since Clang is unable to find <list> etc + // on RHEL 7 with g++ 6.3 or CentOS 7.2. + // A fix for this has been added to Clang 5.0, so, the code can be removed + // once Clang 5.0 is the minimum version. + if (needsGppInternalHeaders()) { + const HeaderPaths gppPaths = gppInternalIncludePaths(compilerFromCMake(QStringLiteral("g++"))); + for (const HeaderPath &h : gppPaths) { + if (h.path.contains("c++") + || h.path.contains("sysroot")) { // centOS + headerPaths.append(h); + } + } + } +#else + HeaderPaths headerPaths; +#endif + detectVulkan(&headerPaths); + std::transform(headerPaths.cbegin(), headerPaths.cend(), + std::back_inserter(result), HeaderPath::includeOption); + return result; +} + +LanguageLevel emulatedCompilerLanguageLevel() +{ + return LanguageLevel::Cpp17; +} + +struct LanguageLevelMapping +{ + const char *option; + LanguageLevel level; +}; + +static const LanguageLevelMapping languageLevelMapping[] = +{ + {"c++11", LanguageLevel::Cpp11}, + {"c++14", LanguageLevel::Cpp14}, + {"c++17", LanguageLevel::Cpp17}, + {"c++20", LanguageLevel::Cpp20}, + {"c++1z", LanguageLevel::Cpp1Z} +}; + +const char *languageLevelOption(LanguageLevel l) +{ + for (const LanguageLevelMapping &m : languageLevelMapping) { + if (m.level == l) + return m.option; + } + return nullptr; +} + +LanguageLevel languageLevelFromOption(const char *o) +{ + for (const LanguageLevelMapping &m : languageLevelMapping) { + if (!strcmp(m.option, o)) + return m.level; + } + return LanguageLevel::Default; +} + +} // namespace clang |