// 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 "filetypes.h" #include "../qmlproject.h" #include "../qmlprojectmanagertr.h" #include #include #include #include #include #include #include #include #include using namespace Utils; namespace QmlProjectManager { namespace QmlProjectExporter { void CMakeGenerator::createMenuAction(QObject *parent) { QAction *action = FileGenerator::createMenuAction( parent, Tr::tr("Enable CMake Generator"), "QmlProject.EnableCMakeGeneration"); QObject::connect( ProjectExplorer::ProjectManager::instance(), &ProjectExplorer::ProjectManager::startupProjectChanged, [action]() { if (auto buildSystem = QmlBuildSystem::getStartupBuildSystem()) { action->setEnabled(!buildSystem->qtForMCUs()); action->setChecked(buildSystem->enableCMakeGeneration()); } } ); QObject::connect(action, &QAction::toggled, [](bool checked) { if (auto buildSystem = QmlBuildSystem::getStartupBuildSystem()) buildSystem->setEnableCMakeGeneration(checked); }); } CMakeGenerator::CMakeGenerator(QmlBuildSystem *bs) : FileGenerator(bs) , m_root(std::make_shared()) {} void CMakeGenerator::updateMenuAction() { FileGenerator::updateMenuAction( "QmlProject.EnableCMakeGeneration", [this](){ return buildSystem()->enableCMakeGeneration(); }); } void CMakeGenerator::updateProject(QmlProject *project) { if (!isEnabled()) return; if (!isActive()) return; createWriter(); if (!m_writer) return; m_root = std::make_shared(); m_root->type = Node::Type::App; m_root->uri = QString("Main"); m_root->name = QString("Main"); m_root->dir = project->rootProjectDirectory(); m_projectName = project->displayName(); ProjectExplorer::ProjectNode *rootProjectNode = project->rootProjectNode(); parseNodeTree(m_root, rootProjectNode); parseSourceTree(); createCMakeFiles(m_root); createSourceFiles(); compareWithFileSystem(m_root); } QString CMakeGenerator::projectName() const { return m_projectName; } Utils::FilePath CMakeGenerator::projectDir() const { if (!m_root) return {}; return m_root->dir; } bool CMakeGenerator::findFile(const Utils::FilePath& file) const { return findFile(m_root, file); } bool CMakeGenerator::isRootNode(const NodePtr &node) const { return node->name == "Main"; } bool CMakeGenerator::hasChildModule(const NodePtr &node) const { for (const NodePtr &child : node->subdirs) { if (child->type == Node::Type::Module) return true; if (hasChildModule(child)) return true; } return false; } void CMakeGenerator::updateModifiedFile(const QString &fileString) { if (!isEnabled() || !m_writer) return; const Utils::FilePath path = Utils::FilePath::fromString(fileString); if (path.fileName() != "qmldir") return; if (path.fileSize() == 0) { if (auto node = findNode(m_root, path.parentDir())) removeFile(node, path); } else if (auto node = findOrCreateNode(m_root, path.parentDir())) { insertFile(node, path); } createCMakeFiles(m_root); createSourceFiles(); } void CMakeGenerator::update(const QSet &added, const QSet &removed) { if (!isEnabled() || !m_writer) return; std::set dirtyModules; for (const QString &add : added) { const Utils::FilePath path = Utils::FilePath::fromString(add); if (ignore(path.parentDir())) continue; if (auto node = findOrCreateNode(m_root, path.parentDir())) { insertFile(node, path); if (auto module = findModuleFor(node)) dirtyModules.insert(module); } else { QString text("Failed to find Folder for file"); logIssue(ProjectExplorer::Task::Error, text, path); } } for (const QString &remove : removed) { const Utils::FilePath path = Utils::FilePath::fromString(remove); if (auto node = findNode(m_root, path.parentDir())) { removeFile(node, path); if (auto module = findModuleFor(node)) dirtyModules.insert(module); } } createCMakeFiles(m_root); createSourceFiles(); } bool CMakeGenerator::ignore(const Utils::FilePath &path) const { if (path.isFile()) { static const QStringList suffixes = { "hints" }; return suffixes.contains(path.suffix(), Qt::CaseInsensitive); } else if (path.isDir()) { if (!m_root->dir.exists()) return true; static const QStringList dirNames = { DEPENDENCIES_DIR }; if (dirNames.contains(path.fileName())) return true; static const QStringList fileNames = {COMPONENTS_IGNORE_FILE, "CMakeCache.txt", "build.ninja"}; Utils::FilePath dir = path; while (dir.isChildOf(m_root->dir)) { for (const QString& fileName : fileNames) { Utils::FilePath checkFile = dir.pathAppended(fileName); if (checkFile.exists()) return true; } dir = dir.parentDir(); } } return false; } bool CMakeGenerator::checkUri(const QString& uri, const Utils::FilePath &path) const { QTC_ASSERT(buildSystem(), return false); Utils::FilePath relative = path.relativeChildPath(m_root->dir); QList pathComponents = relative.pathView().split('/', Qt::SkipEmptyParts); for (const auto& import : buildSystem()->allImports()) { Utils::FilePath importPath = Utils::FilePath::fromUserInput(import); for (const auto& component : importPath.pathView().split('/', Qt::SkipEmptyParts)) { if (component == pathComponents.first()) pathComponents.pop_front(); } } const QStringList uriComponents = uri.split('.', Qt::SkipEmptyParts); if (pathComponents.size() == uriComponents.size()) { for (qsizetype i=0; iwriteRootCMakeFile(node); if (node->type == Node::Type::Module || (hasChildModule(node))) m_writer->writeModuleCMakeFile(node, m_root); for (const NodePtr &n : node->subdirs) createCMakeFiles(n); } void CMakeGenerator::createSourceFiles() const { QTC_ASSERT(m_writer, return); NodePtr sourceNode = {}; for (const NodePtr &child : m_root->subdirs) { if (child->name == m_writer->sourceDirName()) sourceNode = child; } if (sourceNode) m_writer->writeSourceFiles(sourceNode, m_root); } bool CMakeGenerator::isMockModule(const NodePtr &node) const { QTC_ASSERT(buildSystem(), return false); Utils::FilePath dir = node->dir.parentDir(); QString mockDir = dir.relativeChildPath(m_root->dir).path(); for (const QString &import : buildSystem()->mockImports()) { if (import == mockDir) return true; } return false; } bool CMakeGenerator::checkQmlDirLocation(const Utils::FilePath &filePath) const { QTC_ASSERT(m_root, return false); QTC_ASSERT(buildSystem(), return false); Utils::FilePath dirPath = filePath.parentDir().cleanPath(); Utils::FilePath rootPath = m_root->dir.cleanPath(); if (dirPath==rootPath) return false; for (const QString &path : buildSystem()->allImports()) { Utils::FilePath importPath = rootPath.pathAppended(path).cleanPath(); if (dirPath==importPath) return false; } return true; } void CMakeGenerator::readQmlDir(const Utils::FilePath &filePath, NodePtr &node) const { node->uri = ""; node->name = ""; node->singletons.clear(); if (!checkQmlDirLocation(filePath)) { QString text("Unexpected location for qmldir file."); logIssue(ProjectExplorer::Task::Warning, text, filePath); return; } if (isMockModule(node)) node->type = Node::Type::MockModule; else node->type = Node::Type::Module; QFile f(filePath.toUrlishString()); QTC_CHECK(f.open(QIODevice::ReadOnly)); QTextStream stream(&f); Utils::FilePath dir = filePath.parentDir(); static const QRegularExpression whitespaceRegex("\\s+"); while (!stream.atEnd()) { const QString line = stream.readLine(); const QStringList tokenizedLine = line.split(whitespaceRegex); 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(); if (!checkUri(node->uri, node->dir)) { QString text("Unexpected uri %1"); logIssue(ProjectExplorer::Task::Warning, text.arg(node->uri), node->dir); } } NodePtr CMakeGenerator::findModuleFor(const NodePtr &node) const { NodePtr current = node; while (current->parent) { if (current->type == Node::Type::Module) return current; current = current->parent; } return nullptr; } NodePtr CMakeGenerator::findNode(NodePtr &node, const Utils::FilePath &path) const { for (NodePtr &child : node->subdirs) { if (child->dir == path) return child; if (path.isChildOf(child->dir)) return findNode(child, path); } return nullptr; } 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; auto findSubDir = [](NodePtr &node, const Utils::FilePath &path) -> NodePtr { for (NodePtr child : node->subdirs) { if (child->dir == path) return child; } return nullptr; }; const Utils::FilePath relative = path.relativeChildPath(node->dir); const QList components = relative.pathView().split('/'); NodePtr lastNode = node; for (const auto &comp : components) { Utils::FilePath subPath = lastNode->dir.pathAppended(comp.toString()); if (NodePtr sub = findSubDir(lastNode, subPath)) { lastNode = sub; continue; } NodePtr newNode = std::make_shared(); newNode->parent = lastNode; newNode->name = comp.toString(); newNode->dir = subPath; lastNode->subdirs.push_back(newNode); lastNode = newNode; } return lastNode; } bool findFileWithGetter(const Utils::FilePath &file, const NodePtr &node, const FileGetter &getter) { for (const auto &f : getter(node)) { if (f == file) return true; } for (const auto &subdir : node->subdirs) { if (findFileWithGetter(file, subdir, getter)) return true; } return false; } bool CMakeGenerator::findFile(const NodePtr &node, const Utils::FilePath &file) const { if (isAssetFile(file)) { return findFileWithGetter(file, node, [](const NodePtr &n) { return n->assets; }); } else if (isQmlFile(file)) { if (findFileWithGetter(file, node, [](const NodePtr &n) { return n->files; })) return true; else if (findFileWithGetter(file, node, [](const NodePtr &n) { return n->singletons; })) return true; } return false; } void CMakeGenerator::insertFile(NodePtr &node, const FilePath &path) const { const Result<> valid = FileNameValidatingLineEdit::validateFileName(path.fileName(), false); if (!valid) { if (!isImageFile(path)) logIssue(ProjectExplorer::Task::Error, valid.error(), path); } if (path.fileName() == "qmldir") { readQmlDir(path, node); } else if (path.suffix() == "cpp") { node->sources.push_back(path); } else if (isQmlFile(path)) { node->files.push_back(path); } else if (isAssetFile(path)) { node->assets.push_back(path); } } void CMakeGenerator::removeFile(NodePtr &node, const Utils::FilePath &path) const { if (path.fileName() == "qmldir") { node->type = Node::Type::Folder; node->singletons.clear(); node->uri = ""; node->name = path.parentDir().fileName(); } else if (isQmlFile(path)) { auto iter = std::find(node->files.begin(), node->files.end(), path); if (iter != node->files.end()) node->files.erase(iter); } else if (isAssetFile(path)) { auto iter = std::find(node->assets.begin(), node->assets.end(), path); if (iter != node->assets.end()) node->assets.erase(iter); } } void CMakeGenerator::removeAmbiguousFiles(const Utils::FilePath &rootPath) const { const Utils::FilePath rootCMakeFile = rootPath.pathAppended("CMakeLists.txt"); rootCMakeFile.removeFile(); const Utils::FilePath sourceDirPath = rootPath.pathAppended("App"); if (sourceDirPath.exists()) { const Utils::FilePath appCMakeFile = sourceDirPath.pathAppended("CMakeLists.txt"); appCMakeFile.removeFile(); } } void CMakeGenerator::printModules(const NodePtr &node) const { if (node->type == Node::Type::Module) qDebug() << "Module: " << node->name; for (const auto &child : node->subdirs) printModules(child); } 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; }; QString typeString; switch (generatorNode->type) { case Node::Type::App: typeString = "Node::Type::App"; break; case Node::Type::Folder: typeString = "Node::Type::Folder"; break; case Node::Type::Module: typeString = "Node::Type::Module"; break; case Node::Type::MockModule: typeString = "Node::Type::MockModule"; break; case Node::Type::Library: typeString = "Node::Type::Library"; break; } qDebug() << addIndent(indent) << "GeneratorNode: " << generatorNode->name; qDebug() << addIndent(indent) << "type: " << typeString; qDebug() << addIndent(indent) << "directory: " << generatorNode->dir; qDebug() << addIndent(indent) << "files: " << generatorNode->files; qDebug() << addIndent(indent) << "singletons: " << generatorNode->singletons; qDebug() << addIndent(indent) << "assets: " << generatorNode->assets; 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()) { if (ignore(subFolderNode->filePath())) continue; NodePtr childGeneratorNode = std::make_shared(); childGeneratorNode->parent = generatorNode; childGeneratorNode->dir = subFolderNode->filePath(); childGeneratorNode->name = subFolderNode->displayName(); childGeneratorNode->uri = childGeneratorNode->name; parseNodeTree(childGeneratorNode, subFolderNode); generatorNode->subdirs.push_back(childGeneratorNode); } else if (auto *fileNode = childNode->asFileNode()) { insertFile(generatorNode, fileNode->filePath()); } } if (m_writer) m_writer->transformNode(generatorNode); } void CMakeGenerator::parseSourceTree() { QTC_ASSERT(m_writer, return); QString srcDirName = m_writer->sourceDirName(); if (srcDirName.isEmpty()) return; const Utils::FilePath srcDir = m_root->dir.pathAppended(srcDirName); QDirIterator it(srcDir.path(), {"*.cpp"}, QDir::Files, QDirIterator::Subdirectories); NodePtr srcNode = std::make_shared(); srcNode->parent = m_root; srcNode->type = Node::Type::App; srcNode->dir = srcDir; srcNode->uri = srcDir.baseName(); srcNode->name = srcNode->uri; while (it.hasNext()) { auto next = it.next(); srcNode->sources.push_back(Utils::FilePath::fromString(next)); } if (srcNode->sources.empty()) srcNode->sources.push_back(srcDir.pathAppended("main.cpp")); if (m_writer) m_writer->transformNode(srcNode); m_root->subdirs.push_back(srcNode); } void CMakeGenerator::compareWithFileSystem(const NodePtr &node) const { std::vector files; QDirIterator iter(node->dir.path(), QDir::Files, QDirIterator::Subdirectories); while (iter.hasNext()) { auto next = Utils::FilePath::fromString(iter.next()); if (ignore(next.parentDir())) continue; if (isAssetFile(next) && !findFile(next) && !ignore(next)) files.push_back(next); } const QString text("File is not part of the project"); for (const auto &file : files) logIssue(ProjectExplorer::Task::Warning, text, file); } void CMakeGenerator::createWriter() { auto writer = CMakeWriter::create(this); const QmlProject *project = qmlProject(); QTC_ASSERT(project, return ); const Utils::FilePath rootPath = project->projectDirectory(); const Utils::FilePath settingsFile = rootPath.pathAppended("CMakeLists.txt.shared"); if (!settingsFile.exists()) { const QString sharedTemplate = CMakeWriter::readTemplate(":/templates/cmake_shared"); const QString sharedContent = sharedTemplate.arg(writer->identifier()); CMakeWriter::writeFile(settingsFile, sharedContent); m_writer = writer; return; } Utils::PersistentSettingsReader reader; reader.load(settingsFile); auto store = reader.restoreValues(); auto writeSettings = [settingsFile, &store](int identifier) { store["CMake Generator"] = identifier; Utils::PersistentSettingsWriter settingsWriter(settingsFile, "QtCreatorProject"); if (const Result<> res = settingsWriter.save(store); !res) { const QString text = QString("Failed to write settings file: %1").arg(res.error()); logIssue(ProjectExplorer::Task::Error, text, settingsFile); } }; QVariant idVariant = store["CMake Generator"]; if (!idVariant.isValid()) { writeSettings(writer->identifier()); m_writer = writer; return; } int identifier = writer->identifier(); int currentId = idVariant.toInt(); if (currentId == identifier) { m_writer = writer; return; } QMessageBox msgBox; msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); msgBox.setDefaultButton(QMessageBox::Ok); msgBox.setText("The CmakeGenerator Has Changed"); msgBox.setInformativeText( "This operation will delete build files that may contain" " user-made changes. Are you sure you want to proceed?"); int ret = msgBox.exec(); if (ret == QMessageBox::Cancel) { m_writer = CMakeWriter::createAndRecover(currentId, this); return; } removeAmbiguousFiles( rootPath ); writeSettings(writer->identifier()); m_writer = writer; } } // namespace QmlProjectExporter } // namespace QmlProjectManager