/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qbs. ** ** $QT_BEGIN_LICENSE:LGPL$ ** 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 Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** 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-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "msvcinfo.h" #include "visualstudioversioninfo.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #endif #include #include #include using namespace qbs; using namespace qbs::Internal; static std::recursive_mutex envMutex; static QString mkStr(const char *s) { return QString::fromLocal8Bit(s); } static QString mkStr(const QByteArray &ba) { return mkStr(ba.constData()); } class TemporaryEnvChanger { public: TemporaryEnvChanger(const QProcessEnvironment &envChanges) : m_locker(envMutex) { QProcessEnvironment currentEnv = QProcessEnvironment::systemEnvironment(); const auto keys = envChanges.keys(); for (const QString &key : keys) { m_changesToRestore.insert(key, currentEnv.value(key)); qputenv(qPrintable(key), qPrintable(envChanges.value(key))); } } ~TemporaryEnvChanger() { const auto keys = m_changesToRestore.keys(); for (const QString &key : keys) qputenv(qPrintable(key), qPrintable(m_changesToRestore.value(key))); } private: QProcessEnvironment m_changesToRestore; std::lock_guard m_locker; }; static QByteArray runProcess(const QString &exeFilePath, const QStringList &args, const QProcessEnvironment &env = QProcessEnvironment(), bool allowFailure = false, const QByteArray &pipeData = QByteArray()) { TemporaryEnvChanger envChanger(env); QProcess process; process.start(exeFilePath, args); if (!process.waitForStarted()) throw ErrorInfo(mkStr("Could not start %1 (%2)").arg(exeFilePath, process.errorString())); if (!pipeData.isEmpty()) { process.write(pipeData); process.closeWriteChannel(); } if (!process.waitForFinished(-1) || process.exitStatus() != QProcess::NormalExit) throw ErrorInfo(mkStr("Could not run %1 (%2)").arg(exeFilePath, process.errorString())); if (process.exitCode() != 0 && !allowFailure) { ErrorInfo e(mkStr("Process '%1' failed with exit code %2.") .arg(exeFilePath).arg(process.exitCode())); const QByteArray stdErr = process.readAllStandardError(); if (!stdErr.isEmpty()) e.append(mkStr("stderr was: %1").arg(mkStr(stdErr))); const QByteArray stdOut = process.readAllStandardOutput(); if (!stdOut.isEmpty()) e.append(mkStr("stdout was: %1").arg(mkStr(stdOut))); throw e; } return process.readAllStandardOutput().trimmed(); } class DummyFile { public: DummyFile(QString fp) : filePath(std::move(fp)) { } ~DummyFile() { QFile::remove(filePath); } const QString filePath; }; #ifdef Q_OS_WIN static QStringList parseCommandLine(const QString &commandLine) { const auto buf = std::make_unique(size_t(commandLine.size()) + 1); buf[size_t(commandLine.toWCharArray(buf.get()))] = 0; int argCount = 0; const auto argsDeleter = [](LPWSTR *p){ LocalFree(p); }; const auto args = std::unique_ptr( CommandLineToArgvW(buf.get(), &argCount), argsDeleter); if (!args) throw ErrorInfo(mkStr("Could not parse command line arguments: ") + commandLine); QStringList list; list.reserve(argCount); for (int i = 0; i < argCount; ++i) list.push_back(QString::fromWCharArray(args[size_t(i)])); return list; } #endif static QVariantMap getMsvcDefines(const QString &compilerFilePath, const QProcessEnvironment &compilerEnv, MSVC::CompilerLanguage language) { #ifdef Q_OS_WIN QString backendSwitch, languageSwitch; switch (language) { case MSVC::CLanguage: backendSwitch = QStringLiteral("/B1"); languageSwitch = QStringLiteral("/TC"); break; case MSVC::CPlusPlusLanguage: backendSwitch = QStringLiteral("/Bx"); languageSwitch = QStringLiteral("/TP"); break; } const QByteArray commands("set MSC_CMD_FLAGS\n"); QStringList out = QString::fromLocal8Bit(runProcess(compilerFilePath, QStringList() << QStringLiteral("/nologo") << backendSwitch << qEnvironmentVariable("COMSPEC") << QStringLiteral("/c") << languageSwitch << QStringLiteral("NUL"), compilerEnv, true, commands)).split(QLatin1Char('\n')); auto findResult = std::find_if(out.cbegin(), out.cend(), [] (const QString &line) { return line.startsWith(QLatin1String("MSC_CMD_FLAGS=")); }); if (findResult == out.cend()) { throw ErrorInfo(QStringLiteral("Unexpected compiler frontend output: ") + out.join(QLatin1Char('\n'))); } QVariantMap map; const QStringList args = parseCommandLine(findResult->trimmed()); for (const QString &arg : args) { if (!arg.startsWith(QStringLiteral("-D"))) continue; int idx = arg.indexOf(QLatin1Char('='), 2); if (idx > 2) map.insert(arg.mid(2, idx - 2), arg.mid(idx + 1)); else map.insert(arg.mid(2), QVariant()); } return map; #else Q_UNUSED(compilerFilePath); Q_UNUSED(compilerEnv); Q_UNUSED(language); return {}; #endif } /*! \internal clang-cl does not support gcc and msvc ways to dump a macros, so we have to use original clang.exe directly */ static QVariantMap getClangClDefines( const QString &compilerFilePath, const QProcessEnvironment &compilerEnv, MSVC::CompilerLanguage language, const QString &arch) { #ifdef Q_OS_WIN QFileInfo clInfo(compilerFilePath); QFileInfo clangInfo(clInfo.absolutePath() + QLatin1String("/clang-cl.exe")); if (!clangInfo.exists()) throw ErrorInfo(QStringLiteral("%1 does not exist").arg(clangInfo.absoluteFilePath())); QStringList args = { QStringLiteral("/d1PP"), // dump macros QStringLiteral("/E") // preprocess to stdout }; if (language == MSVC::CLanguage) args.append(QStringLiteral("/TC")); else if (language == MSVC::CPlusPlusLanguage) args.append(QStringLiteral("/TP")); if (arch == QLatin1String("x86")) args.append(QStringLiteral("-m32")); else if (arch == QLatin1String("x86_64")) args.append(QStringLiteral("-m64")); args.append(QStringLiteral("NUL")); // filename const auto lines = QString::fromLocal8Bit( runProcess( clangInfo.absoluteFilePath(), args, compilerEnv, true)).split(QLatin1Char('\n')); QVariantMap result; for (const auto &line: lines) { static const auto defineString = QLatin1String("#define "); if (!line.startsWith(defineString)) continue; QStringView view(line.data() + defineString.size()); const auto it = std::find(view.begin(), view.end(), QLatin1Char(' ')); if (it == view.end()) { throw ErrorInfo(QStringLiteral("Unexpected compiler frontend output: ") + lines.join(QLatin1Char('\n'))); } QStringView key(view.begin(), it); QStringView value(it + 1, view.end()); result.insert(key.toString(), value.isEmpty() ? QVariant() : QVariant(value.toString())); } if (result.isEmpty()) { throw ErrorInfo(QStringLiteral("Cannot determine macroses from compiler frontend output: ") + lines.join(QLatin1Char('\n'))); } return result; #else Q_UNUSED(compilerFilePath); Q_UNUSED(compilerEnv); Q_UNUSED(language); Q_UNUSED(arch); return {}; #endif } static QString formatVswhereOutput(const QString &out, const QString &err) { QString ret; if (!out.isEmpty()) { ret.append(Tr::tr("stdout")).append(QLatin1String(":\n")); const auto lines = out.split(QLatin1Char('\n')); for (const QString &line : lines) ret.append(QLatin1Char('\t')).append(line).append(QLatin1Char('\n')); } if (!err.isEmpty()) { ret.append(Tr::tr("stderr")).append(QLatin1String(":\n")); const auto lines = err.split(QLatin1Char('\n')); for (const QString &line : lines) ret.append(QLatin1Char('\t')).append(line).append(QLatin1Char('\n')); } return ret; } static QString wow6432Key() { #ifdef Q_OS_WIN64 return QStringLiteral("\\Wow6432Node"); #else return {}; #endif } static QString vswhereFilePath() { static const std::vector envVarCandidates{"ProgramFiles", "ProgramFiles(x86)"}; for (const char * const envVar : envVarCandidates) { const QString value = QDir::fromNativeSeparators(QString::fromLocal8Bit(qgetenv(envVar))); const QString cmd = value + QStringLiteral("/Microsoft Visual Studio/Installer/vswhere.exe"); if (QFileInfo(cmd).exists()) return cmd; } return {}; } enum class ProductType { VisualStudio, BuildTools }; static std::vector retrieveInstancesFromVSWhere( ProductType productType, Logger &logger) { std::vector result; const QString cmd = vswhereFilePath(); if (cmd.isEmpty()) return result; QProcess vsWhere; QStringList args = productType == ProductType::VisualStudio ? QStringList({QStringLiteral("-all"), QStringLiteral("-legacy"), QStringLiteral("-prerelease")}) : QStringList({QStringLiteral("-products"), QStringLiteral("Microsoft.VisualStudio.Product.BuildTools")}); args << QStringLiteral("-format") << QStringLiteral("json") << QStringLiteral("-utf8"); vsWhere.start(cmd, args); if (!vsWhere.waitForStarted(-1)) return result; if (!vsWhere.waitForFinished(-1)) { logger.qbsWarning() << Tr::tr("The vswhere tool failed to run").append(QLatin1String(": ")) .append(vsWhere.errorString()); return result; } if (vsWhere.exitCode() != 0) { const QString stdOut = QString::fromLocal8Bit(vsWhere.readAllStandardOutput()); const QString stdErr = QString::fromLocal8Bit(vsWhere.readAllStandardError()); logger.qbsWarning() << Tr::tr("The vswhere tool failed to run").append(QLatin1String(".\n")) .append(formatVswhereOutput(stdOut, stdErr)); return result; } QJsonParseError parseError{}; QJsonDocument jsonOutput = QJsonDocument::fromJson(vsWhere.readAllStandardOutput(), &parseError); if (parseError.error != QJsonParseError::NoError) { logger.qbsWarning() << Tr::tr("The vswhere tool produced invalid JSON output: %1") .arg(parseError.errorString()); return result; } const auto jsonArray = jsonOutput.array(); for (const QJsonValue &v : jsonArray) { const QJsonObject o = v.toObject(); MSVCInstallInfo info; info.version = o.value(QStringLiteral("installationVersion")).toString(); if (productType == ProductType::BuildTools) { // For build tools, the version is e.g. "15.8.28010.2036", rather than "15.0". const int dotIndex = info.version.indexOf(QLatin1Char('.')); if (dotIndex != -1) info.version = info.version.left(dotIndex); } info.installDir = o.value(QStringLiteral("installationPath")).toString(); if (!info.version.isEmpty() && !info.installDir.isEmpty()) result.push_back(info); } return result; } static std::vector installedMSVCsFromVsWhere(Logger &logger) { const std::vector vsInstallations = retrieveInstancesFromVSWhere(ProductType::VisualStudio, logger); const std::vector buildToolInstallations = retrieveInstancesFromVSWhere(ProductType::BuildTools, logger); std::vector all; std::copy(vsInstallations.begin(), vsInstallations.end(), std::back_inserter(all)); std::copy(buildToolInstallations.begin(), buildToolInstallations.end(), std::back_inserter(all)); return all; } static std::vector installedMSVCsFromRegistry() { std::vector result; // Detect Visual Studio const QSettings vsRegistry( QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE") + wow6432Key() + QStringLiteral("\\Microsoft\\VisualStudio\\SxS\\VS7"), QSettings::NativeFormat); const auto vsNames = vsRegistry.childKeys(); for (const QString &vsName : vsNames) { MSVCInstallInfo entry; entry.version = vsName; entry.installDir = vsRegistry.value(vsName).toString(); result.push_back(entry); } // Detect Visual C++ Build Tools QSettings vcbtRegistry( QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE") + wow6432Key() + QStringLiteral("\\Microsoft\\VisualCppBuildTools"), QSettings::NativeFormat); const QStringList &vcbtRegistryChildGroups = vcbtRegistry.childGroups(); for (const QString &childGroup : vcbtRegistryChildGroups) { vcbtRegistry.beginGroup(childGroup); bool ok; int installed = vcbtRegistry.value(QStringLiteral("Installed")).toInt(&ok); if (ok && installed) { MSVCInstallInfo entry; entry.version = childGroup; const QSettings vsRegistry( QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE") + wow6432Key() + QStringLiteral("\\Microsoft\\VisualStudio\\") + childGroup + QStringLiteral("\\Setup\\VC"), QSettings::NativeFormat); entry.installDir = vsRegistry.value(QStringLiteral("ProductDir")).toString(); result.push_back(entry); } vcbtRegistry.endGroup(); } return result; } /* Returns the list of compilers present in all MSVC installations (Visual Studios or Build Tools) without the architecture, e.g. [VC\Tools\MSVC\14.16.27023, VC\Tools\MSVC\14.14.26428, ...] */ static std::vector installedCompilersHelper(Logger &logger) { std::vector msvcs; std::vector installInfos = installedMSVCsFromVsWhere(logger); if (installInfos.empty()) installInfos = installedMSVCsFromRegistry(); for (const MSVCInstallInfo &installInfo : installInfos) { MSVC msvc; msvc.internalVsVersion = Version::fromString(installInfo.version, true); if (!msvc.internalVsVersion.isValid()) continue; QDir vsInstallDir(installInfo.installDir); msvc.vsInstallPath = vsInstallDir.absolutePath(); if (vsInstallDir.dirName() != QStringLiteral("VC") && !vsInstallDir.cd(QStringLiteral("VC"))) { continue; } msvc.version = QString::number(Internal::VisualStudioVersionInfo( msvc.internalVsVersion).marketingVersion()); if (msvc.version.isEmpty()) { logger.qbsWarning() << Tr::tr("Unknown MSVC version %1 found.").arg(installInfo.version); continue; } if (msvc.internalVsVersion.majorVersion() < 15) { QDir vcInstallDir = vsInstallDir; if (!vcInstallDir.cd(QStringLiteral("bin"))) continue; msvc.vcInstallPath = vcInstallDir.absolutePath(); msvcs.push_back(msvc); } else { QDir vcInstallDir = vsInstallDir; vcInstallDir.cd(QStringLiteral("Tools/MSVC")); const auto vcVersionStrs = vcInstallDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); std::vector vcVersions; vcVersions.reserve(vcVersionStrs.size()); for (const QString &vcVersionStr : vcVersionStrs) { const Version vcVersion = Version::fromString(vcVersionStr); if (!vcVersion.isValid()) continue; vcVersions.push_back(vcVersion); } // sort the versions so the new one comes first std::sort(vcVersions.begin(), vcVersions.end(), std::greater<>()); for (const Version &vcVersion : vcVersions) { QDir specificVcInstallDir = vcInstallDir; if (!specificVcInstallDir.cd(vcVersion.toString()) || !specificVcInstallDir.cd(QStringLiteral("bin"))) { continue; } msvc.vcInstallPath = specificVcInstallDir.absolutePath(); msvcs.push_back(msvc); } } } return msvcs; } void MSVC::init() { determineCompilerVersion(); } /*! \internal Returns the architecture detected from the compiler path. */ QString MSVC::architectureFromClPath(const QString &clPath) { const auto parentDir = QFileInfo(clPath).absolutePath(); const auto parentDirName = QFileInfo(parentDir).fileName().toLower(); // can be the case when cl.exe is present within the Windows SDK installation... but can it? if (parentDirName == QLatin1String("bin")) return QStringLiteral("x86"); return parentDirName; } QString MSVC::vcVariablesVersionFromBinPath(const QString &binPath) { const auto binDirName = QFileInfo(binPath).fileName().toLower(); // the case when cl.exe is present within the Windows SDK installation if (binDirName == QLatin1String("bin")) return {}; // binPath is something like // Microsoft Visual Studio 14.0/VC/bin/amd64_x86 // or // Microsoft Visual Studio/2019/Community/VC/Tools/MSVC/14.28.29910/bin/Hostx64/x64 QDir dir(binPath); dir.cdUp(); // older Visual Studios do not support multiple compiler versions if (dir.dirName().toLower() == QLatin1String("bin")) return {}; dir.cdUp(); dir.cdUp(); return dir.dirName(); } QString MSVC::canonicalArchitecture(const QString &arch) { if (arch == QLatin1String("x64") || arch == QLatin1String("amd64")) return QStringLiteral("x86_64"); return arch; } std::pair MSVC::getHostTargetArchPair(const QString &arch) { QString hostArch; QString targetArch; const int index = arch.indexOf(QLatin1Char('_')); if (index != -1) { hostArch = arch.mid(0, index); targetArch = arch.mid(index); } else { hostArch = arch; targetArch = arch; } return {canonicalArchitecture(hostArch), canonicalArchitecture(targetArch)}; } QString MSVC::binPathForArchitecture(const QString &arch) const { QString archSubDir; if (arch != StringConstants::x86Arch()) archSubDir = arch; return QDir::cleanPath(vcInstallPath + QLatin1Char('/') + pathPrefix + QLatin1Char('/') + archSubDir); } static QString clExeSuffix() { return QStringLiteral("/cl.exe"); } QString MSVC::clPathForArchitecture(const QString &arch) const { return binPathForArchitecture(arch) + clExeSuffix(); } QVariantMap MSVC::compilerDefines(const QString &compilerFilePath, MSVC::CompilerLanguage language) const { const auto compilerName = QFileInfo(compilerFilePath).fileName().toLower(); if (compilerName == QLatin1String("clang-cl.exe")) return getClangClDefines(compilerFilePath, environment, language, architecture); return getMsvcDefines(compilerFilePath, environment, language); } std::vector MSVC::findSupportedArchitectures(const MSVC &msvc) { std::vector result; auto addResult = [&result](const MSVCArchInfo &ai) { if (QFile::exists(ai.binPath + QLatin1String("/cl.exe"))) result.push_back(ai); }; if (msvc.internalVsVersion.majorVersion() < 15) { static const QStringList knownArchitectures = QStringList() << QStringLiteral("x86") << QStringLiteral("amd64_x86") << QStringLiteral("amd64") << QStringLiteral("x86_amd64") << QStringLiteral("ia64") << QStringLiteral("x86_ia64") << QStringLiteral("x86_arm") << QStringLiteral("amd64_arm"); for (const QString &knownArchitecture : knownArchitectures) { MSVCArchInfo ai; ai.arch = knownArchitecture; ai.binPath = msvc.binPathForArchitecture(knownArchitecture); addResult(ai); } } else { QDir vcInstallDir(msvc.vcInstallPath); const auto hostArchs = vcInstallDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString &hostArch : hostArchs) { QDir subdir = vcInstallDir; if (!subdir.cd(hostArch)) continue; const QString shortHostArch = hostArch.mid(4).toLower(); const auto archs = subdir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString &arch : archs) { MSVCArchInfo ai; ai.binPath = subdir.absoluteFilePath(arch); if (shortHostArch == arch) ai.arch = arch; else ai.arch = shortHostArch + QLatin1Char('_') + arch; addResult(ai); } } } return result; } QVariantMap MSVC::toVariantMap() const { return { {QStringLiteral("version"), version}, {QStringLiteral("internalVsVersion"), internalVsVersion.toString()}, {QStringLiteral("vsInstallPath"), vsInstallPath}, {QStringLiteral("vcInstallPath"), vcInstallPath}, {QStringLiteral("binPath"), binPath}, {QStringLiteral("architecture"), architecture}, }; } /*! \internal Returns the list of all compilers present in all MSVC installations separated by host/target arch, e.g. [ VC\Tools\MSVC\14.16.27023\bin\Hostx64\x64, VC\Tools\MSVC\14.16.27023\bin\Hostx64\x86, VC\Tools\MSVC\14.16.27023\bin\Hostx64\arm, VC\Tools\MSVC\14.16.27023\bin\Hostx64\arm64, VC\Tools\MSVC\14.16.27023\bin\Hostx86\x64, ... ] \note that MSVC.architecture can be either "x64" or "amd64" (depending on the MSVC version) in case of 64-bit platform (but we use the "x86_64" name...) */ std::vector MSVC::installedCompilers(Logger &logger) { std::vector msvcs; const auto instMsvcs = installedCompilersHelper(logger); for (const MSVC &msvc : instMsvcs) { if (msvc.internalVsVersion.majorVersion() < 15) { // Check existence of various install scripts const QString vcvars32bat = msvc.vcInstallPath + QLatin1String("/vcvars32.bat"); if (!QFileInfo(vcvars32bat).isFile()) continue; } const auto ais = findSupportedArchitectures(msvc); for (const MSVCArchInfo &ai : ais) { MSVC specificMSVC = msvc; specificMSVC.architecture = ai.arch; specificMSVC.binPath = ai.binPath; msvcs.push_back(specificMSVC); } } return msvcs; } void MSVC::determineCompilerVersion() { QString cppFilePath; { QTemporaryFile cppFile(QDir::tempPath() + QLatin1String("/qbsXXXXXX.cpp")); cppFile.setAutoRemove(false); if (!cppFile.open()) { throw ErrorInfo(mkStr("Could not create temporary file (%1)") .arg(cppFile.errorString())); } cppFilePath = cppFile.fileName(); cppFile.write("_MSC_FULL_VER"); cppFile.close(); } DummyFile fileDeleter(cppFilePath); std::lock_guard locker(envMutex); const QByteArray origPath = qgetenv("PATH"); qputenv("PATH", environment.value(StringConstants::pathEnvVar()).toLatin1() + ';' + origPath); QByteArray versionStr = runProcess( binPath + clExeSuffix(), QStringList() << QStringLiteral("/nologo") << QStringLiteral("/EP") << QDir::toNativeSeparators(cppFilePath)); qputenv("PATH", origPath); compilerVersion = Version(versionStr.mid(0, 2).toInt(), versionStr.mid(2, 2).toInt(), versionStr.mid(4).toInt()); } QString MSVCInstallInfo::findVcvarsallBat() const { static const auto vcvarsall2017 = QStringLiteral("VC/Auxiliary/Build/vcvarsall.bat"); // 2015, 2013 and 2012 static const auto vcvarsallOld = QStringLiteral("VC/vcvarsall.bat"); QDir dir(installDir); if (dir.exists(vcvarsall2017)) return dir.absoluteFilePath(vcvarsall2017); if (dir.exists(vcvarsallOld)) return dir.absoluteFilePath(vcvarsallOld); return {}; } std::vector MSVCInstallInfo::installedMSVCs(Logger &logger) { const auto installInfos = installedMSVCsFromVsWhere(logger); if (installInfos.empty()) return installedMSVCsFromRegistry(); return installInfos; }