// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "cmakeprojectimporter.h" #include "cmakebuildconfiguration.h" #include "cmakekitinformation.h" #include "cmakeproject.h" #include "cmakeprojectconstants.h" #include "cmakeprojectmanagertr.h" #include "cmaketoolmanager.h" #include "presetsmacros.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace ProjectExplorer; using namespace QtSupport; using namespace Utils; namespace CMakeProjectManager::Internal { static Q_LOGGING_CATEGORY(cmInputLog, "qtc.cmake.import", QtWarningMsg); struct DirectoryData { // Project Stuff: QByteArray cmakeBuildType; FilePath buildDirectory; FilePath cmakeHomeDirectory; bool hasQmlDebugging = false; QString cmakePresetDisplayname; QString cmakePreset; // Kit Stuff FilePath cmakeBinary; QString generator; QString extraGenerator; QString platform; QString toolset; FilePath sysroot; QtProjectImporter::QtVersionData qt; QVector toolChains; }; static FilePaths scanDirectory(const FilePath &path, const QString &prefix) { FilePaths result; qCDebug(cmInputLog) << "Scanning for directories matching" << prefix << "in" << path; const FilePaths entries = path.dirEntries({{prefix + "*"}, QDir::Dirs | QDir::NoDotAndDotDot}); for (const FilePath &entry : entries) { QTC_ASSERT(entry.isDir(), continue); result.append(entry); } return result; } static QString baseCMakeToolDisplayName(CMakeTool &tool) { if (!tool.isValid()) return QString("CMake"); CMakeTool::Version version = tool.version(); return QString("CMake %1.%2.%3").arg(version.major).arg(version.minor).arg(version.patch); } static QString uniqueCMakeToolDisplayName(CMakeTool &tool) { QString baseName = baseCMakeToolDisplayName(tool); QStringList existingNames; for (const CMakeTool *t : CMakeToolManager::cmakeTools()) existingNames << t->displayName(); return Utils::makeUniquelyNumbered(baseName, existingNames); } // CMakeProjectImporter CMakeProjectImporter::CMakeProjectImporter(const FilePath &path, const CMakeProject *project) : QtProjectImporter(path) , m_project(project) , m_presetsTempDir("qtc-cmake-presets-XXXXXXXX") { useTemporaryKitAspect(CMakeKitAspect::id(), [this](Kit *k, const QVariantList &vl) { cleanupTemporaryCMake(k, vl); }, [this](Kit *k, const QVariantList &vl) { persistTemporaryCMake(k, vl); }); } FilePaths CMakeProjectImporter::importCandidates() { FilePaths candidates; candidates << scanDirectory(projectFilePath().absolutePath(), "build"); const QList kits = KitManager::kits(); for (const Kit *k : kits) { FilePath shadowBuildDirectory = CMakeBuildConfiguration::shadowBuildDirectory(projectFilePath(), k, QString(), BuildConfiguration::Unknown); candidates << scanDirectory(shadowBuildDirectory.absolutePath(), QString()); } for (const auto &configPreset : m_project->presetsData().configurePresets) { if (configPreset.hidden.value()) continue; if (configPreset.condition) { if (!CMakePresets::Macros::evaluatePresetCondition(configPreset, projectFilePath())) continue; } const FilePath configPresetDir = m_presetsTempDir.filePath(configPreset.name); configPresetDir.createDir(); candidates << configPresetDir; // If the binaryFilePath exists, do not try to import the existing build, so that // we don't have duplicates, one from the preset and one from the previous configuration. if (configPreset.binaryDir) { Environment env = projectDirectory().deviceEnvironment(); CMakePresets::Macros::expand(configPreset, env, projectDirectory()); QString binaryDir = configPreset.binaryDir.value(); CMakePresets::Macros::expand(configPreset, env, projectDirectory(), binaryDir); const FilePath binaryFilePath = FilePath::fromString(binaryDir); candidates.removeIf([&binaryFilePath](const FilePath &path) { return path == binaryFilePath; }); } } const FilePaths finalists = Utils::filteredUnique(candidates); qCInfo(cmInputLog) << "import candidates:" << finalists; return finalists; } Target *CMakeProjectImporter::preferredTarget(const QList &possibleTargets) { for (Kit *kit : m_project->oldPresetKits()) { const bool haveKit = Utils::contains(possibleTargets, [kit](const auto &target) { return target->kit() == kit; }); if (!haveKit) KitManager::deregisterKit(kit); } m_project->setOldPresetKits({}); return ProjectImporter::preferredTarget(possibleTargets); } static CMakeConfig configurationFromPresetProbe( const FilePath &importPath, const FilePath &sourceDirectory, const PresetsDetails::ConfigurePreset &configurePreset) { const FilePath cmakeListTxt = importPath / "CMakeLists.txt"; cmakeListTxt.writeFileContents(QByteArray("cmake_minimum_required(VERSION 3.15)\n" "\n" "project(preset-probe)\n" "\n")); Process cmake; cmake.setTimeoutS(30); cmake.setDisableUnixTerminal(); const FilePath cmakeExecutable = FilePath::fromString(configurePreset.cmakeExecutable.value()); Environment env = cmakeExecutable.deviceEnvironment(); CMakePresets::Macros::expand(configurePreset, env, sourceDirectory); env.setupEnglishOutput(); cmake.setEnvironment(env); cmake.setTimeOutMessageBoxEnabled(false); QStringList args; args.emplace_back("-S"); args.emplace_back(importPath.path()); args.emplace_back("-B"); args.emplace_back(importPath.pathAppended("build/").path()); if (configurePreset.generator) { args.emplace_back("-G"); args.emplace_back(configurePreset.generator.value()); } if (configurePreset.architecture && configurePreset.architecture.value().value) { if (!configurePreset.architecture->strategy || configurePreset.architecture->strategy != PresetsDetails::ValueStrategyPair::Strategy::external) { args.emplace_back("-A"); args.emplace_back(configurePreset.architecture.value().value.value()); } } if (configurePreset.toolset && configurePreset.toolset.value().value) { if (!configurePreset.toolset->strategy || configurePreset.toolset->strategy != PresetsDetails::ValueStrategyPair::Strategy::external) { args.emplace_back("-T"); args.emplace_back(configurePreset.toolset.value().value.value()); } } if (configurePreset.cacheVariables) { const CMakeConfig cache = configurePreset.cacheVariables ? configurePreset.cacheVariables.value() : CMakeConfig(); const QString cmakeMakeProgram = cache.stringValueOf("CMAKE_MAKE_PROGRAM"); const QString toolchainFile = cache.stringValueOf("CMAKE_TOOLCHAIN_FILE"); const QString prefixPath = cache.stringValueOf("CMAKE_PREFIX_PATH"); const QString findRootPath = cache.stringValueOf("CMAKE_FIND_ROOT_PATH"); const QString qtHostPath = cache.stringValueOf("QT_HOST_PATH"); if (!cmakeMakeProgram.isEmpty()) { args.emplace_back( QStringLiteral("-DCMAKE_MAKE_PROGRAM=%1").arg(cmakeMakeProgram)); } if (!toolchainFile.isEmpty()) { args.emplace_back( QStringLiteral("-DCMAKE_TOOLCHAIN_FILE=%1").arg(toolchainFile)); } if (!prefixPath.isEmpty()) { args.emplace_back(QStringLiteral("-DCMAKE_PREFIX_PATH=%1").arg(prefixPath)); } if (!findRootPath.isEmpty()) { args.emplace_back(QStringLiteral("-DCMAKE_FIND_ROOT_PATH=%1").arg(findRootPath)); } if (!qtHostPath.isEmpty()) { args.emplace_back(QStringLiteral("-DQT_HOST_PATH=%1").arg(qtHostPath)); } } qCDebug(cmInputLog) << "CMake probing for compilers: " << cmakeExecutable.toUserOutput() << args; cmake.setCommand({cmakeExecutable, args}); cmake.runBlocking(); QString errorMessage; const CMakeConfig config = CMakeConfig::fromFile(importPath.pathAppended( "build/CMakeCache.txt"), &errorMessage); return config; } struct QMakeAndCMakePrefixPath { FilePath qmakePath; QString cmakePrefixPath; // can be a semicolon-separated list }; static QMakeAndCMakePrefixPath qtInfoFromCMakeCache(const CMakeConfig &config, const Environment &env) { // Qt4 way to define things (more convenient for us, so try this first;-) const FilePath qmake = config.filePathValueOf("QT_QMAKE_EXECUTABLE"); qCDebug(cmInputLog) << "QT_QMAKE_EXECUTABLE=" << qmake.toUserOutput(); // Check Qt5 settings: oh, the horror! const FilePath qtCMakeDir = [config, env] { FilePath tmp; // Check the CMake "_DIR" variable for (const auto &var : {"Qt6", "Qt6Core", "Qt5", "Qt5Core"}) { tmp = config.filePathValueOf(QByteArray(var) + "_DIR"); if (!tmp.isEmpty()) break; } return tmp; }(); qCDebug(cmInputLog) << "QtXCore_DIR=" << qtCMakeDir.toUserOutput(); const FilePath canQtCMakeDir = FilePath::fromString(qtCMakeDir.toFileInfo().canonicalFilePath()); qCInfo(cmInputLog) << "QtXCore_DIR (canonical)=" << canQtCMakeDir.toUserOutput(); const QString prefixPath = [qtCMakeDir, canQtCMakeDir, config, env] { QString result; if (!qtCMakeDir.isEmpty()) { result = canQtCMakeDir.parentDir().parentDir().parentDir().path(); // Up 3 levels... } else { // Check the CMAKE_PREFIX_PATH and "_ROOT" CMake or environment variables // This can be a single value or a semicolon-separated list for (const auto &var : {"CMAKE_PREFIX_PATH", "Qt6_ROOT", "Qt5_ROOT"}) { result = config.stringValueOf(var); if (result.isEmpty()) result = env.value(QString::fromUtf8(var)); if (!result.isEmpty()) break; } } return result; }(); qCDebug(cmInputLog) << "PrefixPath:" << prefixPath; if (!qmake.isEmpty() && !prefixPath.isEmpty()) return {qmake, prefixPath}; FilePath toolchainFile = config.filePathValueOf(QByteArray("CMAKE_TOOLCHAIN_FILE")); if (prefixPath.isEmpty() && toolchainFile.isEmpty()) return {qmake, QString()}; // Run a CMake project that would do qmake probing TemporaryDirectory qtcQMakeProbeDir("qtc-cmake-qmake-probe-XXXXXXXX"); FilePath cmakeListTxt(qtcQMakeProbeDir.filePath("CMakeLists.txt")); cmakeListTxt.writeFileContents(QByteArray(R"( cmake_minimum_required(VERSION 3.15) project(qmake-probe LANGUAGES NONE) # Bypass Qt6's usage of find_dependency, which would require compiler # and source code probing, which slows things unnecessarily file(WRITE "${CMAKE_SOURCE_DIR}/CMakeFindDependencyMacro.cmake" [=[ macro(find_dependency dep) endmacro() ]=]) set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}") find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED) if (CMAKE_CROSSCOMPILING) find_program(qmake_binary NAMES qmake qmake.bat PATHS "${Qt${QT_VERSION_MAJOR}_DIR}/../../../bin" NO_DEFAULT_PATH) file(WRITE "${CMAKE_SOURCE_DIR}/qmake-location.txt" "${qmake_binary}") else() file(GENERATE OUTPUT "${CMAKE_SOURCE_DIR}/qmake-location.txt" CONTENT "$") endif() # Remove a Qt CMake hack that adds lib/cmake at the end of every path in CMAKE_PREFIX_PATH list(REMOVE_DUPLICATES CMAKE_PREFIX_PATH) list(TRANSFORM CMAKE_PREFIX_PATH REPLACE "/lib/cmake$" "") file(WRITE "${CMAKE_SOURCE_DIR}/cmake-prefix-path.txt" "${CMAKE_PREFIX_PATH}") )")); Process cmake; cmake.setTimeoutS(5); cmake.setDisableUnixTerminal(); Environment cmakeEnv(env); cmakeEnv.setupEnglishOutput(); cmake.setEnvironment(cmakeEnv); cmake.setTimeOutMessageBoxEnabled(false); QString cmakeGenerator = config.stringValueOf(QByteArray("CMAKE_GENERATOR")); QString cmakeGeneratorPlatform = config.stringValueOf(QByteArray("CMAKE_GENERATOR_PLATFORM")); QString cmakeGeneratorToolset = config.stringValueOf(QByteArray("CMAKE_GENERATOR_TOOLSET")); FilePath cmakeExecutable = config.filePathValueOf(QByteArray("CMAKE_COMMAND")); FilePath cmakeMakeProgram = config.filePathValueOf(QByteArray("CMAKE_MAKE_PROGRAM")); FilePath hostPath = config.filePathValueOf(QByteArray("QT_HOST_PATH")); QStringList args; args.push_back("-S"); args.push_back(qtcQMakeProbeDir.path().path()); args.push_back("-B"); args.push_back(qtcQMakeProbeDir.filePath("build").path()); args.push_back("-G"); args.push_back(cmakeGenerator); if (!cmakeGeneratorPlatform.isEmpty()) { args.push_back("-A"); args.push_back(cmakeGeneratorPlatform); } if (!cmakeGeneratorToolset.isEmpty()) { args.push_back("-T"); args.push_back(cmakeGeneratorToolset); } if (!cmakeMakeProgram.isEmpty()) { args.push_back(QStringLiteral("-DCMAKE_MAKE_PROGRAM=%1").arg(cmakeMakeProgram.toString())); } if (toolchainFile.isEmpty()) { args.push_back(QStringLiteral("-DCMAKE_PREFIX_PATH=%1").arg(prefixPath)); } else { if (!prefixPath.isEmpty()) args.push_back(QStringLiteral("-DCMAKE_FIND_ROOT_PATH=%1").arg(prefixPath)); args.push_back(QStringLiteral("-DCMAKE_TOOLCHAIN_FILE=%1").arg(toolchainFile.toString())); } if (!hostPath.isEmpty()) { args.push_back(QStringLiteral("-DQT_HOST_PATH=%1").arg(hostPath.toString())); } qCDebug(cmInputLog) << "CMake probing for qmake path: " << cmakeExecutable.toUserOutput() << args; cmake.setCommand({cmakeExecutable, args}); cmake.runBlocking(); const FilePath qmakeLocationTxt = qtcQMakeProbeDir.filePath("qmake-location.txt"); const FilePath qmakeLocation = FilePath::fromUtf8( qmakeLocationTxt.fileContents().value_or(QByteArray())); qCDebug(cmInputLog) << "qmake location: " << qmakeLocation.toUserOutput(); const FilePath prefixPathTxt = qtcQMakeProbeDir.filePath("cmake-prefix-path.txt"); const QString resultedPrefixPath = QString::fromUtf8( prefixPathTxt.fileContents().value_or(QByteArray())); qCDebug(cmInputLog) << "PrefixPath [after qmake probe]: " << resultedPrefixPath; return {qmakeLocation, resultedPrefixPath}; } static QVector extractToolChainsFromCache(const CMakeConfig &config) { QVector result; bool haveCCxxCompiler = false; for (const CMakeConfigItem &i : config) { if (!i.key.startsWith("CMAKE_") || !i.key.endsWith("_COMPILER")) continue; const QByteArray language = i.key.mid(6, i.key.count() - 6 - 9); // skip "CMAKE_" and "_COMPILER" Id languageId; if (language == "CXX") { haveCCxxCompiler = true; languageId = ProjectExplorer::Constants::CXX_LANGUAGE_ID; } else if (language == "C") { haveCCxxCompiler = true; languageId = ProjectExplorer::Constants::C_LANGUAGE_ID; } else languageId = Id::fromName(language); result.append({FilePath::fromUtf8(i.value), languageId}); } if (!haveCCxxCompiler) { const QByteArray generator = config.valueOf("CMAKE_GENERATOR"); QString cCompilerName; QString cxxCompilerName; if (generator.contains("Visual Studio")) { cCompilerName = "cl.exe"; cxxCompilerName = "cl.exe"; } else if (generator.contains("Xcode")) { cCompilerName = "clang"; cxxCompilerName = "clang++"; } if (!cCompilerName.isEmpty() && !cxxCompilerName.isEmpty()) { const FilePath linker = config.filePathValueOf("CMAKE_LINKER"); if (!linker.isEmpty()) { const FilePath compilerPath = linker.parentDir(); result.append({compilerPath.pathAppended(cCompilerName), ProjectExplorer::Constants::C_LANGUAGE_ID}); result.append({compilerPath.pathAppended(cxxCompilerName), ProjectExplorer::Constants::CXX_LANGUAGE_ID}); } } } return result; } static QString extractVisualStudioPlatformFromConfig(const CMakeConfig &config) { const QString cmakeGenerator = config.stringValueOf(QByteArray("CMAKE_GENERATOR")); QString platform; if (cmakeGenerator.contains("Visual Studio")) { const FilePath linker = config.filePathValueOf("CMAKE_LINKER"); const QString toolsDir = linker.parentDir().fileName(); if (toolsDir.compare("x64", Qt::CaseInsensitive) == 0) { platform = "x64"; } else if (toolsDir.compare("x86", Qt::CaseInsensitive) == 0) { platform = "Win32"; } else if (toolsDir.compare("arm64", Qt::CaseInsensitive) == 0) { platform = "ARM64"; } else if (toolsDir.compare("arm", Qt::CaseInsensitive) == 0) { platform = "ARM"; } } return platform; } void updateCompilerPaths(CMakeConfig &config, const Environment &env) { auto updateRelativePath = [&config, env](const QByteArray &key) { FilePath pathValue = config.filePathValueOf(key); if (pathValue.isAbsolutePath() || pathValue.isEmpty()) return; pathValue = env.searchInPath(pathValue.fileName()); auto it = std::find_if(config.begin(), config.end(), [&key](const CMakeConfigItem &item) { return item.key == key; }); QTC_ASSERT(it != config.end(), return); it->value = pathValue.path().toUtf8(); }; updateRelativePath("CMAKE_C_COMPILER"); updateRelativePath("CMAKE_CXX_COMPILER"); } void updateConfigWithDirectoryData(CMakeConfig &config, const std::unique_ptr &data) { auto updateCompilerValue = [&config, &data](const QByteArray &key, const Utils::Id &language) { auto it = std::find_if(config.begin(), config.end(), [&key](const CMakeConfigItem &ci) { return ci.key == key; }); auto tcd = Utils::findOrDefault(data->toolChains, [&language](const ToolChainDescription &t) { return t.language == language; }); if (it != config.end() && it->value.isEmpty()) it->value = tcd.compilerPath.toString().toUtf8(); else config << CMakeConfigItem(key, CMakeConfigItem::FILEPATH, tcd.compilerPath.toString().toUtf8()); }; updateCompilerValue("CMAKE_C_COMPILER", ProjectExplorer::Constants::C_LANGUAGE_ID); updateCompilerValue("CMAKE_CXX_COMPILER", ProjectExplorer::Constants::CXX_LANGUAGE_ID); if (data->qt.qt) config << CMakeConfigItem("QT_QMAKE_EXECUTABLE", CMakeConfigItem::FILEPATH, data->qt.qt->qmakeFilePath().toString().toUtf8()); } ToolChain *findExternalToolchain(const QString &presetArchitecture, const QString &presetToolset) { // A compiler path example. Note that the compiler version is not the same version from MsvcToolChain // ... \MSVC\14.29.30133\bin\Hostx64\x64\cl.exe // // And the CMakePresets.json // // "toolset": { // "value": "v142,host=x64,version=14.29.30133", // "strategy": "external" // }, // "architecture": { // "value": "x64", // "strategy": "external" // } auto msvcToolchains = ToolChainManager::toolchains([](const ToolChain *tc) { return tc->typeId() == ProjectExplorer::Constants::MSVC_TOOLCHAIN_TYPEID; }); const QSet msvcFlavors = Utils::toSet(Utils::transform(msvcToolchains, [](const ToolChain *tc) { return tc->targetAbi().osFlavor(); })); return ToolChainManager::toolChain( [presetArchitecture, presetToolset, msvcFlavors](const ToolChain *tc) -> bool { if (tc->typeId() != ProjectExplorer::Constants::MSVC_TOOLCHAIN_TYPEID) return false; const FilePath compilerPath = tc->compilerCommand(); const QString architecture = compilerPath.parentDir().fileName().toLower(); const QString host = compilerPath.parentDir().parentDir().fileName().toLower().replace("host", "host="); const QString version = QString("version=%1") .arg(compilerPath.parentDir().parentDir().parentDir().parentDir().fileName()); static std::pair abiTable[] = { {QStringLiteral("v143"), Abi::WindowsMsvc2022Flavor}, {QStringLiteral("v142"), Abi::WindowsMsvc2019Flavor}, {QStringLiteral("v141"), Abi::WindowsMsvc2017Flavor}, }; Abi::OSFlavor toolsetAbi = Abi::UnknownFlavor; for (const auto &abiPair : abiTable) { if (presetToolset.contains(abiPair.first)) { toolsetAbi = abiPair.second; break; } } // User didn't specify any flavor, so pick the highest toolchain available if (toolsetAbi == Abi::UnknownFlavor) { for (const auto &abiPair : abiTable) { if (msvcFlavors.contains(abiPair.second)) { toolsetAbi = abiPair.second; break; } } } if (toolsetAbi != tc->targetAbi().osFlavor()) return false; if (presetToolset.contains("host=") && !presetToolset.contains(host)) return false; // Make sure we match also version=14.29 auto versionIndex = presetToolset.indexOf("version="); if (versionIndex != -1 && !version.startsWith(presetToolset.mid(versionIndex))) return false; if (presetArchitecture != architecture) return false; qCDebug(cmInputLog) << "For external architecture" << presetArchitecture << "and toolset" << presetToolset << "the following toolchain was selected:\n" << compilerPath.toString(); return true; }); } QList CMakeProjectImporter::examineDirectory(const FilePath &importPath, QString *warningMessage) const { QList result; qCInfo(cmInputLog) << "Examining directory:" << importPath.toUserOutput(); if (importPath.isChildOf(m_presetsTempDir.path())) { auto data = std::make_unique(); const QString presetName = importPath.fileName(); PresetsDetails::ConfigurePreset configurePreset = Utils::findOrDefault(m_project->presetsData().configurePresets, [presetName](const PresetsDetails::ConfigurePreset &preset) { return preset.name == presetName; }); Environment env = projectDirectory().deviceEnvironment(); CMakePresets::Macros::expand(configurePreset, env, projectDirectory()); if (configurePreset.displayName) data->cmakePresetDisplayname = configurePreset.displayName.value(); else data->cmakePresetDisplayname = configurePreset.name; data->cmakePreset = configurePreset.name; if (!configurePreset.cmakeExecutable) { const CMakeTool *cmakeTool = CMakeToolManager::defaultCMakeTool(); if (cmakeTool) configurePreset.cmakeExecutable = cmakeTool->cmakeExecutable().toString(); } data->cmakeBinary = Utils::FilePath::fromString(configurePreset.cmakeExecutable.value()); if (configurePreset.generator) data->generator = configurePreset.generator.value(); if (configurePreset.binaryDir) { QString binaryDir = configurePreset.binaryDir.value(); CMakePresets::Macros::expand(configurePreset, env, projectDirectory(), binaryDir); data->buildDirectory = Utils::FilePath::fromString(binaryDir); } const bool architectureExternalStrategy = configurePreset.architecture && configurePreset.architecture->strategy && configurePreset.architecture->strategy == PresetsDetails::ValueStrategyPair::Strategy::external; const bool toolsetExternalStrategy = configurePreset.toolset && configurePreset.toolset->strategy && configurePreset.toolset->strategy == PresetsDetails::ValueStrategyPair::Strategy::external; if (!architectureExternalStrategy && configurePreset.architecture && configurePreset.architecture.value().value) data->platform = configurePreset.architecture.value().value.value(); if (!toolsetExternalStrategy && configurePreset.toolset && configurePreset.toolset.value().value) data->toolset = configurePreset.toolset.value().value.value(); if (architectureExternalStrategy && toolsetExternalStrategy) { const ToolChain *tc = findExternalToolchain(configurePreset.architecture->value.value_or(QString()), configurePreset.toolset->value.value_or(QString())); if (tc) tc->addToEnvironment(env); } CMakePresets::Macros::updateToolchainFile(configurePreset, env, projectDirectory(), data->buildDirectory); CMakePresets::Macros::updateCacheVariables(configurePreset, env, projectDirectory()); const CMakeConfig cache = configurePreset.cacheVariables ? configurePreset.cacheVariables.value() : CMakeConfig(); data->sysroot = cache.filePathValueOf("CMAKE_SYSROOT"); CMakeConfig config; if (cache.valueOf("CMAKE_C_COMPILER").isEmpty() && cache.valueOf("CMAKE_CXX_COMPILER").isEmpty()) { QApplication::setOverrideCursor(Qt::WaitCursor); config = configurationFromPresetProbe(importPath, projectDirectory(), configurePreset); QApplication::restoreOverrideCursor(); if (!configurePreset.generator) { QString cmakeGenerator = config.stringValueOf(QByteArray("CMAKE_GENERATOR")); configurePreset.generator = cmakeGenerator; data->generator = cmakeGenerator; data->platform = extractVisualStudioPlatformFromConfig(config); if (!data->platform.isEmpty()) { configurePreset.architecture = PresetsDetails::ValueStrategyPair(); configurePreset.architecture->value = data->platform; } } } else { config = cache; updateCompilerPaths(config, env); config << CMakeConfigItem("CMAKE_COMMAND", CMakeConfigItem::PATH, configurePreset.cmakeExecutable.value().toUtf8()); if (configurePreset.generator) config << CMakeConfigItem("CMAKE_GENERATOR", CMakeConfigItem::STRING, configurePreset.generator.value().toUtf8()); } const auto [qmake, cmakePrefixPath] = qtInfoFromCMakeCache(config, env); if (!qmake.isEmpty()) data->qt = findOrCreateQtVersion(qmake); if (!cmakePrefixPath.isEmpty() && config.valueOf("CMAKE_PREFIX_PATH").isEmpty()) config << CMakeConfigItem("CMAKE_PREFIX_PATH", CMakeConfigItem::PATH, cmakePrefixPath.toUtf8()); // ToolChains: data->toolChains = extractToolChainsFromCache(config); // Update QT_QMAKE_EXECUTABLE and CMAKE_C|XX_COMPILER config values updateConfigWithDirectoryData(config, data); QByteArrayList buildConfigurationTypes = {cache.valueOf("CMAKE_BUILD_TYPE")}; if (buildConfigurationTypes.front().isEmpty()) { buildConfigurationTypes.clear(); QByteArray buildConfigurationTypesString = cache.valueOf("CMAKE_CONFIGURATION_TYPES"); if (!buildConfigurationTypesString.isEmpty()) { buildConfigurationTypes = buildConfigurationTypesString.split(';'); } else { for (int type = CMakeBuildConfigurationFactory::BuildTypeDebug; type != CMakeBuildConfigurationFactory::BuildTypeLast; ++type) { BuildInfo info = CMakeBuildConfigurationFactory::createBuildInfo( CMakeBuildConfigurationFactory::BuildType(type)); buildConfigurationTypes << info.typeName.toUtf8(); } } } for (const auto &buildType : buildConfigurationTypes) { DirectoryData *newData = new DirectoryData(*data); newData->cmakeBuildType = buildType; result.emplace_back(newData); } return result; } const FilePath cacheFile = importPath.pathAppended("CMakeCache.txt"); if (!cacheFile.exists()) { qCDebug(cmInputLog) << cacheFile.toUserOutput() << "does not exist, returning."; return result; } QString errorMessage; const CMakeConfig config = CMakeConfig::fromFile(cacheFile, &errorMessage); if (config.isEmpty() || !errorMessage.isEmpty()) { qCDebug(cmInputLog) << "Failed to read configuration from" << cacheFile << errorMessage; return result; } QByteArrayList buildConfigurationTypes = {config.valueOf("CMAKE_BUILD_TYPE")}; if (buildConfigurationTypes.front().isEmpty()) { QByteArray buildConfigurationTypesString = config.valueOf("CMAKE_CONFIGURATION_TYPES"); if (!buildConfigurationTypesString.isEmpty()) buildConfigurationTypes = buildConfigurationTypesString.split(';'); } const Environment env = projectDirectory().deviceEnvironment(); for (auto const &buildType: std::as_const(buildConfigurationTypes)) { auto data = std::make_unique(); data->cmakeHomeDirectory = FilePath::fromUserInput(config.stringValueOf("CMAKE_HOME_DIRECTORY")) .canonicalPath(); const FilePath canonicalProjectDirectory = projectDirectory().canonicalPath(); if (data->cmakeHomeDirectory != canonicalProjectDirectory) { *warningMessage = Tr::tr("Unexpected source directory \"%1\", expected \"%2\". " "This can be correct in some situations, for example when " "importing a standalone Qt test, but usually this is an error. " "Import the build anyway?") .arg(data->cmakeHomeDirectory.toUserOutput(), canonicalProjectDirectory.toUserOutput()); } data->hasQmlDebugging = CMakeBuildConfiguration::hasQmlDebugging(config); data->buildDirectory = importPath; data->cmakeBuildType = buildType; data->cmakeBinary = config.filePathValueOf("CMAKE_COMMAND"); data->generator = config.stringValueOf("CMAKE_GENERATOR"); data->extraGenerator = config.stringValueOf("CMAKE_EXTRA_GENERATOR"); data->platform = config.stringValueOf("CMAKE_GENERATOR_PLATFORM"); if (data->platform.isEmpty()) data->platform = extractVisualStudioPlatformFromConfig(config); data->toolset = config.stringValueOf("CMAKE_GENERATOR_TOOLSET"); data->sysroot = config.filePathValueOf("CMAKE_SYSROOT"); // Qt: const auto [qmake, cmakePrefixPath] = qtInfoFromCMakeCache(config, env); if (!qmake.isEmpty()) data->qt = findOrCreateQtVersion(qmake); // ToolChains: data->toolChains = extractToolChainsFromCache(config); qCInfo(cmInputLog) << "Offering to import" << importPath.toUserOutput(); result.push_back(static_cast(data.release())); } return result; } void CMakeProjectImporter::ensureBuildDirectory(DirectoryData &data, const Kit *k) const { if (!data.buildDirectory.isEmpty()) return; const auto cmakeBuildType = CMakeBuildConfigurationFactory::buildTypeFromByteArray( data.cmakeBuildType); auto buildInfo = CMakeBuildConfigurationFactory::createBuildInfo(cmakeBuildType); data.buildDirectory = CMakeBuildConfiguration::shadowBuildDirectory(projectFilePath(), k, buildInfo.typeName, buildInfo.buildType); } bool CMakeProjectImporter::matchKit(void *directoryData, const Kit *k) const { DirectoryData *data = static_cast(directoryData); CMakeTool *cm = CMakeKitAspect::cmakeTool(k); if (!cm || cm->cmakeExecutable() != data->cmakeBinary) return false; if (CMakeGeneratorKitAspect::generator(k) != data->generator || CMakeGeneratorKitAspect::extraGenerator(k) != data->extraGenerator || CMakeGeneratorKitAspect::platform(k) != data->platform || CMakeGeneratorKitAspect::toolset(k) != data->toolset) return false; if (SysRootKitAspect::sysRoot(k) != data->sysroot) return false; if (data->qt.qt && QtSupport::QtKitAspect::qtVersionId(k) != data->qt.qt->uniqueId()) return false; const bool compilersMatch = [k, data] { const QList allLanguages = ToolChainManager::allLanguages(); for (const ToolChainDescription &tcd : data->toolChains) { if (!Utils::contains(allLanguages, [&tcd](const Id &language) { return language == tcd.language; })) continue; ToolChain *tc = ToolChainKitAspect::toolChain(k, tcd.language); if ((!tc || !tc->matchesCompilerCommand(tcd.compilerPath))) { return false; } } return true; }(); const bool noCompilers = [k, data] { const QList allLanguages = ToolChainManager::allLanguages(); for (const ToolChainDescription &tcd : data->toolChains) { if (!Utils::contains(allLanguages, [&tcd](const Id &language) { return language == tcd.language; })) continue; ToolChain *tc = ToolChainKitAspect::toolChain(k, tcd.language); if (tc && tc->matchesCompilerCommand(tcd.compilerPath)) { return false; } } return true; }(); bool haveCMakePreset = false; if (!data->cmakePreset.isEmpty()) { const auto presetConfigItem = CMakeConfigurationKitAspect::cmakePresetConfigItem(k); const QString presetName = presetConfigItem.expandedValue(k); if (data->cmakePreset != presetName) return false; ensureBuildDirectory(*data, k); haveCMakePreset = true; } if (!compilersMatch && !(haveCMakePreset && noCompilers)) return false; qCDebug(cmInputLog) << k->displayName() << "matches directoryData for" << data->buildDirectory.toUserOutput(); return true; } Kit *CMakeProjectImporter::createKit(void *directoryData) const { DirectoryData *data = static_cast(directoryData); return QtProjectImporter::createTemporaryKit(data->qt, [&data, this](Kit *k) { const CMakeToolData cmtd = findOrCreateCMakeTool(data->cmakeBinary); QTC_ASSERT(cmtd.cmakeTool, return); if (cmtd.isTemporary) addTemporaryData(CMakeKitAspect::id(), cmtd.cmakeTool->id().toSetting(), k); CMakeKitAspect::setCMakeTool(k, cmtd.cmakeTool->id()); CMakeGeneratorKitAspect::setGenerator(k, data->generator); CMakeGeneratorKitAspect::setExtraGenerator(k, data->extraGenerator); CMakeGeneratorKitAspect::setPlatform(k, data->platform); CMakeGeneratorKitAspect::setToolset(k, data->toolset); SysRootKitAspect::setSysRoot(k, data->sysroot); for (const ToolChainDescription &cmtcd : data->toolChains) { const ToolChainData tcd = findOrCreateToolChains(cmtcd); QTC_ASSERT(!tcd.tcs.isEmpty(), continue); if (tcd.areTemporary) { for (ToolChain *tc : tcd.tcs) addTemporaryData(ToolChainKitAspect::id(), tc->id(), k); } ToolChainKitAspect::setToolChain(k, tcd.tcs.at(0)); } if (!data->cmakePresetDisplayname.isEmpty()) { k->setUnexpandedDisplayName( QString("%1 (CMake preset)").arg(data->cmakePresetDisplayname)); CMakeConfigurationKitAspect::setCMakePreset(k, data->cmakePreset); } if (!data->cmakePreset.isEmpty()) ensureBuildDirectory(*data, k); qCInfo(cmInputLog) << "Temporary Kit created."; }); } const QList CMakeProjectImporter::buildInfoList(void *directoryData) const { auto data = static_cast(directoryData); // create info: CMakeBuildConfigurationFactory::BuildType buildType = CMakeBuildConfigurationFactory::buildTypeFromByteArray(data->cmakeBuildType); // RelWithDebInfo + QML Debugging = Profile if (buildType == CMakeBuildConfigurationFactory::BuildTypeRelWithDebInfo && data->hasQmlDebugging) buildType = CMakeBuildConfigurationFactory::BuildTypeProfile; BuildInfo info = CMakeBuildConfigurationFactory::createBuildInfo(buildType); info.buildDirectory = data->buildDirectory; QVariantMap config = info.extraInfo.toMap(); // new empty, or existing one from createBuildInfo config.insert(Constants::CMAKE_HOME_DIR, data->cmakeHomeDirectory.toVariant()); // Potentially overwrite the default QML Debugging settings for the build type as set by // createBuildInfo, in case we are importing a "Debug" CMake configuration without QML Debugging config.insert(Constants::QML_DEBUG_SETTING, data->hasQmlDebugging ? TriState::Enabled.toVariant() : TriState::Default.toVariant()); info.extraInfo = config; qCDebug(cmInputLog) << "BuildInfo configured."; return {info}; } CMakeProjectImporter::CMakeToolData CMakeProjectImporter::findOrCreateCMakeTool(const FilePath &cmakeToolPath) const { CMakeToolData result; result.cmakeTool = CMakeToolManager::findByCommand(cmakeToolPath); if (!result.cmakeTool) { qCDebug(cmInputLog) << "Creating temporary CMakeTool for" << cmakeToolPath.toUserOutput(); UpdateGuard guard(*this); auto newTool = std::make_unique(CMakeTool::ManualDetection, CMakeTool::createId()); newTool->setFilePath(cmakeToolPath); newTool->setDisplayName(uniqueCMakeToolDisplayName(*newTool)); result.cmakeTool = newTool.get(); result.isTemporary = true; CMakeToolManager::registerCMakeTool(std::move(newTool)); } return result; } void CMakeProjectImporter::deleteDirectoryData(void *directoryData) const { delete static_cast(directoryData); } void CMakeProjectImporter::cleanupTemporaryCMake(Kit *k, const QVariantList &vl) { if (vl.isEmpty()) return; // No temporary CMake QTC_ASSERT(vl.count() == 1, return); CMakeKitAspect::setCMakeTool(k, Id()); // Always mark Kit as not using this Qt CMakeToolManager::deregisterCMakeTool(Id::fromSetting(vl.at(0))); qCDebug(cmInputLog) << "Temporary CMake tool cleaned up."; } void CMakeProjectImporter::persistTemporaryCMake(Kit *k, const QVariantList &vl) { if (vl.isEmpty()) return; // No temporary CMake QTC_ASSERT(vl.count() == 1, return); const QVariant data = vl.at(0); CMakeTool *tmpCmake = CMakeToolManager::findById(Id::fromSetting(data)); CMakeTool *actualCmake = CMakeKitAspect::cmakeTool(k); // User changed Kit away from temporary CMake that was set up: if (tmpCmake && actualCmake != tmpCmake) CMakeToolManager::deregisterCMakeTool(tmpCmake->id()); qCDebug(cmInputLog) << "Temporary CMake tool made persistent."; } } // CMakeProjectManager::Internal #ifdef WITH_TESTS #include "cmakeprojectplugin.h" #include namespace CMakeProjectManager { namespace Internal { void CMakeProjectPlugin::testCMakeProjectImporterQt_data() { QTest::addColumn("cache"); QTest::addColumn("expectedQmake"); QTest::newRow("Empty input") << QStringList() << QString(); QTest::newRow("Qt4") << QStringList({QString::fromLatin1("QT_QMAKE_EXECUTABLE=/usr/bin/xxx/qmake")}) << "/usr/bin/xxx/qmake"; // Everything else will require Qt installations! } void CMakeProjectPlugin::testCMakeProjectImporterQt() { QFETCH(QStringList, cache); QFETCH(QString, expectedQmake); CMakeConfig config; for (const QString &c : std::as_const(cache)) { const int pos = c.indexOf('='); Q_ASSERT(pos > 0); const QString key = c.left(pos); const QString value = c.mid(pos + 1); config.append(CMakeConfigItem(key.toUtf8(), value.toUtf8())); } auto [realQmake, cmakePrefixPath] = qtInfoFromCMakeCache(config, Environment::systemEnvironment()); QCOMPARE(realQmake.path(), expectedQmake); } void CMakeProjectPlugin::testCMakeProjectImporterToolChain_data() { QTest::addColumn("cache"); QTest::addColumn("expectedLanguages"); QTest::addColumn("expectedToolChains"); QTest::newRow("Empty input") << QStringList() << QByteArrayList() << QStringList(); QTest::newRow("Unrelated input") << QStringList("CMAKE_SOMETHING_ELSE=/tmp") << QByteArrayList() << QStringList(); QTest::newRow("CXX compiler") << QStringList({"CMAKE_CXX_COMPILER=/usr/bin/g++"}) << QByteArrayList({"Cxx"}) << QStringList({"/usr/bin/g++"}); QTest::newRow("CXX compiler, C compiler") << QStringList({"CMAKE_CXX_COMPILER=/usr/bin/g++", "CMAKE_C_COMPILER=/usr/bin/clang"}) << QByteArrayList({"Cxx", "C"}) << QStringList({"/usr/bin/g++", "/usr/bin/clang"}); QTest::newRow("CXX compiler, C compiler, strange compiler") << QStringList({"CMAKE_CXX_COMPILER=/usr/bin/g++", "CMAKE_C_COMPILER=/usr/bin/clang", "CMAKE_STRANGE_LANGUAGE_COMPILER=/tmp/strange/compiler"}) << QByteArrayList({"Cxx", "C", "STRANGE_LANGUAGE"}) << QStringList({"/usr/bin/g++", "/usr/bin/clang", "/tmp/strange/compiler"}); QTest::newRow("CXX compiler, C compiler, strange compiler (with junk)") << QStringList({"FOO=test", "CMAKE_CXX_COMPILER=/usr/bin/g++", "CMAKE_BUILD_TYPE=debug", "CMAKE_C_COMPILER=/usr/bin/clang", "SOMETHING_COMPILER=/usr/bin/something", "CMAKE_STRANGE_LANGUAGE_COMPILER=/tmp/strange/compiler", "BAR=more test"}) << QByteArrayList({"Cxx", "C", "STRANGE_LANGUAGE"}) << QStringList({"/usr/bin/g++", "/usr/bin/clang", "/tmp/strange/compiler"}); } void CMakeProjectPlugin::testCMakeProjectImporterToolChain() { QFETCH(QStringList, cache); QFETCH(QByteArrayList, expectedLanguages); QFETCH(QStringList, expectedToolChains); QCOMPARE(expectedLanguages.count(), expectedToolChains.count()); CMakeConfig config; for (const QString &c : std::as_const(cache)) { const int pos = c.indexOf('='); Q_ASSERT(pos > 0); const QString key = c.left(pos); const QString value = c.mid(pos + 1); config.append(CMakeConfigItem(key.toUtf8(), value.toUtf8())); } const QVector tcs = extractToolChainsFromCache(config); QCOMPARE(tcs.count(), expectedLanguages.count()); for (int i = 0; i < tcs.count(); ++i) { QCOMPARE(tcs.at(i).language, expectedLanguages.at(i)); QCOMPARE(tcs.at(i).compilerPath.toString(), expectedToolChains.at(i)); } } } // namespace Internal } // namespace CMakeProjectManager #endif