diff --git a/src/plugins/cmakeprojectmanager/CMakeLists.txt b/src/plugins/cmakeprojectmanager/CMakeLists.txt index d1ded9c5235..71830cf9b87 100644 --- a/src/plugins/cmakeprojectmanager/CMakeLists.txt +++ b/src/plugins/cmakeprojectmanager/CMakeLists.txt @@ -50,3 +50,11 @@ add_qtc_plugin(CMakeProjectManager 3rdparty/cmake/cmListFileCache.h 3rdparty/rstparser/rstparser.cc 3rdparty/rstparser/rstparser.h ) + +file(GLOB_RECURSE test_cases RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} testcases/*) +qtc_add_resources(CMakeProjectManager "testcases" + CONDITION WITH_TESTS + PREFIX "/cmakeprojectmanager" + BASE "." + FILES ${test_cases} +) diff --git a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp index e59c34bbf47..28cba515a2a 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp +++ b/src/plugins/cmakeprojectmanager/cmakebuildsystem.cpp @@ -62,6 +62,11 @@ #include #include +#ifdef WITH_TESTS +#include +#include +#endif + using namespace ProjectExplorer; using namespace TextEditor; using namespace Utils; @@ -1077,6 +1082,178 @@ void CMakeBuildSystem::buildNamedTarget(const QString &target) CMakeProjectManager::Internal::buildTarget(this, target); } +static Result insertDependencies( + const QString &targetName, + const FilePath &targetCMakeFile, + int targetDefinitionLine, + const QStringList &dependencies, + int qtMajorVersion) +{ + std::optional cmakeListFile = getUncachedCMakeListFile(targetCMakeFile); + if (!cmakeListFile) + return ResultError("Failed to read " + targetCMakeFile.toUserOutput()); + + std::optional function + = findFunction(*cmakeListFile, [targetDefinitionLine](const auto &func) { + return func.Line() == targetDefinitionLine; + }); + if (!function.has_value()) + return ResultError(QString("Failed to locate the target defining function at %1").arg(targetDefinitionLine)); + const int targetDefinitionLastLine = function->LineEnd(); + + // + // find_package + // + const QString qtPackage = QString("Qt%1").arg(qtMajorVersion); + function = findFunction( + *cmakeListFile, + [qtPackage](const auto &func) { + return func.LowerCaseName() == "find_package" && func.Arguments().size() > 0 + && func.Arguments()[0].Value == qtPackage; + }, + /* reverse = */ true); + + const QString findComponents = transform(dependencies, [](const QString &dep) { + QTC_ASSERT(dep.size() > 3, return dep); + return dep.mid(3); + }).join(" "); + QString snippet = QString("find_package(%1 REQUIRED COMPONENTS %2)\n%3") + .arg(qtPackage) + .arg(findComponents) + .arg(!function ? QString("\n") : QString("")); + + int insertionLine = function ? function->LineEnd() + 1 : targetDefinitionLine; + Result inserted = insertSnippetSilently(targetCMakeFile, {snippet, insertionLine, 0}); + if (!inserted) + return inserted; + const int insertedFindPackageOffset = 2; + + // + // target_link_libraries + // + cmakeListFile = getUncachedCMakeListFile(targetCMakeFile); + + function = findFunction( + *cmakeListFile, + [targetName](const auto &func) { + return func.LowerCaseName() == "target_link_libraries" && func.Arguments().size() > 0 + && func.Arguments()[0].Value == targetName; + }, + /* reverse = */ true); + + const QString targetPrefix = QString("Qt%1::").arg(qtMajorVersion); + const QString linkLibraries + = transform(dependencies, [targetPrefix](const QString &dep) -> QString { + QTC_ASSERT(dep.size() > 3, return targetPrefix + dep); + return targetPrefix + dep.mid(3); + }).join(" "); + snippet = QString("%1target_link_libraries(%2 PRIVATE %3)\n") + .arg(!function ? QString("\n") : QString("")) + .arg(targetName) + .arg(linkLibraries); + + insertionLine = (function ? function->LineEnd() + : targetDefinitionLastLine + insertedFindPackageOffset) + + 1; + return insertSnippetSilently(targetCMakeFile, {snippet, insertionLine, 0}); +} + +bool CMakeBuildSystem::addDependencies( + ProjectExplorer::Node *context, const QStringList &dependencies) +{ + if (auto n = dynamic_cast(context)) { + const QString targetName = n->buildKey(); + const std::optional cmakeFile = cmakeFileForBuildKey(targetName, buildTargets()); + if (!cmakeFile) + return false; + + int qtMajorVersion = 6; + if (auto qt = m_findPackagesFilesHash.value("Qt5Core"); qt.hasValidTarget()) + qtMajorVersion = 5; + + Result inserted = insertDependencies( + targetName, + cmakeFile->targetFilePath, + cmakeFile->targetLine, + dependencies, + qtMajorVersion); + if (!inserted) { + qCCritical(cmakeBuildSystemLog) << inserted.error(); + return false; + } + + return true; + } + return BuildSystem::addDependencies(context, dependencies); +} + +#ifdef WITH_TESTS +class AddDependenciesTest final : public QObject +{ + Q_OBJECT + +private slots: + void test() + { + const auto projectDir = std::make_unique( + ":/cmakeprojectmanager/testcases/adddependencies"); + + QVERIFY(insertDependencies( + "HelloQt", + projectDir->filePath().pathAppended("existing_qt5.cmake"), + 18, + {"Qt.Concurrent"}, + 5)); + QVERIFY(insertDependencies( + "HelloQt", + projectDir->filePath().pathAppended("existing_qt6.cmake"), + 8, + {"Qt.Concurrent"}, + 6)); + QVERIFY(insertDependencies( + "HelloCpp", + projectDir->filePath().pathAppended("no_qt6.cmake"), + 8, + {"Qt.Concurrent"}, + 6)); + + // Compare files. + static const QString suffix = "_expected.cmake"; + const FileFilter filter({"*" + suffix}, QDir::Files); + const FilePaths expectedDocuments = projectDir->filePath().dirEntries(filter); + QVERIFY(!expectedDocuments.isEmpty()); + for (const FilePath &expected : expectedDocuments) { + const FilePath actual = expected.parentDir().pathAppended( + expected.fileName().chopped(suffix.length()) + ".cmake"); + QVERIFY(actual.exists()); + const auto actualContents = actual.fileContents(); + QVERIFY(actualContents); + const auto expectedContents = expected.fileContents(); + const QByteArrayList actualLines = actualContents->split('\n'); + const QByteArrayList expectedLines = expectedContents->split('\n'); + if (actualLines.size() != expectedLines.size()) { + qDebug().noquote().nospace() << "---\n" << *expectedContents << "EOF"; + qDebug().noquote().nospace() << "+++\n" << *actualContents << "EOF"; + } + QCOMPARE(actualLines.size(), expectedLines.size()); + for (int i = 0; i < actualLines.size(); ++i) { + const QByteArray actualLine = actualLines.at(i); + const QByteArray expectedLine = expectedLines.at(i); + if (actualLine != expectedLine) + qDebug() << "Unexpected content in line" << (i + 1) << "of file" + << actual.fileName(); + QCOMPARE(actualLine, expectedLine); + } + } + } +}; + +QObject *createAddDependenciesTest() +{ + return new AddDependenciesTest; +} +#endif + FilePaths CMakeBuildSystem::filesGeneratedFrom(const FilePath &sourceFile) const { FilePath project = projectDirectory(); @@ -2608,3 +2785,7 @@ ExtraCompiler *CMakeBuildSystem::findExtraCompiler(const ExtraCompilerFilter &fi } } // CMakeProjectManager::Internal + +#ifdef WITH_TESTS +#include +#endif diff --git a/src/plugins/cmakeprojectmanager/cmakebuildsystem.h b/src/plugins/cmakeprojectmanager/cmakebuildsystem.h index 727ac43c01f..f4b299c68c3 100644 --- a/src/plugins/cmakeprojectmanager/cmakebuildsystem.h +++ b/src/plugins/cmakeprojectmanager/cmakebuildsystem.h @@ -68,6 +68,8 @@ public: Utils::FilePaths filesGeneratedFrom(const Utils::FilePath &sourceFile) const final; + bool addDependencies(ProjectExplorer::Node *context, const QStringList &dependencies) final; + // Actions: void runCMake(); void runCMakeAndScanProjectTree(); @@ -269,5 +271,9 @@ private: QString m_warning; }; +#ifdef WITH_TESTS +QObject *createAddDependenciesTest(); +#endif + } // namespace Internal } // namespace CMakeProjectManager diff --git a/src/plugins/cmakeprojectmanager/cmakeprojectmanager.qbs b/src/plugins/cmakeprojectmanager/cmakeprojectmanager.qbs index de342ee6d26..50cc25e5fe2 100644 --- a/src/plugins/cmakeprojectmanager/cmakeprojectmanager.qbs +++ b/src/plugins/cmakeprojectmanager/cmakeprojectmanager.qbs @@ -108,4 +108,10 @@ QtcPlugin { "rstparser/rstparser.h" ] } + + QtcTestFiles { + name: "test data" + files: "testcases/**/*" + fileTags: qtc.withPluginTests ? ["qt.core.resource_data"] : [] + } } diff --git a/src/plugins/cmakeprojectmanager/cmakeprojectplugin.cpp b/src/plugins/cmakeprojectmanager/cmakeprojectplugin.cpp index ff36f2f9702..bf5d7d0ea6b 100644 --- a/src/plugins/cmakeprojectmanager/cmakeprojectplugin.cpp +++ b/src/plugins/cmakeprojectmanager/cmakeprojectplugin.cpp @@ -80,6 +80,7 @@ class CMakeProjectPlugin final : public ExtensionSystem::IPlugin addTestCreator(createCMakeOutputParserTest); addTestCreator(createCMakeAutogenParserTest); addTestCreator(createCMakeProjectImporterTest); + addTestCreator(createAddDependenciesTest); #endif FileIconProvider::registerIconOverlayForSuffix(Constants::Icons::FILE_OVERLAY, "cmake"); diff --git a/src/plugins/cmakeprojectmanager/testcases/adddependencies/existing_qt5.cmake b/src/plugins/cmakeprojectmanager/testcases/adddependencies/existing_qt5.cmake new file mode 100644 index 00000000000..41cb87cbf16 --- /dev/null +++ b/src/plugins/cmakeprojectmanager/testcases/adddependencies/existing_qt5.cmake @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.1.0) + +project(HelloQt VERSION 1.0.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +if(CMAKE_VERSION VERSION_LESS "3.7.0") + set(CMAKE_INCLUDE_CURRENT_DIR ON) +endif() + +find_package(Qt5 COMPONENTS Widgets REQUIRED) + +add_executable(HelloQt + mainwindow.ui + mainwindow.cpp + main.cpp + resources.qrc +) + +target_link_libraries(HelloQt Qt5::Widgets) diff --git a/src/plugins/cmakeprojectmanager/testcases/adddependencies/existing_qt5_expected.cmake b/src/plugins/cmakeprojectmanager/testcases/adddependencies/existing_qt5_expected.cmake new file mode 100644 index 00000000000..781cc2ecfb3 --- /dev/null +++ b/src/plugins/cmakeprojectmanager/testcases/adddependencies/existing_qt5_expected.cmake @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.1.0) + +project(HelloQt VERSION 1.0.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +if(CMAKE_VERSION VERSION_LESS "3.7.0") + set(CMAKE_INCLUDE_CURRENT_DIR ON) +endif() + +find_package(Qt5 COMPONENTS Widgets REQUIRED) +find_package(Qt5 REQUIRED COMPONENTS Concurrent) + +add_executable(HelloQt + mainwindow.ui + mainwindow.cpp + main.cpp + resources.qrc +) + +target_link_libraries(HelloQt Qt5::Widgets) +target_link_libraries(HelloQt PRIVATE Qt5::Concurrent) diff --git a/src/plugins/cmakeprojectmanager/testcases/adddependencies/existing_qt6.cmake b/src/plugins/cmakeprojectmanager/testcases/adddependencies/existing_qt6.cmake new file mode 100644 index 00000000000..a77b901a98b --- /dev/null +++ b/src/plugins/cmakeprojectmanager/testcases/adddependencies/existing_qt6.cmake @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.19) +project(HelloQt LANGUAGES CXX) + +find_package(Qt6 6.5 REQUIRED COMPONENTS Core Widgets) + +qt_standard_project_setup() + +qt_add_executable(HelloQt + WIN32 MACOSX_BUNDLE + main.cpp + mainwindow.cpp + mainwindow.h + mainwindow.ui +) + +target_link_libraries(HelloQt + PRIVATE + Qt::Core + Qt::Widgets +) + +include(GNUInstallDirs) + +install(TARGETS HelloQt + BUNDLE DESTINATION . + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + +qt_generate_deploy_app_script( + TARGET HelloQt + OUTPUT_SCRIPT deploy_script + NO_UNSUPPORTED_PLATFORM_ERROR +) +install(SCRIPT ${deploy_script}) diff --git a/src/plugins/cmakeprojectmanager/testcases/adddependencies/existing_qt6_expected.cmake b/src/plugins/cmakeprojectmanager/testcases/adddependencies/existing_qt6_expected.cmake new file mode 100644 index 00000000000..a27c8fdf0f2 --- /dev/null +++ b/src/plugins/cmakeprojectmanager/testcases/adddependencies/existing_qt6_expected.cmake @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.19) +project(HelloQt LANGUAGES CXX) + +find_package(Qt6 6.5 REQUIRED COMPONENTS Core Widgets) +find_package(Qt6 REQUIRED COMPONENTS Concurrent) + +qt_standard_project_setup() + +qt_add_executable(HelloQt + WIN32 MACOSX_BUNDLE + main.cpp + mainwindow.cpp + mainwindow.h + mainwindow.ui +) + +target_link_libraries(HelloQt + PRIVATE + Qt::Core + Qt::Widgets +) +target_link_libraries(HelloQt PRIVATE Qt6::Concurrent) + +include(GNUInstallDirs) + +install(TARGETS HelloQt + BUNDLE DESTINATION . + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + +qt_generate_deploy_app_script( + TARGET HelloQt + OUTPUT_SCRIPT deploy_script + NO_UNSUPPORTED_PLATFORM_ERROR +) +install(SCRIPT ${deploy_script}) diff --git a/src/plugins/cmakeprojectmanager/testcases/adddependencies/no_qt6.cmake b/src/plugins/cmakeprojectmanager/testcases/adddependencies/no_qt6.cmake new file mode 100644 index 00000000000..0da4cba56f4 --- /dev/null +++ b/src/plugins/cmakeprojectmanager/testcases/adddependencies/no_qt6.cmake @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.16) + +project(HelloCpp LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(HelloCpp main.cpp) + +include(GNUInstallDirs) +install(TARGETS HelloCpp + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) diff --git a/src/plugins/cmakeprojectmanager/testcases/adddependencies/no_qt6_expected.cmake b/src/plugins/cmakeprojectmanager/testcases/adddependencies/no_qt6_expected.cmake new file mode 100644 index 00000000000..568ead4f552 --- /dev/null +++ b/src/plugins/cmakeprojectmanager/testcases/adddependencies/no_qt6_expected.cmake @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.16) + +project(HelloCpp LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 REQUIRED COMPONENTS Concurrent) + +add_executable(HelloCpp main.cpp) + +target_link_libraries(HelloCpp PRIVATE Qt6::Concurrent) + +include(GNUInstallDirs) +install(TARGETS HelloCpp + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +)