diff options
author | Knud Dollereder <knud.dollereder@qt.io> | 2024-02-01 15:17:58 +0100 |
---|---|---|
committer | Knud Dollereder <knud.dollereder@qt.io> | 2024-02-05 16:21:24 +0000 |
commit | 8b97598011b79152c5addc367f938556cefc7fb2 (patch) | |
tree | 71f7493ba0be6158553a3a56a9d3698d7f65e309 | |
parent | c58efc4310d25bdbd62766f0efa087cbdafcd1f1 (diff) |
QmlProjectManager: Add new cmake generator
Automatic cmake generation can now be enabled by setting
the qmlproject property enableCMakeGeneration to true
Change-Id: I98523a9479d0cd812e43a9bd0b700120358260f6
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: Qt CI Patch Build Bot <ci_patchbuild_bot@qt.io>
Reviewed-by: Tim Jenssen <tim.jenssen@qt.io>
Reviewed-by: Burak Hancerli <burak.hancerli@qt.io>
27 files changed, 818 insertions, 2 deletions
diff --git a/src/plugins/qmlprojectmanager/CMakeLists.txt b/src/plugins/qmlprojectmanager/CMakeLists.txt index a1212e8bc1..b6fa746996 100644 --- a/src/plugins/qmlprojectmanager/CMakeLists.txt +++ b/src/plugins/qmlprojectmanager/CMakeLists.txt @@ -50,6 +50,7 @@ extend_qtc_plugin(QmlProjectManager cmakeprojectconverterdialog.cpp cmakeprojectconverterdialog.h generatecmakelists.cpp generatecmakelists.h generatecmakelistsconstants.h + cmakegenerator.cpp cmakegenerator.h boilerplate.qrc ) @@ -57,7 +58,7 @@ add_qtc_library(QmlProjectManagerLib OBJECT CONDITION Qt6_VERSION VERSION_GREATER_EQUAL 6.4.3 EXCLUDE_FROM_INSTALL DEPENDS - QmlJS Utils + QmlJS Utils ProjectExplorer INCLUDES ${CMAKE_CURRENT_LIST_DIR} SOURCES_PREFIX ${CMAKE_CURRENT_LIST_DIR}/buildsystem diff --git a/src/plugins/qmlprojectmanager/buildsystem/projectitem/converters.cpp b/src/plugins/qmlprojectmanager/buildsystem/projectitem/converters.cpp index b35679ed34..08fba2c098 100644 --- a/src/plugins/qmlprojectmanager/buildsystem/projectitem/converters.cpp +++ b/src/plugins/qmlprojectmanager/buildsystem/projectitem/converters.cpp @@ -143,6 +143,7 @@ QString jsonToQmlProject(const QJsonObject &rootObject) appendString("mainFile", runConfig["mainFile"].toString()); appendString("mainUiFile", runConfig["mainUiFile"].toString()); appendString("targetDirectory", deploymentConfig["targetDirectory"].toString()); + appendBool("enableCMakeGeneration", deploymentConfig["enableCMakeGeneration"].toBool()); appendBool("widgetApp", runConfig["widgetApp"].toBool()); appendStringArray("importPaths", rootObject["importPaths"].toVariant().toStringList()); appendBreak(); @@ -282,7 +283,8 @@ QJsonObject qmlProjectTojson(const Utils::FilePath &projectFile) || propName.contains("mainuifile", Qt::CaseInsensitive) || propName.contains("forcefreetype", Qt::CaseInsensitive)) { currentObj = &runConfigObject; - } else if (propName.contains("targetdirectory", Qt::CaseInsensitive)) { + } else if (propName.contains("targetdirectory", Qt::CaseInsensitive) + || propName.contains("enableCMakeGeneration", Qt::CaseInsensitive)) { currentObj = &deploymentObject; } else if (propName.contains("qtformcus", Qt::CaseInsensitive)) { qtForMCUs = value.toBool(); diff --git a/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.cpp b/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.cpp index 77d0eea82d..5524e927e5 100644 --- a/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.cpp +++ b/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.cpp @@ -422,4 +422,16 @@ void QmlProjectItem::insertAndUpdateProjectFile(const QString &key, const QJsonV m_projectFile.writeFileContents(Converters::jsonToQmlProject(m_project).toUtf8()); } +bool QmlProjectItem::enableCMakeGeneration() const +{ + return m_project["deployment"].toObject()["enableCMakeGeneration"].toBool(); +} + +void QmlProjectItem::setEnableCMakeGeneration(bool enable) +{ + QJsonObject obj = m_project["deployment"].toObject(); + obj["enableCMakeGeneration"] = enable; + insertAndUpdateProjectFile("deployment", obj); +} + } // namespace QmlProjectManager diff --git a/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.h b/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.h index 214614f536..7d57ad2e60 100644 --- a/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.h +++ b/src/plugins/qmlprojectmanager/buildsystem/projectitem/qmlprojectitem.h @@ -88,6 +88,9 @@ public: QJsonObject project() const; + bool enableCMakeGeneration() const; + void setEnableCMakeGeneration(bool enable); + signals: void qmlFilesChanged(const QSet<QString> &, const QSet<QString> &); diff --git a/src/plugins/qmlprojectmanager/buildsystem/qmlbuildsystem.cpp b/src/plugins/qmlprojectmanager/buildsystem/qmlbuildsystem.cpp index 5131d9c57c..e91fd72baa 100644 --- a/src/plugins/qmlprojectmanager/buildsystem/qmlbuildsystem.cpp +++ b/src/plugins/qmlprojectmanager/buildsystem/qmlbuildsystem.cpp @@ -77,6 +77,7 @@ void updateMcuBuildStep(Target *target, bool mcuEnabled) QmlBuildSystem::QmlBuildSystem(Target *target) : BuildSystem(target) + , m_cmakeGen(new GenerateCmake::CMakeGenerator(this, this)) { // refresh first - project information is used e.g. to decide the default RC's refresh(RefreshOptions::Project); @@ -86,10 +87,12 @@ QmlBuildSystem::QmlBuildSystem(Target *target) connect(target->project(), &Project::activeTargetChanged, this, [this](Target *target) { refresh(RefreshOptions::NoFileRefresh); + m_cmakeGen->initialize(qmlProject()); updateMcuBuildStep(target, qtForMCUs()); }); connect(target->project(), &Project::projectFileIsDirty, this, [this]() { refresh(RefreshOptions::Project); + m_cmakeGen->initialize(qmlProject()); updateMcuBuildStep(project()->activeTarget(), qtForMCUs()); }); @@ -220,6 +223,13 @@ void QmlBuildSystem::initProjectItem() &QmlProjectItem::qmlFilesChanged, this, &QmlBuildSystem::refreshFiles); + + connect(m_projectItem.get(), + &QmlProjectItem::qmlFilesChanged, + m_cmakeGen, + &GenerateCmake::CMakeGenerator::update); + + m_cmakeGen->setEnabled(m_projectItem->enableCMakeGeneration()); } void QmlBuildSystem::parseProjectFiles() diff --git a/src/plugins/qmlprojectmanager/buildsystem/qmlbuildsystem.h b/src/plugins/qmlprojectmanager/buildsystem/qmlbuildsystem.h index 3615979e2d..524af5cd1b 100644 --- a/src/plugins/qmlprojectmanager/buildsystem/qmlbuildsystem.h +++ b/src/plugins/qmlprojectmanager/buildsystem/qmlbuildsystem.h @@ -8,6 +8,8 @@ #include "../qmlprojectmanager_global.h" #include <projectexplorer/buildsystem.h> +#include "qmlprojectmanager/cmakegen/cmakegenerator.h" + namespace QmlProjectManager { class QmlProject; @@ -122,6 +124,8 @@ private: void registerMenuButtons(); void updateDeploymentData(); friend class FilesUpdateBlocker; + + GenerateCmake::CMakeGenerator* m_cmakeGen; }; } // namespace QmlProjectManager diff --git a/src/plugins/qmlprojectmanager/cmakegen/boilerplate.qrc b/src/plugins/qmlprojectmanager/cmakegen/boilerplate.qrc index a89643d6ff..10fab59838 100644 --- a/src/plugins/qmlprojectmanager/cmakegen/boilerplate.qrc +++ b/src/plugins/qmlprojectmanager/cmakegen/boilerplate.qrc @@ -1,5 +1,8 @@ <RCC> <qresource prefix="/boilerplatetemplates"> + <file>gencmakeroot.tpl</file> + <file>gencmakemodule.tpl</file> + <file>gencmakeheadercomponents.tpl</file> <file>qmlprojectmaincpp.tpl</file> <file>qmlprojectmaincppheader.tpl</file> <file>qmlprojectenvheader.tpl</file> diff --git a/src/plugins/qmlprojectmanager/cmakegen/cmakegenerator.cpp b/src/plugins/qmlprojectmanager/cmakegen/cmakegenerator.cpp new file mode 100644 index 0000000000..55bcea6a87 --- /dev/null +++ b/src/plugins/qmlprojectmanager/cmakegen/cmakegenerator.cpp @@ -0,0 +1,550 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "cmakegenerator.h" +#include "generatecmakelistsconstants.h" + +#include "projectexplorer/projectmanager.h" +#include "projectexplorer/projectnodes.h" +#include "qmlprojectmanager/qmlproject.h" + +#include <QRegularExpression> + +#include <set> + +namespace QmlProjectManager { + +namespace GenerateCmake { + +const char TEMPLATE_CMAKELISTS_ROOT[] = ":/boilerplatetemplates/gencmakeroot.tpl"; +const char TEMPLATE_CMAKELISTS_MODULE[] = ":/boilerplatetemplates/gencmakemodule.tpl"; + +const char TEMPLATE_QMLMODULES[] = ":/boilerplatetemplates/qmlprojectmodules.tpl"; +const char TEMPLATE_SOURCE_MAIN[] = ":/boilerplatetemplates/qmlprojectmaincpp.tpl"; +const char TEMPLATE_HEADER_IMPORT_COMPS[] = ":/boilerplatetemplates/gencmakeheadercomponents.tpl"; +const char TEMPLATE_HEADER_IMPORT_PLUGINS[] = ":/boilerplatetemplates/qmlprojectmaincppheader.tpl"; +const char TEMPLATE_HEADER_ENVIRONMENT[] = ":/boilerplatetemplates/qmlprojectenvheader.tpl"; + +const char DO_NOT_EDIT_FILE_COMMENT[] = +"### This file is automatically generated by Qt Design Studio.\n" +"### Do not change\n\n"; + +const char ADD_SUBDIR[] = "add_subdirectory(%1)\n"; + +const char BIG_RESOURCE_TEMPLATE[] = R"( +qt6_add_resources(%1 %2 + BIG_RESOURCES + PREFIX "%3" + VERSION 1.0 + FILES %4 +))"; + +CMakeGenerator::CMakeGenerator(QmlBuildSystem *bs, QObject *parent) + : QObject(parent) + , m_root(std::make_shared<Node>()) + , m_buildSystem(bs) +{} + +void CMakeGenerator::setEnabled(bool enabled) +{ + m_enabled = enabled; +} + +void CMakeGenerator::initialize(QmlProject *project) +{ + if (!m_enabled) + return; + + m_srcs.clear(); + m_moduleNames.clear(); + + m_root = std::make_shared<Node>(); + m_root->name = QString("Root"); + m_root->dir = project->rootProjectDirectory(); + + m_projectName = project->displayName(); + + ProjectExplorer::ProjectNode *rootProjectNode = project->rootProjectNode(); + parseNodeTree(m_root, rootProjectNode); + parseSourceTree(); + + createCMakeFiles(m_root); + createEntryPoints(m_root); +} + +void CMakeGenerator::update(const QSet<QString> &added, const QSet<QString> &removed) +{ + if (!m_enabled) + return; + + std::set<NodePtr> dirtyModules; + for (const QString &add : added) { + const Utils::FilePath path = Utils::FilePath::fromString(add); + if (auto node = findOrCreateNode(m_root, path)) { + insertFile(node, path); + if (auto module = findModuleFor(node)) + dirtyModules.insert(module); + } else { + qDebug() << "CmakeGen: Failed to find Folder node " << path; + } + } + + for (const QString &remove : removed) { + const Utils::FilePath path = Utils::FilePath::fromString(remove); + if (auto node = findNode(m_root, path)) { + removeFile(node, path); + if (auto module = findModuleFor(node)) + dirtyModules.insert(module); + } + } + + for (auto module : dirtyModules) + createModuleCMakeFile(module); +} + +std::vector<Utils::FilePath> CMakeGenerator::qmlFiles(const NodePtr &node) const +{ + std::vector<Utils::FilePath> out = node->files; + for (const NodePtr &child : node->subdirs) { + if (child->module) + continue; + + auto childFiles = qmlFiles(child); + out.insert(out.end(), childFiles.begin(), childFiles.end()); + } + return out; +} + +std::vector<Utils::FilePath> CMakeGenerator::singletons(const NodePtr &node) const +{ + std::vector<Utils::FilePath> out = node->singletons; + for (const NodePtr &child : node->subdirs) { + if (child->module) + continue; + + auto childFiles = singletons(child); + out.insert(out.end(), childFiles.begin(), childFiles.end()); + } + return out; +} + +std::vector<Utils::FilePath> CMakeGenerator::resources(const NodePtr &node) const +{ + std::vector<Utils::FilePath> out = node->resources; + for (const NodePtr &child : node->subdirs) { + if (child->module) + continue; + + auto childFiles = resources(child); + out.insert(out.end(), childFiles.begin(), childFiles.end()); + } + return out; +} + +std::vector<Utils::FilePath> CMakeGenerator::sources(const NodePtr &node) const +{ + std::vector<Utils::FilePath> out = node->sources; + for (const NodePtr &child : node->subdirs) { + if (child->module) + continue; + + auto childFiles = sources(child); + out.insert(out.end(), childFiles.begin(), childFiles.end()); + } + return out; +} + +void CMakeGenerator::createCMakeFiles(const NodePtr &node) const +{ + if (node->name == "Root") { + createMainCMakeFile(node); + createQmlModuleFile(node); + } else if (node->module || hasChildModule(node)) { + createModuleCMakeFile(node); + } + for (const NodePtr &n : node->subdirs) + createCMakeFiles(n); +} + +void CMakeGenerator::createMainCMakeFile(const NodePtr &node) const +{ + const QString appName = m_projectName + "App"; + + const QString qtcontrolsConfFile = makeEnvironmentVariable(Constants::ENV_VARIABLE_CONTROLCONF); + + QString fileSection = ""; + if (!qtcontrolsConfFile.isEmpty()) + fileSection = QString(" FILES\n %1").arg(qtcontrolsConfFile); + + const QString fileTemplate = readTemplate(TEMPLATE_CMAKELISTS_ROOT); + const QString fileContent = fileTemplate.arg(appName, m_srcs.join(" "), fileSection); + + const Utils::FilePath file = node->dir.pathAppended("CMakeLists.txt"); + writeFile(file, fileContent); +} + +void CMakeGenerator::createQmlModuleFile(const NodePtr &node) const +{ + const QString appName = m_projectName + "App"; + + QString subdirIncludes; + for (const NodePtr &n : node->subdirs) + subdirIncludes.append(QString(ADD_SUBDIR).arg(n->name)); + + QString modulesAsPlugins; + for (const QString &moduleName : m_moduleNames) + modulesAsPlugins.append(" " + moduleName + "plugin\n"); + + const QString fileTemplate = readTemplate(TEMPLATE_QMLMODULES); + const QString fileContent = fileTemplate.arg(appName, subdirIncludes, modulesAsPlugins); + + const Utils::FilePath file = node->dir.pathAppended("qmlModules"); + writeFile(file, fileContent); +} + +void CMakeGenerator::createModuleCMakeFile(const NodePtr &node) const +{ + QString subDirContent; + for (const NodePtr &n : node->subdirs) { + if (n->module || hasChildModule(n)) + subDirContent.append(QString(ADD_SUBDIR).arg(n->dir.fileName())); + } + + QString content; + if (!node->module && hasChildModule(node)) { + content.append(DO_NOT_EDIT_FILE_COMMENT); + content.append(subDirContent); + Utils::FilePath file = node->dir.pathAppended("CMakeLists.txt"); + writeFile(file, content); + return; + } + + auto makeRelative = [](const Utils::FilePath &base, + const Utils::FilePath &converted) -> QString { + return "\"" + Utils::FilePath::calcRelativePath(converted.toString(), base.toString()) + "\""; + }; + + QString uri = node->uri; + if (uri.isEmpty()) + uri = node->dir.baseName(); + + const QString setProperties( + "set_source_files_properties(%1\n PROPERTIES\n %2 %3\n)\n\n"); + + for (const Utils::FilePath &path : node->singletons) { + content.append(setProperties.arg(path.fileName()).arg("QT_QML_SINGLETON_TYPE").arg("true")); + } + + if (!subDirContent.isEmpty()) + content.append(subDirContent); + + QString qmlFileContent; + for (const Utils::FilePath &path : qmlFiles(node)) { + qmlFileContent.append(QString(" %1\n").arg(makeRelative(node->dir, path))); + } + + QString moduleContent; + if (!qmlFileContent.isEmpty()) + moduleContent.append(QString(" QML_FILES\n%1").arg(qmlFileContent)); + + std::vector<QString> bigResources; + QString resourceFiles; + for (const Utils::FilePath &path : resources(node)) { + if (path.fileSize() > 5000000) { + bigResources.push_back(makeRelative(node->dir, path)); + continue; + } + resourceFiles.append(QString(" %1\n").arg(makeRelative(node->dir, path))); + } + + if (!resourceFiles.isEmpty()) + moduleContent.append(QString(" RESOURCES\n%1").arg(resourceFiles)); + + QString bigResourceContent; + if (!bigResources.empty()) { + QString resourceContent; + for (const QString &res : bigResources) + resourceContent.append(QString("\n %1").arg(res)); + + const QString prefixPath = QString(uri).replace('.', '/'); + const QString prefix = "/qt/qml/" + prefixPath; + const QString resourceName = node->name + "BigResource"; + + bigResourceContent = QString::fromUtf8(BIG_RESOURCE_TEMPLATE, -1) + .arg(node->name, resourceName, prefix, resourceContent); + } + + const QString fileTemplate = readTemplate(TEMPLATE_CMAKELISTS_MODULE); + const QString fileContent = + fileTemplate.arg(content, node->name, uri, moduleContent, bigResourceContent); + + const Utils::FilePath file = node->dir.pathAppended("CMakeLists.txt"); + writeFile(file, fileContent); +} + +void CMakeGenerator::createEntryPoints(const NodePtr &node) const +{ + createMainCppFile(node); +} + +void CMakeGenerator::createMainCppFile(const NodePtr &node) const +{ + const Utils::FilePath srcDir = node->dir.pathAppended(Constants::DIRNAME_CPP); + if (!srcDir.exists()) { + srcDir.createDir(); + + const Utils::FilePath componentsHeaderPath = srcDir.pathAppended( + "import_qml_components_plugins.h"); + const QString componentsHeaderContent = readTemplate(TEMPLATE_HEADER_IMPORT_COMPS); + writeFile(componentsHeaderPath, componentsHeaderContent); + + const Utils::FilePath cppFilePath = srcDir.pathAppended("main.cpp"); + const QString cppContent = readTemplate(TEMPLATE_SOURCE_MAIN); + writeFile(cppFilePath, cppContent); + + const Utils::FilePath envHeaderPath = srcDir.pathAppended("app_environment.h"); + if (m_buildSystem) { + QString environment; + const QString qtcontrolsConfFile = makeEnvironmentVariable( + Constants::ENV_VARIABLE_CONTROLCONF); + for (Utils::EnvironmentItem &envItem : m_buildSystem->environment()) { + QString key = envItem.name; + QString value = envItem.value; + if (value == qtcontrolsConfFile) + value.prepend(":/"); + environment.append(QString(" qputenv(\"%1\", \"%2\");\n").arg(key).arg(value)); + } + const QString envHeaderContent = readTemplate(TEMPLATE_HEADER_ENVIRONMENT).arg(environment); + writeFile(envHeaderPath, envHeaderContent); + } + } + + QString moduleContent; + for (const QString &module : m_moduleNames) + moduleContent.append(QString("Q_IMPORT_QML_PLUGIN(%1)\n").arg(module + "Plugin")); + + const QString headerContent = readTemplate(TEMPLATE_HEADER_IMPORT_PLUGINS).arg(moduleContent); + const Utils::FilePath headerFilePath = srcDir.pathAppended("import_qml_plugins.h"); + writeFile(headerFilePath, headerContent); +} + +void CMakeGenerator::writeFile(const Utils::FilePath &path, const QString &content) const +{ + QFile fileHandle(path.toString()); + fileHandle.open(QIODevice::WriteOnly); + QTextStream stream(&fileHandle); + stream << content; + fileHandle.close(); +} + +QString CMakeGenerator::makeEnvironmentVariable(const QString &key) const +{ + QString value = {}; + + if (m_buildSystem) { + auto envItems = m_buildSystem->environment(); + auto confEnv = std::find_if(envItems.begin(), + envItems.end(), + [key](Utils::NameValueItem &item) { return item.name == key; }); + if (confEnv != envItems.end()) + value = confEnv->value; + } + return value; +} + +QString CMakeGenerator::readTemplate(const QString &templatePath) const +{ + QFile templatefile(templatePath); + templatefile.open(QIODevice::ReadOnly); + QTextStream stream(&templatefile); + QString content = stream.readAll(); + templatefile.close(); + return content; +} + +void CMakeGenerator::readQmlDir(const Utils::FilePath &filePath, NodePtr &node) const +{ + node->module = true; + + QFile f(filePath.toString()); + f.open(QIODevice::ReadOnly); + QTextStream stream(&f); + + Utils::FilePath dir = filePath.parentDir(); + while (!stream.atEnd()) { + const QString line = stream.readLine(); + const QStringList tokenizedLine = line.split(QRegularExpression("\\s+")); + const QString maybeFileName = tokenizedLine.last(); + if (tokenizedLine.first().compare("module", Qt::CaseInsensitive) == 0) { + node->uri = tokenizedLine.last(); + node->name = QString(node->uri).replace('.', '_'); + } else if (maybeFileName.endsWith(".qml", Qt::CaseInsensitive)) { + Utils::FilePath tmp = dir.pathAppended(maybeFileName); + if (tokenizedLine.first() == "singleton") + node->singletons.push_back(tmp); + } + } + + f.close(); +} + +CMakeGenerator::NodePtr CMakeGenerator::findModuleFor(const NodePtr &node) const +{ + NodePtr current = node; + while (current->parent) { + if (current->module) + return current; + + current = current->parent; + } + return nullptr; +} + +CMakeGenerator::NodePtr CMakeGenerator::findNode(NodePtr &node, const Utils::FilePath &path) const +{ + const Utils::FilePath parentDir = path.parentDir(); + for (NodePtr &child : node->subdirs) { + if (child->dir == parentDir) + return child; + if (path.isChildOf(child->dir)) + return findNode(child, path); + } + return nullptr; +} + +CMakeGenerator::NodePtr CMakeGenerator::findOrCreateNode(NodePtr &node, + const Utils::FilePath &path) const +{ + if (auto found = findNode(node, path)) + return found; + + if (!path.isChildOf(node->dir)) + return nullptr; + + const Utils::FilePath parentDir = path.parentDir(); + const Utils::FilePath relative = parentDir.relativeChildPath(node->dir); + const QChar separator = relative.pathComponentSeparator(); + const QList<QStringView> components = relative.pathView().split(separator); + + NodePtr last = node; + for (const auto &comp : components) { + NodePtr newNode = std::make_shared<Node>(); + newNode->parent = last; + newNode->name = comp.toString(); + newNode->dir = last->dir.pathAppended(comp.toString()); + last->subdirs.push_back(newNode); + last = newNode; + } + return last; +} + +void CMakeGenerator::insertFile(NodePtr &node, const Utils::FilePath &path) const +{ + if (path.fileName() == "qmldir") { + readQmlDir(path, node); + } else if (path.suffix() == "qml" || path.suffix() == "ui.qml") { + node->files.push_back(path); + } else if (path.suffix() == "cpp") { + node->sources.push_back(path); + } else if (isResource(path)) { + node->resources.push_back(path); + } +} + +void CMakeGenerator::removeFile(NodePtr &node, const Utils::FilePath &path) const +{ + if (path.fileName() == "qmldir") { + node->module = false; + node->singletons.clear(); + node->uri = ""; + node->name = path.parentDir().fileName(); + + } else if (path.suffix() == "qml") { + auto iter = std::find(node->files.begin(), node->files.end(), path); + if (iter != node->files.end()) + node->files.erase(iter); + } else if (isResource(path)) { + auto iter = std::find(node->resources.begin(), node->resources.end(), path); + if (iter != node->resources.end()) + node->resources.erase(iter); + } +} + +bool CMakeGenerator::hasChildModule(const NodePtr &node) const +{ + for (const NodePtr &child : node->subdirs) { + if (child->module) + return true; + if (hasChildModule(child)) + return true; + } + return false; +} + +bool CMakeGenerator::isResource(const Utils::FilePath &path) const +{ + static const QStringList suffixes = { + "json", "mesh", "dae", "qad", "hints", "png", "hdr", "ttf", "jpg", + "JPG", "js", "qsb", "frag", "frag.qsb", "vert", "vert.qsb", "svg", "ktx"}; + return suffixes.contains(path.suffix()); +} + +void CMakeGenerator::printNodeTree(const NodePtr &generatorNode, size_t indent) const +{ + auto addIndent = [](size_t level) -> QString { + QString str; + for (size_t i = 0; i < level; ++i) + str += " "; + return str; + }; + + qDebug() << addIndent(indent) << "GeneratorNode: " << generatorNode->name; + qDebug() << addIndent(indent) << "directory: " << generatorNode->dir; + qDebug() << addIndent(indent) << "files: " << generatorNode->files; + qDebug() << addIndent(indent) << "singletons: " << generatorNode->singletons; + qDebug() << addIndent(indent) << "resources: " << generatorNode->resources; + qDebug() << addIndent(indent) << "sources: " << generatorNode->sources; + + for (const auto &child : generatorNode->subdirs) + printNodeTree(child, indent + 1); +} + +void CMakeGenerator::parseNodeTree(NodePtr &generatorNode, + const ProjectExplorer::FolderNode *folderNode) +{ + for (const auto *childNode : folderNode->nodes()) { + if (const auto *subFolderNode = childNode->asFolderNode()) { + CMakeGenerator::NodePtr childGeneratorNode = std::make_shared<Node>(); + childGeneratorNode->parent = generatorNode; + childGeneratorNode->name = subFolderNode->displayName(); + childGeneratorNode->dir = subFolderNode->filePath(); + parseNodeTree(childGeneratorNode, subFolderNode); + generatorNode->subdirs.push_back(childGeneratorNode); + } else if (auto *fileNode = childNode->asFileNode()) { + insertFile(generatorNode, fileNode->filePath()); + } + } + + if (generatorNode->name == "content") + generatorNode->module = true; + + if (generatorNode->module) + m_moduleNames.push_back(generatorNode->name); +} + +void CMakeGenerator::parseSourceTree() +{ + m_srcs.clear(); + const QString srcDir = m_root->dir.pathAppended(Constants::DIRNAME_CPP).path(); + QDirIterator it(srcDir, QStringList({"*.cpp"}), QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + QString relative = Utils::FilePath::calcRelativePath(it.next(), m_root->dir.path()); + m_srcs.push_back(relative); + } + + if (m_srcs.empty()) + m_srcs.push_back("src/main.cpp"); +} + +} // namespace GenerateCmake +} // namespace QmlProjectManager diff --git a/src/plugins/qmlprojectmanager/cmakegen/cmakegenerator.h b/src/plugins/qmlprojectmanager/cmakegen/cmakegenerator.h new file mode 100644 index 0000000000..54445d3561 --- /dev/null +++ b/src/plugins/qmlprojectmanager/cmakegen/cmakegenerator.h @@ -0,0 +1,101 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +#pragma once + +#include "utils/filepath.h" + +#include <QObject> + +namespace ProjectExplorer { +class FolderNode; +} + +namespace QmlProjectManager { + +class QmlProject; +class QmlBuildSystem; + +namespace GenerateCmake { + +// TODO: +// - Create "module" for src dir +// - Replace AppName in templates with ${CMAKE_PROJECT_NAME} +// - Introduce Blacklist (designer) + +class CMakeGenerator : public QObject +{ + Q_OBJECT + +public: + CMakeGenerator(QmlBuildSystem *bs, QObject *parent = nullptr); + + void setEnabled(bool enabled); + + void initialize(QmlProject *project); + + void update(const QSet<QString> &added, const QSet<QString> &removed); + +private: + struct Node + { + std::shared_ptr<Node> parent = nullptr; + bool module = false; + + QString uri; + QString name; + Utils::FilePath dir; + + std::vector<std::shared_ptr<Node>> subdirs; + std::vector<Utils::FilePath> files; + std::vector<Utils::FilePath> singletons; + std::vector<Utils::FilePath> resources; + std::vector<Utils::FilePath> sources; + }; + + using NodePtr = std::shared_ptr<Node>; + + std::vector<Utils::FilePath> qmlFiles(const NodePtr &node) const; + std::vector<Utils::FilePath> singletons(const NodePtr &node) const; + std::vector<Utils::FilePath> resources(const NodePtr &node) const; + std::vector<Utils::FilePath> sources(const NodePtr &node) const; + + void createCMakeFiles(const NodePtr &node) const; + + void createQmlModuleFile(const NodePtr &node) const; + void createMainCMakeFile(const NodePtr &node) const; + void createModuleCMakeFile(const NodePtr &node) const; + + void createEntryPoints(const NodePtr &node) const; + void createMainCppFile(const NodePtr &node) const; + void writeFile(const Utils::FilePath &path, const QString &content) const; + + QString makeEnvironmentVariable(const QString &key) const; + + QString readTemplate(const QString &templatePath) const; + void readQmlDir(const Utils::FilePath &filePath, NodePtr &node) const; + + NodePtr findModuleFor(const NodePtr &node) const; + NodePtr findNode(NodePtr &node, const Utils::FilePath &path) const; + NodePtr findOrCreateNode(NodePtr &node, const Utils::FilePath &path) const; + + void insertFile(NodePtr &node, const Utils::FilePath &path) const; + void removeFile(NodePtr &node, const Utils::FilePath &path) const; + + bool hasChildModule(const NodePtr &node) const; + bool isResource(const Utils::FilePath &path) const; + + void printNodeTree(const NodePtr &generatorNode, size_t indent = 0) const; + + void parseNodeTree(NodePtr &generatorNode, const ProjectExplorer::FolderNode *folderNode); + void parseSourceTree(); + + bool m_enabled = false; + QString m_projectName = {}; + NodePtr m_root = {}; + QStringList m_srcs = {}; + std::vector<QString> m_moduleNames = {}; + QmlBuildSystem *m_buildSystem = nullptr; +}; + +} // namespace GenerateCmake +} // namespace QmlProjectManager diff --git a/src/plugins/qmlprojectmanager/cmakegen/gencmakeheadercomponents.tpl b/src/plugins/qmlprojectmanager/cmakegen/gencmakeheadercomponents.tpl new file mode 100644 index 0000000000..167481d7c7 --- /dev/null +++ b/src/plugins/qmlprojectmanager/cmakegen/gencmakeheadercomponents.tpl @@ -0,0 +1,19 @@ +/* + * This file is automatically generated by Qt Design Studio. + * Do not change. +*/ + +#include "qqmlextensionplugin.h" + +#ifdef BUILD_QDS_COMPONENTS + +Q_IMPORT_QML_PLUGIN(QtQuick_Studio_ComponentsPlugin) +Q_IMPORT_QML_PLUGIN(QtQuick_Studio_EffectsPlugin) +Q_IMPORT_QML_PLUGIN(QtQuick_Studio_ApplicationPlugin) +Q_IMPORT_QML_PLUGIN(FlowViewPlugin) +Q_IMPORT_QML_PLUGIN(QtQuick_Studio_LogicHelperPlugin) +Q_IMPORT_QML_PLUGIN(QtQuick_Studio_MultiTextPlugin) +Q_IMPORT_QML_PLUGIN(QtQuick_Studio_EventSimulatorPlugin) +Q_IMPORT_QML_PLUGIN(QtQuick_Studio_EventSystemPlugin) + +#endif diff --git a/src/plugins/qmlprojectmanager/cmakegen/gencmakemodule.tpl b/src/plugins/qmlprojectmanager/cmakegen/gencmakemodule.tpl new file mode 100644 index 0000000000..7c4b38b043 --- /dev/null +++ b/src/plugins/qmlprojectmanager/cmakegen/gencmakemodule.tpl @@ -0,0 +1,14 @@ +### This file is automatically generated by Qt Design Studio. +### Do not change + +%1 + +qt_add_library(%2 STATIC) +qt6_add_qml_module(%2 + URI "%3" + VERSION 1.0 + RESOURCE_PREFIX "/qt/qml" +%4 +) + +%5 diff --git a/src/plugins/qmlprojectmanager/cmakegen/gencmakeroot.tpl b/src/plugins/qmlprojectmanager/cmakegen/gencmakeroot.tpl new file mode 100644 index 0000000000..0b8553d195 --- /dev/null +++ b/src/plugins/qmlprojectmanager/cmakegen/gencmakeroot.tpl @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.21.1) + +option(LINK_INSIGHT "Link Qt Insight Tracker library" ON) +option(BUILD_QDS_COMPONENTS "Build design studio components" ON) + +project(%1 LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/qml) +set(QML_IMPORT_PATH ${QT_QML_OUTPUT_DIRECTORY} + CACHE STRING "Import paths for Qt Creator's code model" + FORCE +) + +find_package(Qt6 6.2 REQUIRED COMPONENTS Core Gui Qml Quick) + +if (Qt6_VERSION VERSION_GREATER_EQUAL 6.3) + qt_standard_project_setup() +endif() + +qt_add_executable(%1 %2) + +qt_add_resources(%1 "configuration" + PREFIX "/" +%3 +) + +target_link_libraries(%1 PRIVATE + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Quick + Qt${QT_VERSION_MAJOR}::Qml +) + +if (BUILD_QDS_COMPONENTS) + include(${CMAKE_CURRENT_SOURCE_DIR}/qmlcomponents OPTIONAL) +endif() + +include(${CMAKE_CURRENT_SOURCE_DIR}/qmlmodules) + +if (LINK_INSIGHT) + include(${CMAKE_CURRENT_SOURCE_DIR}/insight OPTIONAL) +endif () + +include(GNUInstallDirs) +install(TARGETS %1 + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.jsontoqml b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.jsontoqml index 2a67b12d50..90c980cda1 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.jsontoqml +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.jsontoqml @@ -7,6 +7,7 @@ Project { mainFile: "content/App.qml" mainUiFile: "content/Screen01.ui.qml" targetDirectory: "/opt/UntitledProject13" + enableCMakeGeneration: false widgetApp: true importPaths: [ "imports","asset_imports" ] diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.qmlproject b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.qmlproject index 260938164a..cb0ca5f9e1 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.qmlproject +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.qmlproject @@ -90,6 +90,8 @@ Project { /* Required for deployment */ targetDirectory: "/opt/UntitledProject13" + enableCMakeGeneration: false + qdsVersion: "4.0" quickVersion: "6.2" diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.qmltojson b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.qmltojson index c4475af39c..815e9a0cc5 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.qmltojson +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-1/testfile.qmltojson @@ -1,5 +1,6 @@ { "deployment": { + "enableCMakeGeneration": false, "targetDirectory": "/opt/UntitledProject13" }, "environment": { diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.jsontoqml b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.jsontoqml index 5878bdafc7..16617e015d 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.jsontoqml +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.jsontoqml @@ -6,6 +6,7 @@ import QmlProject Project { mainFile: "fileSelectors.qml" targetDirectory: "/opt/fileSelectors" + enableCMakeGeneration: false widgetApp: false importPaths: [ "imports" ] diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.qmlproject b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.qmlproject index 3ceeda651a..29bc108e68 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.qmlproject +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.qmlproject @@ -44,4 +44,6 @@ Project { /* Required for deployment */ targetDirectory: "/opt/fileSelectors" + + enableCMakeGeneration: false } diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.qmltojson b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.qmltojson index a26e0fc160..2231f36ff2 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.qmltojson +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-2/testfile.qmltojson @@ -1,5 +1,6 @@ { "deployment": { + "enableCMakeGeneration": false, "targetDirectory": "/opt/fileSelectors" }, "environment": { diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.jsontoqml b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.jsontoqml index 2a67b12d50..90c980cda1 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.jsontoqml +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.jsontoqml @@ -7,6 +7,7 @@ Project { mainFile: "content/App.qml" mainUiFile: "content/Screen01.ui.qml" targetDirectory: "/opt/UntitledProject13" + enableCMakeGeneration: false widgetApp: true importPaths: [ "imports","asset_imports" ] diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.qmlproject b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.qmlproject index 1ec43b95d5..faf115d609 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.qmlproject +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.qmlproject @@ -90,6 +90,8 @@ Project { /* Required for deployment */ QDS.targetDirectory: "/opt/UntitledProject13" + QDS.enableCMakeGeneration: false + QDS.qdsVersion: "4.0" QDS.quickVersion: "6.2" diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.qmltojson b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.qmltojson index c4475af39c..815e9a0cc5 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.qmltojson +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-3/testfile.qmltojson @@ -1,5 +1,6 @@ { "deployment": { + "enableCMakeGeneration": false, "targetDirectory": "/opt/UntitledProject13" }, "environment": { diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.jsontoqml b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.jsontoqml index 99e4f60bb3..04911c40f8 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.jsontoqml +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-1/testfile.jsontoqml @@ -6,6 +6,7 @@ import QmlProject Project { mainFile: "Main.qml" targetDirectory: "/opt/UntitledProject13" + enableCMakeGeneration: false widgetApp: true importPaths: [ "imports","asset_imports" ] diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-2/testfile.jsontoqml b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-2/testfile.jsontoqml index 2e73146cda..ad0102201f 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-2/testfile.jsontoqml +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/converter/test-set-mcu-2/testfile.jsontoqml @@ -4,6 +4,7 @@ import QmlProject Project { + enableCMakeGeneration: false widgetApp: false qt6Project: false diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/empty.qmlproject b/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/empty.qmlproject index 66adaaa7d9..bcf8995b7a 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/empty.qmlproject +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/empty.qmlproject @@ -11,6 +11,7 @@ Project { importPaths: [ ] targetDirectory: "" + enableCMakeGeneration: false fileSelectors: [ ] qdsVersion: "" diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/with_qds_prefix.qmlproject b/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/with_qds_prefix.qmlproject index a8b5b459d6..6422179e66 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/with_qds_prefix.qmlproject +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/with_qds_prefix.qmlproject @@ -11,6 +11,7 @@ Project { QDS.importPaths: [ "imports", "asset_imports" ] QDS.targetDirectory: "/opt/targetDirectory" + QDS.enableCMakeGeneration: true QDS.fileSelectors: [ "WXGA", "darkTheme", "ShowIndicator"] QDS.qdsVersion: "3.9" diff --git a/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/without_qds_prefix.qmlproject b/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/without_qds_prefix.qmlproject index 6bee599955..77b5556893 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/without_qds_prefix.qmlproject +++ b/tests/unit/tests/unittests/qmlprojectmanager/data/getter-setter/without_qds_prefix.qmlproject @@ -11,6 +11,7 @@ Project { importPaths: [ "imports", "asset_imports" ] targetDirectory: "/opt/targetDirectory" + enableCMakeGeneration: true fileSelectors: [ "WXGA", "darkTheme", "ShowIndicator"] qdsVersion: "3.9" diff --git a/tests/unit/tests/unittests/qmlprojectmanager/projectitem-test.cpp b/tests/unit/tests/unittests/qmlprojectmanager/projectitem-test.cpp index 49caf96074..1e1d7d1189 100644 --- a/tests/unit/tests/unittests/qmlprojectmanager/projectitem-test.cpp +++ b/tests/unit/tests/unittests/qmlprojectmanager/projectitem-test.cpp @@ -121,6 +121,13 @@ TEST_F(QmlProjectItem, get_with_qds_prefix_tar_get_with_qds_prefix_directory) ASSERT_THAT(targetDirectory, Eq("/opt/targetDirectory")); } +TEST_F(QmlProjectItem, get_with_qds_prefix_enable_cmake_generation) +{ + auto enable = projectItemWithQdsPrefix->enableCMakeGeneration(); + + ASSERT_TRUE(enable); +} + TEST_F(QmlProjectItem, get_with_qds_prefix_import_paths) { auto importPaths = projectItemWithQdsPrefix->importPaths(); @@ -266,6 +273,13 @@ TEST_F(QmlProjectItem, get_without_qds_prefix_tar_get_without_qds_prefix_directo ASSERT_THAT(targetDirectory, Eq("/opt/targetDirectory")); } +TEST_F(QmlProjectItem, get_without_qds_prefix_enable_cmake_generation) +{ + auto enable = projectItemWithoutQdsPrefix->enableCMakeGeneration(); + + ASSERT_TRUE(enable); +} + TEST_F(QmlProjectItem, get_without_qds_prefix_import_paths) { auto importPaths = projectItemWithoutQdsPrefix->importPaths(); @@ -413,6 +427,13 @@ TEST_F(QmlProjectItem, get_empty_tar_get_empty_directory) ASSERT_THAT(targetDirectory, IsEmpty()); } +TEST_F(QmlProjectItem, get_empty_enable_cmake_generation) +{ + auto enable = projectItemEmpty->enableCMakeGeneration(); + + ASSERT_FALSE(enable); +} + TEST_F(QmlProjectItem, get_empty_import_paths) { auto importPaths = projectItemEmpty->importPaths(); @@ -677,6 +698,13 @@ TEST_F(QmlProjectItem, set_design_studio_version) ASSERT_EQ(projectItemSetters->versionDesignStudio(), "6"); } +TEST_F(QmlProjectItem, set_enable_cmake_generation) +{ + projectItemSetters->setEnableCMakeGeneration(true); + + ASSERT_EQ(projectItemSetters->enableCMakeGeneration(), true); +} + // TODO: We should move these 2 tests into the integration tests TEST_F(QmlProjectItem, test_file_filters) { |