// Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "generatecmakelists.h" #include "generatecmakelistsconstants.h" #include "cmakegeneratordialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Utils; using namespace QmlProjectManager::GenerateCmake::Constants; namespace QmlProjectManager { namespace GenerateCmake { bool operator==(const GeneratableFile &left, const GeneratableFile &right) { return (left.filePath == right.filePath && left.content == right.content); } enum ProjectDirectoryError { NoError = 0, MissingContentDir = 1<<1, MissingImportDir = 1<<2, MissingAssetDir = 1<<3, MissingAssetImportDir = 1<<4, MissingCppDir = 1<<5, MissingMainCMake = 1<<6, MissingMainQml = 1<<7, MissingAppMainQml = 1<<8, MissingQmlModules = 1<<9, MissingMainCpp = 1<<10, MissingMainCppHeader = 1<<11, MissingEnvHeader = 1<<12 }; const QString MENU_ITEM_GENERATE = QCoreApplication::translate("QmlDesigner::GenerateCmake", "Generate CMake Build Files..."); void generateMenuEntry(QObject *parent) { Core::ActionContainer *menu = Core::ActionManager::actionContainer(Core::Constants::M_FILE); Core::ActionContainer *exportMenu = Core::ActionManager::createMenu( QmlProjectManager::Constants::EXPORT_MENU); exportMenu->menu()->setTitle( QCoreApplication::translate("QmlDesigner::GenerateCmake", "Export Project")); menu->addMenu(exportMenu, Core::Constants::G_FILE_EXPORT); exportMenu->appendGroup(QmlProjectManager::Constants::G_EXPORT_GENERATE); exportMenu->appendGroup(QmlProjectManager::Constants::G_EXPORT_CONVERT); exportMenu->addSeparator(QmlProjectManager::Constants::G_EXPORT_CONVERT); auto action = new QAction(MENU_ITEM_GENERATE, parent); QObject::connect(action, &QAction::triggered, GenerateCmake::onGenerateCmakeLists); Core::Command *cmd = Core::ActionManager::registerAction(action, "QmlProject.CreateCMakeLists"); exportMenu->addAction(cmd, QmlProjectManager::Constants::G_EXPORT_GENERATE); action->setEnabled(false); QObject::connect(ProjectExplorer::SessionManager::instance(), &ProjectExplorer::SessionManager::startupProjectChanged, [action]() { auto qmlProject = qobject_cast( ProjectExplorer::SessionManager::startupProject()); action->setEnabled(qmlProject != nullptr); }); } void onGenerateCmakeLists() { FilePath rootDir = ProjectExplorer::SessionManager::startupProject()->projectDirectory(); int projectDirErrors = isProjectCorrectlyFormed(rootDir); if (projectDirErrors != NoError) { showProjectDirErrorDialog(projectDirErrors); if (isErrorFatal(projectDirErrors)) return; } CmakeFileGenerator cmakeGen; cmakeGen.prepare(rootDir); FilePaths allFiles; for (const GeneratableFile &file: cmakeGen.fileQueue().queuedFiles()) allFiles.append(file.filePath); CmakeGeneratorDialog dialog(rootDir, allFiles); if (dialog.exec()) { FilePaths confirmedFiles = dialog.getFilePaths(); cmakeGen.filterFileQueue(confirmedFiles); cmakeGen.execute(); } } bool isErrorFatal(int error) { if (error & MissingContentDir || error & MissingImportDir || error & MissingCppDir || error & MissingAppMainQml) return true; return false; } int isProjectCorrectlyFormed(const FilePath &rootDir) { int errors = NoError; if (!rootDir.pathAppended(DIRNAME_CONTENT).exists()) errors |= MissingContentDir; if (!rootDir.pathAppended(DIRNAME_CONTENT).pathAppended(FILENAME_APPMAINQML).exists()) errors |= MissingAppMainQml; if (!rootDir.pathAppended(DIRNAME_IMPORT).exists()) errors |= MissingImportDir; if (!rootDir.pathAppended(DIRNAME_ASSETIMPORT).exists()) errors |= MissingAssetImportDir; if (!rootDir.pathAppended(DIRNAME_CPP).exists()) errors |= MissingCppDir; if (!rootDir.pathAppended(DIRNAME_CPP).pathAppended(FILENAME_MAINCPP).exists()) errors |= MissingMainCpp; if (!rootDir.pathAppended(DIRNAME_CPP).pathAppended(FILENAME_MAINCPP_HEADER).exists()) errors |= MissingMainCppHeader; if (!rootDir.pathAppended(DIRNAME_CPP).pathAppended(FILENAME_ENV_HEADER).exists()) errors |= MissingEnvHeader; if (!rootDir.pathAppended(FILENAME_CMAKELISTS).exists()) errors |= MissingMainCMake; if (!rootDir.pathAppended(FILENAME_MODULES).exists()) errors |= MissingQmlModules; if (!rootDir.pathAppended(FILENAME_MAINQML).exists()) errors |= MissingMainQml; return errors; } const QString WARNING_MISSING_STRUCTURE_FATAL = QCoreApplication::translate("QmlDesigner::GenerateCmake", "The project is not properly structured for automatically generating CMake files.\n\nAborting process.\n\nThe following files or directories are missing:\n\n%1"); //const QString WARNING_MISSING_STRUCTURE_NONFATAL = QCoreApplication::translate("QmlDesigner::GenerateCmake", // "The project is not properly structured for automatically generating CMake files.\n\nThe following files or directories are missing and may be created:\n\n%1"); const QString WARNING_TITLE_FATAL = QCoreApplication::translate("QmlDesigner::GenerateCmake", "Cannot Generate CMake Files"); //const QString WARNING_TITLE_NONFATAL = QCoreApplication::translate("QmlDesigner::GenerateCmake", // "Problems with Generating CMake Files"); void showProjectDirErrorDialog(int error) { bool isFatal = isErrorFatal(error); if (isFatal) { QString fatalList; if (error & MissingContentDir) fatalList.append(QString(DIRNAME_CONTENT) + "\n"); if (error & MissingAppMainQml) fatalList.append(QString(DIRNAME_CONTENT) + QDir::separator() + QString(FILENAME_APPMAINQML) + "\n"); if (error & MissingCppDir) fatalList.append(QString(DIRNAME_CPP) + "\n"); if (error & MissingImportDir) fatalList.append(QString(DIRNAME_IMPORT) + "\n"); QMessageBox::critical(nullptr, WARNING_TITLE_FATAL, WARNING_MISSING_STRUCTURE_FATAL.arg(fatalList)); } } bool FileQueue::queueFile(const FilePath &filePath, const QString &fileContent) { GeneratableFile file; file.filePath = filePath; file.content = fileContent; file.fileExists = filePath.exists(); m_queuedFiles.append(file); return true; } const QVector FileQueue::queuedFiles() const { return m_queuedFiles; } bool FileQueue::writeQueuedFiles() { for (GeneratableFile &file: m_queuedFiles) if (!writeFile(file)) return false; return true; } bool FileQueue::writeFile(const GeneratableFile &file) { QFile fileHandle(file.filePath.toString()); fileHandle.open(QIODevice::WriteOnly); QTextStream stream(&fileHandle); stream << file.content; fileHandle.close(); return true; } void FileQueue::filterFiles(const Utils::FilePaths keepFiles) { QtConcurrent::blockingFilter(m_queuedFiles, [keepFiles](const GeneratableFile &qf) { return keepFiles.contains(qf.filePath); }); } QString readTemplate(const QString &templatePath) { QFile templatefile(templatePath); templatefile.open(QIODevice::ReadOnly); QTextStream stream(&templatefile); QString content = stream.readAll(); templatefile.close(); return content; } const QString projectEnvironmentVariable(const QString &key) { QString value = {}; auto *target = ProjectExplorer::SessionManager::startupProject()->activeTarget(); if (target && target->buildSystem()) { auto buildSystem = qobject_cast(target->buildSystem()); if (buildSystem) { auto envItems = buildSystem->environment(); auto confEnv = std::find_if(envItems.begin(), envItems.end(), [key](NameValueItem &item){return item.name == key;}); if (confEnv != envItems.end()) value = confEnv->value; } } return value; } const QDir::Filters FILES_ONLY = QDir::Files; const QDir::Filters DIRS_ONLY = QDir::Dirs|QDir::Readable|QDir::NoDotAndDotDot; const char MAIN_CMAKEFILE_TEMPLATE_PATH[] = ":/boilerplatetemplates/qmlprojectmaincmakelists.tpl"; const char QMLMODULES_FILE_TEMPLATE_PATH[] = ":/boilerplatetemplates/qmlprojectmodules.tpl"; bool CmakeFileGenerator::prepare(const FilePath &rootDir, bool checkFileBelongs) { m_checkFileIsInProject = checkFileBelongs; FilePath contentDir = rootDir.pathAppended(DIRNAME_CONTENT); FilePath importDir = rootDir.pathAppended(DIRNAME_IMPORT); FilePath assetImportDir = rootDir.pathAppended(DIRNAME_ASSETIMPORT); generateModuleCmake(contentDir); generateImportCmake(importDir); generateImportCmake(assetImportDir); generateMainCmake(rootDir); generateEntryPointFiles(rootDir); return true; } const FileQueue CmakeFileGenerator::fileQueue() const { return m_fileQueue; } void CmakeFileGenerator::filterFileQueue(const Utils::FilePaths &keepFiles) { m_fileQueue.filterFiles(keepFiles); } bool CmakeFileGenerator::execute() { return m_fileQueue.writeQueuedFiles(); } 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"; void CmakeFileGenerator::generateMainCmake(const FilePath &rootDir) { //TODO startupProject() may be a terrible way to try to get "current project". It's not necessarily the same thing at all. QString projectName = ProjectExplorer::SessionManager::startupProject()->displayName(); QString appName = projectName + "App"; QString fileSection = ""; const QString qtcontrolsConfFile = GenerateCmake::projectEnvironmentVariable(ENV_VARIABLE_CONTROLCONF); if (!qtcontrolsConfFile.isEmpty()) fileSection = QString(" FILES\n %1").arg(qtcontrolsConfFile); QString cmakeFileContent = GenerateCmake::readTemplate(MAIN_CMAKEFILE_TEMPLATE_PATH) .arg(appName) .arg(fileSection); queueCmakeFile(rootDir, cmakeFileContent); QString subdirIncludes; subdirIncludes.append(QString(ADD_SUBDIR).arg(DIRNAME_CONTENT)); subdirIncludes.append(QString(ADD_SUBDIR).arg(DIRNAME_IMPORT)); if (rootDir.pathAppended(DIRNAME_ASSETIMPORT).exists()) subdirIncludes.append(QString(ADD_SUBDIR).arg(DIRNAME_ASSETIMPORT)); QString modulesAsPlugins; for (const QString &moduleName : m_moduleNames) modulesAsPlugins.append(" " + moduleName + "plugin\n"); QString moduleFileContent = GenerateCmake::readTemplate(QMLMODULES_FILE_TEMPLATE_PATH) .arg(appName) .arg(subdirIncludes) .arg(modulesAsPlugins); m_fileQueue.queueFile(rootDir.pathAppended(FILENAME_MODULES), moduleFileContent); } void CmakeFileGenerator::generateImportCmake(const FilePath &dir, const QString &modulePrefix) { if (!dir.exists()) return; QString fileContent; fileContent.append(DO_NOT_EDIT_FILE_COMMENT); FilePaths subDirs = dir.dirEntries(DIRS_ONLY); for (FilePath &subDir : subDirs) { if (isDirBlacklisted(subDir)) continue; if (getDirectoryTreeQmls(subDir).isEmpty() && getDirectoryTreeResources(subDir).isEmpty()) continue; fileContent.append(QString(ADD_SUBDIR).arg(subDir.fileName())); QString prefix = modulePrefix.isEmpty() ? modulePrefix % subDir.fileName() : QString(modulePrefix + '.') + subDir.fileName(); if (getDirectoryQmls(subDir).isEmpty()) { generateImportCmake(subDir, prefix); } else { generateModuleCmake(subDir, prefix); } } queueCmakeFile(dir, fileContent); } const char MODULEFILE_PROPERTY_SINGLETON[] = "QT_QML_SINGLETON_TYPE"; const char MODULEFILE_PROPERTY_SET[] = "set_source_files_properties(%1\n PROPERTIES\n %2 %3\n)\n\n"; const char MODULEFILE_TEMPLATE_PATH[] = ":/boilerplatetemplates/qmlprojectmodulecmakelists.tpl"; void CmakeFileGenerator::generateModuleCmake(const FilePath &dir, const QString &uri) { QString fileTemplate = GenerateCmake::readTemplate(MODULEFILE_TEMPLATE_PATH); QString singletonContent; FilePaths qmldirFileList = dir.dirEntries({QStringList(FILENAME_QMLDIR), FILES_ONLY}); if (!qmldirFileList.isEmpty()) { QStringList singletons = getSingletonsFromQmldirFile(qmldirFileList.first()); for (QString &singleton : singletons) { singletonContent.append(QString(MODULEFILE_PROPERTY_SET).arg(singleton).arg(MODULEFILE_PROPERTY_SINGLETON).arg("true")); } } QStringList qmlFileList = getDirectoryTreeQmls(dir); QString qmlFiles; for (QString &qmlFile : qmlFileList) qmlFiles.append(QString(" %1\n").arg(qmlFile)); QStringList resourceFileList = getDirectoryTreeResources(dir); QString resourceFiles; for (QString &resourceFile : resourceFileList) resourceFiles.append(QString(" %1\n").arg(resourceFile)); QString moduleContent; if (!qmlFiles.isEmpty()) { moduleContent.append(QString(" QML_FILES\n%1").arg(qmlFiles)); } if (!resourceFiles.isEmpty()) { moduleContent.append(QString(" RESOURCES\n%1").arg(resourceFiles)); } QString moduleUri = uri.isEmpty() ? dir.fileName() : uri; QString moduleName = QString(moduleUri).replace('.', '_'); m_moduleNames.append(moduleName); QString fileContent; fileContent.append(fileTemplate.arg(singletonContent, moduleName, moduleUri, moduleContent)); queueCmakeFile(dir, fileContent); } QStringList CmakeFileGenerator::getSingletonsFromQmldirFile(const FilePath &filePath) { QStringList singletons; QFile f(filePath.toString()); f.open(QIODevice::ReadOnly); QTextStream stream(&f); while (!stream.atEnd()) { QString line = stream.readLine(); if (line.startsWith("singleton", Qt::CaseInsensitive)) { QStringList tokenizedLine = line.split(QRegularExpression("\\s+")); QString fileName = tokenizedLine.last(); if (fileName.endsWith(".qml", Qt::CaseInsensitive)) { singletons.append(fileName); } } } f.close(); return singletons; } QStringList CmakeFileGenerator::getDirectoryQmls(const FilePath &dir) { QStringList moduleFiles; const QStringList qmlFilesOnly(FILENAME_FILTER_QML); FilePaths allFiles = dir.dirEntries({qmlFilesOnly, FILES_ONLY}); for (FilePath &file : allFiles) { if (includeFile(file)) { moduleFiles.append(file.fileName()); } } return moduleFiles; } QStringList CmakeFileGenerator::getDirectoryResources(const FilePath &dir) { QStringList moduleFiles; FilePaths allFiles = dir.dirEntries(FILES_ONLY); for (FilePath &file : allFiles) { if (!file.fileName().endsWith(".qml", Qt::CaseInsensitive) && includeFile(file)) { moduleFiles.append(file.fileName()); } } return moduleFiles; } QStringList CmakeFileGenerator::getDirectoryTreeQmls(const FilePath &dir) { QStringList qmlFileList; qmlFileList.append(getDirectoryQmls(dir)); FilePaths subDirsList = dir.dirEntries(DIRS_ONLY); for (FilePath &subDir : subDirsList) { if (isDirBlacklisted(subDir)) continue; QStringList subDirQmlFiles = getDirectoryTreeQmls(subDir); for (QString &qmlFile : subDirQmlFiles) { qmlFileList.append(subDir.fileName().append('/').append(qmlFile)); } } return qmlFileList; } QStringList CmakeFileGenerator::getDirectoryTreeResources(const FilePath &dir) { QStringList resourceFileList; resourceFileList.append(getDirectoryResources(dir)); FilePaths subDirsList = dir.dirEntries(DIRS_ONLY); for (FilePath &subDir : subDirsList) { if (isDirBlacklisted(subDir)) continue; QStringList subDirResources = getDirectoryTreeResources(subDir); for (QString &resource : subDirResources) { resourceFileList.append(subDir.fileName().append('/').append(resource)); } } return resourceFileList; } void CmakeFileGenerator::queueCmakeFile(const FilePath &dir, const QString &content) { FilePath filePath = dir.pathAppended(FILENAME_CMAKELISTS); m_fileQueue.queueFile(filePath, content); } bool CmakeFileGenerator::isFileBlacklisted(const QString &fileName) { return (!fileName.compare(FILENAME_QMLDIR) || !fileName.compare(FILENAME_CMAKELISTS)); } bool CmakeFileGenerator::isDirBlacklisted(const FilePath &dir) { return (!dir.fileName().compare(DIRNAME_DESIGNER)); } bool CmakeFileGenerator::includeFile(const FilePath &filePath) { if (m_checkFileIsInProject) { ProjectExplorer::Project *project = ProjectExplorer::SessionManager::startupProject(); if (!project->isKnownFile(filePath)) return false; } return !isFileBlacklisted(filePath.fileName()); } bool CmakeFileGenerator::generateEntryPointFiles(const FilePath &dir) { const QString qtcontrolsConf = GenerateCmake::projectEnvironmentVariable(ENV_VARIABLE_CONTROLCONF); if (!qtcontrolsConf.isEmpty()) m_resourceFileLocations.append(qtcontrolsConf); bool cppOk = generateMainCpp(dir); bool qmlOk = generateMainQml(dir); return cppOk && qmlOk; } const char MAIN_CPPFILE_TEMPLATE_PATH[] = ":/boilerplatetemplates/qmlprojectmaincpp.tpl"; const char MAIN_CPPFILE_HEADER_TEMPLATE_PATH[] = ":/boilerplatetemplates/qmlprojectmaincppheader.tpl"; const char MAIN_CPPFILE_HEADER_PLUGIN_LINE[] = "Q_IMPORT_QML_PLUGIN(%1)\n"; const char ENV_HEADER_TEMPLATE_PATH[] = ":/boilerplatetemplates/qmlprojectenvheader.tpl"; const char ENV_HEADER_VARIABLE_LINE[] = " qputenv(\"%1\", \"%2\");\n"; bool CmakeFileGenerator::generateMainCpp(const FilePath &dir) { FilePath srcDir = dir.pathAppended(DIRNAME_CPP); QString cppContent = GenerateCmake::readTemplate(MAIN_CPPFILE_TEMPLATE_PATH); FilePath cppFilePath = srcDir.pathAppended(FILENAME_MAINCPP); bool cppOk = m_fileQueue.queueFile(cppFilePath, cppContent); QString modulesAsPlugins; for (const QString &moduleName : m_moduleNames) modulesAsPlugins.append( QString(MAIN_CPPFILE_HEADER_PLUGIN_LINE).arg(moduleName + "Plugin")); QString headerContent = GenerateCmake::readTemplate(MAIN_CPPFILE_HEADER_TEMPLATE_PATH) .arg(modulesAsPlugins); FilePath headerFilePath = srcDir.pathAppended(FILENAME_MAINCPP_HEADER); bool pluginHeaderOk = m_fileQueue.queueFile(headerFilePath, headerContent); bool envHeaderOk = true; QString environment; auto *target = ProjectExplorer::SessionManager::startupProject()->activeTarget(); if (target && target->buildSystem()) { auto buildSystem = qobject_cast(target->buildSystem()); if (buildSystem) { for (EnvironmentItem &envItem : buildSystem->environment()) { QString key = envItem.name; QString value = envItem.value; if (isFileResource(value)) value.prepend(":/"); environment.append(QString(ENV_HEADER_VARIABLE_LINE).arg(key).arg(value)); } QString envHeaderContent = GenerateCmake::readTemplate(ENV_HEADER_TEMPLATE_PATH) .arg(environment); FilePath envHeaderPath = srcDir.pathAppended(FILENAME_ENV_HEADER); envHeaderOk = m_fileQueue.queueFile(envHeaderPath, envHeaderContent); } } return cppOk && pluginHeaderOk && envHeaderOk; } const char MAIN_QMLFILE_TEMPLATE_PATH[] = ":/boilerplatetemplates/qmlprojectmainqml.tpl"; bool CmakeFileGenerator::generateMainQml(const FilePath &dir) { QString content = GenerateCmake::readTemplate(MAIN_QMLFILE_TEMPLATE_PATH); FilePath filePath = dir.pathAppended(FILENAME_MAINQML); return m_fileQueue.queueFile(filePath, content); } bool CmakeFileGenerator::isFileResource(const QString &relativeFilePath) { if (m_resourceFileLocations.contains(relativeFilePath)) return true; return false; } } //GenerateCmake } //QmlProjectManager