/**************************************************************************** ** ** 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 "setupqt.h" #include "../shared/logging/consolelogger.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace qbs { using Internal::none_of; using Internal::contains; using Internal::HostOsInfo; using Internal::Tr; static QStringList qmakeExecutableNames() { const QString baseName = HostOsInfo::appendExecutableSuffix(QStringLiteral("qmake")); QStringList lst(baseName); if (HostOsInfo::isLinuxHost()) { // Some distributions ship binaries called qmake-qt5 or qmake-qt4. lst << baseName + QLatin1String("-qt5") << baseName + QLatin1String("-qt4"); } return lst; } static QStringList collectQmakePaths() { const QStringList qmakeExeNames = qmakeExecutableNames(); QStringList qmakePaths; QByteArray environmentPath = qgetenv("PATH"); const QList environmentPaths = environmentPath.split(HostOsInfo::pathListSeparator().toLatin1()); for (const QByteArray &path : environmentPaths) { for (const QString &qmakeExecutableName : qmakeExeNames) { QFileInfo pathFileInfo(QDir(QLatin1String(path)), qmakeExecutableName); if (pathFileInfo.exists()) { QString qmakePath = pathFileInfo.absoluteFilePath(); if (!qmakePaths.contains(qmakePath)) qmakePaths.push_back(qmakePath); } } } return qmakePaths; } bool SetupQt::isQMakePathValid(const QString &qmakePath) { QFileInfo qmakeFileInfo(qmakePath); return qmakeFileInfo.exists() && qmakeFileInfo.isFile() && qmakeFileInfo.isExecutable(); } std::vector SetupQt::fetchEnvironments() { std::vector qtEnvironments; const auto qmakePaths = collectQmakePaths(); for (const QString &qmakePath : qmakePaths) { const EnhancedQtEnvironment env = fetchEnvironment(qmakePath); if (none_of(qtEnvironments, [&env](const EnhancedQtEnvironment &otherEnv) { return env.includePath == otherEnv.includePath; })) { qtEnvironments.push_back(env); } } return qtEnvironments; } void SetupQt::addQtBuildVariant(QtEnvironment *env, const QString &buildVariantName) { if (env->qtConfigItems.contains(buildVariantName)) env->buildVariant << buildVariantName; } static QMap qmakeQueryOutput(const QString &qmakePath) { QProcess qmakeProcess; qmakeProcess.start(qmakePath, QStringList() << QLatin1String("-query")); if (!qmakeProcess.waitForStarted()) throw ErrorInfo(SetupQt::tr("%1 cannot be started.").arg(qmakePath)); qmakeProcess.waitForFinished(); const QByteArray output = qmakeProcess.readAllStandardOutput(); QMap ret; const auto lines = output.split('\n'); for (const QByteArray &line : lines) { int idx = line.indexOf(':'); if (idx >= 0) ret[line.left(idx)] = line.mid(idx + 1).trimmed(); } return ret; } static QByteArray readFileContent(const QString &filePath) { QFile file(filePath); if (file.open(QFile::ReadOnly)) return file.readAll(); return QByteArray(); } static QString configVariable(const QByteArray &configContent, const QString &key) { QRegExp regexp(QLatin1String("\\s*") + key + QLatin1String("\\s*\\+{0,1}=(.*)"), Qt::CaseSensitive); const QList configContentLines = configContent.split('\n'); bool success = false; for (const QByteArray &configContentLine : configContentLines) { success = regexp.exactMatch(QString::fromLocal8Bit(configContentLine)); if (success) break; } if (success) return regexp.capturedTexts()[1].simplified(); return QString(); } static QStringList configVariableItems(const QByteArray &configContent, const QString &key) { return configVariable(configContent, key).split(QLatin1Char(' '), QString::SkipEmptyParts); } typedef QMap QueryMap; static QString pathQueryValue(const QueryMap &queryMap, const QByteArray &key) { return QDir::fromNativeSeparators(QString::fromLocal8Bit(queryMap.value(key))); } EnhancedQtEnvironment SetupQt::fetchEnvironment(const QString &qmakePath) { EnhancedQtEnvironment qtEnvironment; QueryMap queryOutput = qmakeQueryOutput(qmakePath); qtEnvironment.installPrefixPath = pathQueryValue(queryOutput, "QT_INSTALL_PREFIX"); qtEnvironment.documentationPath = pathQueryValue(queryOutput, "QT_INSTALL_DOCS"); qtEnvironment.includePath = pathQueryValue(queryOutput, "QT_INSTALL_HEADERS"); qtEnvironment.libraryPath = pathQueryValue(queryOutput, "QT_INSTALL_LIBS"); qtEnvironment.binaryPath = pathQueryValue(queryOutput, "QT_HOST_BINS"); if (qtEnvironment.binaryPath.isEmpty()) qtEnvironment.binaryPath = pathQueryValue(queryOutput, "QT_INSTALL_BINS"); qtEnvironment.documentationPath = pathQueryValue(queryOutput, "QT_INSTALL_DOCS"); qtEnvironment.pluginPath = pathQueryValue(queryOutput, "QT_INSTALL_PLUGINS"); qtEnvironment.qmlPath = pathQueryValue(queryOutput, "QT_INSTALL_QML"); qtEnvironment.qmlImportPath = pathQueryValue(queryOutput, "QT_INSTALL_IMPORTS"); qtEnvironment.qtVersion = QString::fromLocal8Bit(queryOutput.value("QT_VERSION")); const Version qtVersion = Version::fromString(qtEnvironment.qtVersion); QString mkspecsBaseSrcPath; if (qtVersion.majorVersion() >= 5) { qtEnvironment.mkspecBasePath = pathQueryValue(queryOutput, "QT_HOST_DATA") + QLatin1String("/mkspecs"); mkspecsBaseSrcPath = pathQueryValue(queryOutput, "QT_HOST_DATA/src") + QLatin1String("/mkspecs"); } else { qtEnvironment.mkspecBasePath = pathQueryValue(queryOutput, "QT_INSTALL_DATA") + QLatin1String("/mkspecs"); } if (!QFile::exists(qtEnvironment.mkspecBasePath)) throw ErrorInfo(tr("Cannot extract the mkspecs directory.")); const QByteArray qconfigContent = readFileContent(qtEnvironment.mkspecBasePath + QLatin1String("/qconfig.pri")); qtEnvironment.qtMajorVersion = configVariable(qconfigContent, QLatin1String("QT_MAJOR_VERSION")).toInt(); qtEnvironment.qtMinorVersion = configVariable(qconfigContent, QLatin1String("QT_MINOR_VERSION")).toInt(); qtEnvironment.qtPatchVersion = configVariable(qconfigContent, QLatin1String("QT_PATCH_VERSION")).toInt(); qtEnvironment.qtNameSpace = configVariable(qconfigContent, QLatin1String("QT_NAMESPACE")); qtEnvironment.qtLibInfix = configVariable(qconfigContent, QLatin1String("QT_LIBINFIX")); qtEnvironment.architecture = configVariable(qconfigContent, QLatin1String("QT_TARGET_ARCH")); if (qtEnvironment.architecture.isEmpty()) qtEnvironment.architecture = configVariable(qconfigContent, QLatin1String("QT_ARCH")); if (qtEnvironment.architecture.isEmpty()) qtEnvironment.architecture = QLatin1String("x86"); qtEnvironment.configItems = configVariableItems(qconfigContent, QLatin1String("CONFIG")); qtEnvironment.qtConfigItems = configVariableItems(qconfigContent, QLatin1String("QT_CONFIG")); // retrieve the mkspec if (qtVersion.majorVersion() >= 5) { const QString mkspecName = QString::fromLocal8Bit(queryOutput.value("QMAKE_XSPEC")); qtEnvironment.mkspecName = mkspecName; qtEnvironment.mkspecPath = qtEnvironment.mkspecBasePath + QLatin1Char('/') + mkspecName; if (!mkspecsBaseSrcPath.isEmpty() && !QFile::exists(qtEnvironment.mkspecPath)) qtEnvironment.mkspecPath = mkspecsBaseSrcPath + QLatin1Char('/') + mkspecName; } else { if (HostOsInfo::isWindowsHost()) { const QString baseDirPath = qtEnvironment.mkspecBasePath + QLatin1String("/default/"); const QByteArray fileContent = readFileContent(baseDirPath + QLatin1String("qmake.conf")); qtEnvironment.mkspecPath = configVariable(fileContent, QLatin1String("QMAKESPEC_ORIGINAL")); if (!QFile::exists(qtEnvironment.mkspecPath)) { // Work around QTBUG-28792. // The value of QMAKESPEC_ORIGINAL is wrong for MinGW packages. Y u h8 me? const QRegExp rex(QLatin1String("\\binclude\\(([^)]+)/qmake\\.conf\\)")); if (rex.indexIn(QString::fromLocal8Bit(fileContent)) != -1) qtEnvironment.mkspecPath = QDir::cleanPath(baseDirPath + rex.cap(1)); } } else { qtEnvironment.mkspecPath = QFileInfo(qtEnvironment.mkspecBasePath + QLatin1String("/default")).symLinkTarget(); } // E.g. in qmake.conf for Qt 4.8/mingw we find this gem: // QMAKESPEC_ORIGINAL=C:\\Qt\\Qt\\4.8\\mingw482\\mkspecs\\win32-g++ qtEnvironment.mkspecPath = QDir::cleanPath(qtEnvironment.mkspecPath); qtEnvironment.mkspecName = qtEnvironment.mkspecPath; int idx = qtEnvironment.mkspecName.lastIndexOf(QLatin1Char('/')); if (idx != -1) qtEnvironment.mkspecName.remove(0, idx + 1); } // determine MSVC version if (isMsvcQt(qtEnvironment)) { bool ok; qtEnvironment.msvcVersion.setMajorVersion( configVariable(qconfigContent, QLatin1String("QT_MSVC_MAJOR_VERSION")).toInt(&ok)); if (ok) { qtEnvironment.msvcVersion.setMinorVersion( configVariable(qconfigContent, QLatin1String("QT_MSVC_MINOR_VERSION")).toInt(&ok)); } if (ok) { qtEnvironment.msvcVersion.setPatchLevel( configVariable(qconfigContent, QLatin1String("QT_MSVC_PATCH_VERSION")).toInt(&ok)); } if (!ok) qtEnvironment.msvcVersion = msvcCompilerVersionFromMkspecName(qtEnvironment.mkspecName); } // determine whether we have a framework build qtEnvironment.frameworkBuild = qtEnvironment.mkspecPath.contains(QLatin1String("macx")) && qtEnvironment.configItems.contains(QLatin1String("qt_framework")); // determine whether Qt is built with debug, release or both addQtBuildVariant(&qtEnvironment, QLatin1String("debug")); addQtBuildVariant(&qtEnvironment, QLatin1String("release")); if (!QFileInfo(qtEnvironment.mkspecPath).exists()) throw ErrorInfo(tr("mkspec '%1' does not exist").arg(qtEnvironment.mkspecPath)); return qtEnvironment; } static bool isToolchainProfile(const Profile &profile) { const auto actual = Internal::Set::fromList( profile.allKeys(Profile::KeySelectionRecursive)); Internal::Set expected = Internal::Set { QLatin1String("qbs.toolchain") }; if (HostOsInfo::isMacosHost()) expected.insert(QLatin1String("qbs.targetPlatform")); // match only Xcode profiles return Internal::Set(actual).unite(expected) == actual; } static bool isQtProfile(const Profile &profile) { const QStringList searchPaths = profile.value(QStringLiteral("preferences.qbsSearchPaths")).toStringList(); return std::any_of(searchPaths.cbegin(), searchPaths.cend(), [] (const QString &path) { return QFileInfo(path + QStringLiteral("/modules/Qt")).isDir(); }); } template bool areProfilePropertiesIncompatible(const T &set1, const T &set2) { // Two objects are only considered incompatible if they are both non empty and compare inequal // This logic is used for comparing target OS, toolchain lists, and architectures return set1.size() > 0 && set2.size() > 0 && set1 != set2; } static QStringList qbsToolchainFromQtMkspec(const QString &mkspec) { if (mkspec.contains(QLatin1String("-msvc"))) return QStringList() << QLatin1String("msvc"); if (mkspec == QLatin1String("win32-g++")) return QStringList() << QLatin1String("mingw") << QLatin1String("gcc"); if (mkspec.contains(QLatin1String("-clang"))) return QStringList() << QLatin1String("clang") << QLatin1String("llvm") << QLatin1String("gcc"); if (mkspec.contains(QLatin1String("-llvm"))) return QStringList() << QLatin1String("llvm") << QLatin1String("gcc"); if (mkspec.contains(QLatin1String("-g++"))) return QStringList() << QLatin1String("gcc"); // Worry about other, less common toolchains (ICC, QCC, etc.) later... return QStringList(); } enum Match { MatchFull, MatchPartial, MatchNone }; static Match compatibility(const EnhancedQtEnvironment &env, const Profile &toolchainProfile) { Match match = MatchFull; const auto toolchainNames = Internal::Set::fromList( toolchainProfile.value(QLatin1String("qbs.toolchain")).toStringList()); const auto mkspecToolchainNames = Internal::Set::fromList( qbsToolchainFromQtMkspec(env.mkspecName)); if (areProfilePropertiesIncompatible(toolchainNames, mkspecToolchainNames)) { auto intersection = toolchainNames; intersection.intersect(mkspecToolchainNames); if (!intersection.empty()) match = MatchPartial; else return MatchNone; } const auto targetPlatform = toolchainProfile.value( QLatin1String("qbs.targetPlatform")).toString(); if (!targetPlatform.isEmpty() && targetPlatform != qbsTargetPlatformFromQtMkspec(env.mkspecName)) return MatchNone; const QString toolchainArchitecture = toolchainProfile.value(QLatin1String("qbs.architecture")) .toString(); if (areProfilePropertiesIncompatible(canonicalArchitecture(env.architecture), canonicalArchitecture(toolchainArchitecture))) return MatchNone; if (env.msvcVersion.isValid()) { // We want to know for sure that MSVC compiler versions match, // because it's especially important for this toolchain const Version compilerVersion = Version::fromString( toolchainProfile.value(QLatin1String("cpp.compilerVersion")).toString()); static const Version vs2017Version{19, 10}; if (env.msvcVersion >= vs2017Version) { if (env.msvcVersion.majorVersion() != compilerVersion.majorVersion() || compilerVersion < vs2017Version) { return MatchNone; } } else if (env.msvcVersion.majorVersion() != compilerVersion.majorVersion() || env.msvcVersion.minorVersion() != compilerVersion.minorVersion()) { return MatchNone; } } return match; } QString profileNameWithoutHostArch(const QString &profileName) { QString result; int i = profileName.indexOf(QLatin1Char('-')); if (i == -1) return result; ++i; int j = profileName.indexOf(QLatin1Char('_'), i); if (j == -1) return result; result = profileName.mid(0, i) + profileName.mid(j + 1); return result; } // "Compressing" MSVC profiles means that if MSVC2017-x64 and MSVC2017-x86_x64 fully match, // then we drop the crosscompiling toolchain MSVC2017-x86_x64. static void compressMsvcProfiles(QStringList &profiles) { auto it = std::remove_if(profiles.begin(), profiles.end(), [&profiles] (const QString &profileName) { int idx = profileName.indexOf(QLatin1Char('_')); if (idx == -1) return false; return contains(profiles, profileNameWithoutHostArch(profileName)); }); if (it != profiles.end()) profiles.erase(it, profiles.end()); } void SetupQt::saveToQbsSettings(const QString &qtVersionName, const EnhancedQtEnvironment &qtEnvironment, Settings *settings) { const QString cleanQtVersionName = Profile::cleanName(qtVersionName); QString msg = QCoreApplication::translate("SetupQt", "Creating profile '%1'.") .arg(cleanQtVersionName); printf("%s\n", qPrintable(msg)); const ErrorInfo errorInfo = setupQtProfile(cleanQtVersionName, settings, qtEnvironment); if (errorInfo.hasError()) throw errorInfo; // If this profile does not specify a toolchain and we find exactly one profile that looks // like it might have been added by qbs-setup-toolchains, let's use that one as our // base profile. Profile profile(cleanQtVersionName, settings); if (!profile.baseProfile().isEmpty()) return; if (isToolchainProfile(profile)) return; QStringList fullMatches; QStringList partialMatches; const auto profileNames = settings->profiles(); for (const QString &profileName : profileNames) { const Profile otherProfile(profileName, settings); if (profileName == profile.name() || !isToolchainProfile(otherProfile) || isQtProfile(otherProfile)) continue; switch (compatibility(qtEnvironment, otherProfile)) { case MatchFull: fullMatches << profileName; break; case MatchPartial: partialMatches << profileName; break; default: break; } } if (fullMatches.size() > 1) compressMsvcProfiles(fullMatches); QString bestMatch; if (fullMatches.size() == 1) bestMatch = fullMatches.front(); else if (fullMatches.empty() && partialMatches.size() == 1) bestMatch = partialMatches.front(); if (bestMatch.isEmpty()) { QString message = Tr::tr("You need to set up toolchain information before you can " "use this Qt version for building. "); if (fullMatches.empty() && partialMatches.empty()) { message += Tr::tr("However, no toolchain profile was found. Either create one " "using qbs-setup-toolchains and set it as this profile's " "base profile or add the toolchain settings manually " "to this profile."); } else { message += Tr::tr("Consider setting one of these profiles as this profile's base " "profile: %1.").arg((fullMatches + partialMatches) .join(QLatin1String(", "))); } qbsWarning() << message; } else { profile.setBaseProfile(bestMatch); qbsInfo() << Tr::tr("Setting profile '%1' as the base profile for this profile.") .arg(bestMatch); } } bool SetupQt::checkIfMoreThanOneQtWithTheSameVersion(const QString &qtVersion, const std::vector &qtEnvironments) { bool foundOneVersion = false; for (const QtEnvironment &qtEnvironment : qtEnvironments) { if (qtEnvironment.qtVersion == qtVersion) { if (foundOneVersion) return true; foundOneVersion = true; } } return false; } } // namespace qbs