// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 #include "qmlbuildsystem.h" #include "../qmlprojectconstants.h" #include "../qmlprojectmanagertr.h" #include "../qmlproject.h" #include "projectitem/qmlprojectitem.h" #include "projectnode/qmlprojectnodes.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace ProjectExplorer; using namespace Utils; using namespace ExtensionSystem; namespace QmlProjectManager { namespace { Q_LOGGING_CATEGORY(infoLogger, "QmlProjectManager.QmlBuildSystem", QtInfoMsg) } void updateMcuBuildStep(BuildConfiguration *bc, bool mcuEnabled) { if (auto plugin = findMcuSupportPlugin()) { QMetaObject::invokeMethod(plugin, "updateDeployStep", Qt::DirectConnection, Q_ARG(ProjectExplorer::BuildConfiguration *, bc), Q_ARG(bool, mcuEnabled)); } else if (mcuEnabled) { qWarning() << "Failed to find McuSupport plugin but qtForMCUs is enabled in the project"; } } QmlBuildSystem::QmlBuildSystem(BuildConfiguration *bc) : BuildSystem(bc) , m_fileGen(new QmlProjectExporter::Exporter(this)) { // refresh first - project information is used e.g. to decide the default RC's refresh(RefreshOptions::Project); updateDeploymentData(); // registerMenuButtons(); //is wip connect(project(), &Project::activeBuildConfigurationChanged, this, [this](BuildConfiguration *bc) { refresh(RefreshOptions::NoFileRefresh); m_fileGen->updateProject(qmlProject()); updateMcuBuildStep(bc, qtForMCUs()); }); connect(project(), &Project::projectFileIsDirty, this, [this] { refresh(RefreshOptions::Project); m_fileGen->updateProject(qmlProject()); m_fileGen->updateMenuAction(); updateMcuBuildStep(project()->activeBuildConfiguration(), qtForMCUs()); }); // FIXME: Check. Probably bogus after the BuildSystem move. // // addedTarget calls updateEnabled on the runconfigurations // // which needs to happen after refresh // foreach (Target *t, targets()) // addedTarget(t); } void QmlBuildSystem::updateDeploymentData() { if (!m_projectItem) return; if (RunDeviceTypeKitAspect::deviceTypeId(kit()) == ProjectExplorer::Constants::DESKTOP_DEVICE_TYPE) { return; } ProjectExplorer::DeploymentData deploymentData; for (const auto &file : m_projectItem->files()) { deploymentData.addFile(file, targetFile(file).parentDir().path()); } setDeploymentData(deploymentData); } QString QmlBuildSystem::defaultFontFamilyMCU() const { const QJsonObject project = m_projectItem->project(); QString defaultFontFamily = project["mcu"].toObject()["config"].toObject()["defaultFontFamily"].toString(); if (!defaultFontFamily.isEmpty()) { return defaultFontFamily; } return QmlProjectManager::Constants::FALLBACK_MCU_FONT_FAMILY; } //probably this method needs to be moved into QmlProjectPlugin::initialize to be called only once void QmlBuildSystem::registerMenuButtons() { Core::ActionContainer *menu = Core::ActionManager::actionContainer(Core::Constants::M_FILE); // QML Project file update button // This button saves the current configuration into the .qmlproject file auto action = new QAction(Tr::tr("Update QmlProject File"), this); //this registerAction registers a new action for each opened project, //causes the "action is already registered" warning if you have multiple opened projects, //is not a big thing for qds, but is annoying for qtc and should be fixed. Core::Command *cmd = Core::ActionManager::registerAction(action, "QmlProject.ProjectManager"); menu->addAction(cmd, Core::Constants::G_FILE_SAVE); QObject::connect(action, &QAction::triggered, this, &QmlBuildSystem::updateProjectFile); } //wip: bool QmlBuildSystem::updateProjectFile() { QFile file(mainFilePath().fileName().append("project-test")); if (!file.open(QIODevice::ReadWrite | QIODevice::Truncate)) { qCritical() << "Cannot open Qml Project file for editing!"; return false; } QTextStream ts(&file); // License ts << "/* " "File generated by Qt Creator" "Copyright (C) 2016 The Qt Company Ltd." "SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH " "Qt-GPL-exception-1.0" "*/" << Qt::endl << Qt::endl; // Components ts << "import QmlProject 1.1" << Qt::endl << Qt::endl; return true; } QmlProject *QmlBuildSystem::qmlProject() const { return qobject_cast(project()); } void QmlBuildSystem::triggerParsing() { refresh(RefreshOptions::Project); } FilePath QmlBuildSystem::canonicalProjectDir() const { return projectFilePath() .canonicalPath() .normalizedPathName() .parentDir(); } void QmlBuildSystem::refresh(RefreshOptions options) { ParseGuard guard = guardParsingRun(); switch (options) { case RefreshOptions::NoFileRefresh: break; case RefreshOptions::Project: initProjectItem(); [[fallthrough]]; case RefreshOptions::Files: parseProjectFiles(); } updateQmlCodeModel(); guard.markAsSuccess(); emit projectChanged(); } void QmlBuildSystem::updateQmlCodeModelInfo(QmlCodeModelInfo &projectInfo) { for (const QString &importPath : absoluteImportPaths()) projectInfo.qmlImportPaths.append(FilePath::fromString(importPath)); } void QmlBuildSystem::initProjectItem() { const FilePath projectPath = projectFilePath(); m_projectItem.reset(new QmlProjectItem{projectPath}); connect(m_projectItem.data(), &QmlProjectItem::filesChanged, this, &QmlBuildSystem::refreshFiles); m_fileGen->updateProjectItem(m_projectItem.data(), true); initMcuProjectItems(); } void QmlBuildSystem::initMcuProjectItems() { m_mcuProjectItems.clear(); m_mcuProjectFilesWatcher.clear(); const QStringList mcuProjectFiles = m_projectItem->qmlProjectModules(); for (const QString &mcuProjectFile : mcuProjectFiles) { FilePath mcuProjectFilePath = projectFilePath().parentDir().resolvePath(mcuProjectFile); auto qmlProjectItem = QSharedPointer(new QmlProjectItem{mcuProjectFilePath}); m_mcuProjectItems.append(qmlProjectItem); connect(qmlProjectItem.data(), &QmlProjectItem::filesChanged, this, &QmlBuildSystem::refreshFiles); m_fileGen->updateProjectItem(m_projectItem.data(), false); m_mcuProjectFilesWatcher.addFile(mcuProjectFilePath, FileSystemWatcher::WatchModifiedDate); connect(&m_mcuProjectFilesWatcher, &FileSystemWatcher::fileChanged, this, [this](const FilePath &file) { Q_UNUSED(file) initMcuProjectItems(); refresh(RefreshOptions::Files); }); } } void QmlBuildSystem::parseProjectFiles() { if (auto modelManager = QmlJS::ModelManagerInterface::instance()) { modelManager->updateSourceFiles(m_projectItem->files(), true); } const QString mainFileName = m_projectItem->mainFile(); if (!mainFileName.isEmpty()) { const FilePath mainFilePath = canonicalProjectDir().resolvePath(mainFileName); const Result res = mainFilePath.fileContents(); if (!res) { Core::MessageManager::writeFlashing( Tr::tr("Warning while loading project file %1.").arg(projectFilePath().toUserOutput())); Core::MessageManager::writeSilently(res.error()); } } generateProjectTree(); } void QmlBuildSystem::generateProjectTree() { auto newRoot = std::make_unique(project()); std::set uniqueFiles; for (const auto &file : m_projectItem->files()) uniqueFiles.insert(file); for (const auto &mcuProjectItem : m_mcuProjectItems) { for (const auto &file : mcuProjectItem->files()) uniqueFiles.insert(file); } for (const auto &file : uniqueFiles) { const FileType fileType = (file == projectFilePath()) ? FileType::Project : FileNode::fileTypeForFileName(file); newRoot->addNestedNode(std::make_unique(file, fileType)); } if (!projectFilePath().endsWith(Constants::fakeProjectName)) newRoot->addNestedNode(std::make_unique(projectFilePath(), FileType::Project)); setRootProjectNode(std::move(newRoot)); updateDeploymentData(); } bool QmlBuildSystem::setFileSettingInProjectFile(const QString &setting, const FilePath &mainFilePath, const QString &oldFile) { // make sure to change it also in the qmlproject file const FilePath qmlProjectFilePath = project()->projectFilePath(); Core::FileChangeBlocker fileChangeBlocker(qmlProjectFilePath); const QList editors = Core::DocumentModel::editorsForFilePath( qmlProjectFilePath); TextEditor::TextDocument *document = nullptr; if (!editors.isEmpty()) { document = qobject_cast(editors.first()->document()); if (document && document->isModified()) if (!Core::DocumentManager::saveDocument(document)) return false; } TextFileFormat textFileFormat; // qml files are defined to be utf-8 const TextFileFormat::ReadResult readResult = textFileFormat.readFile(qmlProjectFilePath, TextEncoding::Utf8); if (readResult.code != TextFileFormat::ReadSuccess) qWarning() << "Failed to read file" << qmlProjectFilePath << ":" << readResult.error; const QString settingQmlCode = setting + ":"; const FilePath projectDir = project()->projectFilePath().parentDir(); const QString relativePath = mainFilePath.relativeChildPath(projectDir).path(); QString fileContent = readResult.content; if (fileContent.indexOf(settingQmlCode) < 0) { QString addedText = QString("\n %1 \"%2\"\n").arg(settingQmlCode, relativePath); auto index = fileContent.lastIndexOf("}"); fileContent.insert(index, addedText); } else { QString originalFileName = oldFile; originalFileName.replace(".", "\\."); const QRegularExpression expression( QString("%1\\s*\"(%2)\"").arg(settingQmlCode, originalFileName)); const QRegularExpressionMatch match = expression.match(fileContent); fileContent.replace(match.capturedStart(1), match.capturedLength(1), relativePath); } if (const Result<> res = textFileFormat.writeFile(qmlProjectFilePath, fileContent); !res) qWarning() << "Failed to write file" << qmlProjectFilePath << ":" << res.error(); refresh(RefreshOptions::Project); return true; } bool QmlBuildSystem::blockFilesUpdate() const { return m_blockFilesUpdate; } void QmlBuildSystem::setBlockFilesUpdate(bool newBlockFilesUpdate) { m_blockFilesUpdate = newBlockFilesUpdate; } FilePath QmlBuildSystem::getStartupQmlFileWithFallback() const { const auto currentProject = project(); if (!currentProject) return {}; if (!target()) return {}; if (projectFilePath().endsWith(Constants::fakeProjectName)) return {}; const auto getFirstFittingFile = [](const FilePaths &files) -> FilePath { for (const auto &file : files) { if (file.exists()) return file; } return {}; }; const QStringView uiqmlstr = u"ui.qml"; const QStringView qmlstr = u"qml"; // First we check if mainUiFile is a valid ui.qml or .qml file const FilePath mainUiFile = mainUiFilePath(); if (const QString extension = mainUiFile.completeSuffix(); mainUiFile.exists() && (extension == uiqmlstr || extension == qmlstr)) { return mainUiFile; } // Then we check if there are any valid ui.qml files. mainQmlFile is preferred to be checked first. const FilePath mainQmlFile = mainFilePath(); if (mainQmlFile.exists() && mainQmlFile.completeSuffix() == uiqmlstr) return mainQmlFile; const FilePaths uiFiles = currentProject->files([&](const ProjectExplorer::Node *node) { return node->filePath().completeSuffix() == uiqmlstr; }); if (!uiFiles.isEmpty()) { if (const auto file = getFirstFittingFile(uiFiles); !file.isEmpty()) return file; } //check the suffix of mainQmlFile again, since there are no valid ui.qml files: if (mainQmlFile.exists() && mainQmlFile.completeSuffix() == qmlstr) return mainQmlFile; //maybe it's also worth priotizing qml files containing common words like "Screen"? const FilePaths qmlFiles = currentProject->files([&](const ProjectExplorer::Node *node) { return node->filePath().completeSuffix() == qmlstr; }); if (!qmlFiles.isEmpty()) { if (const auto file = getFirstFittingFile(qmlFiles); !file.isEmpty()) return file; } //if no source files exist in the project, lets try to open the .qmlproject file itself const FilePath projectFile = projectFilePath(); if (projectFile.exists()) return projectFile; return {}; } QmlBuildSystem *QmlBuildSystem::getStartupBuildSystem() { return qobject_cast(activeBuildSystemForActiveProject()); } void QmlBuildSystem::addQmlProjectModule(const FilePath &path) { m_projectItem->addQmlProjectModule(path.toFSPathString()); } void QmlBuildSystem::addFileFilter(const FilePath &path) { m_projectItem->addFileFilter(path); } FilePath QmlBuildSystem::mainFilePath() const { const QString fileName = mainFile(); if (fileName.isEmpty() || fileName.isNull()) { return {}; } return projectDirectory().pathAppended(fileName); } FilePath QmlBuildSystem::mainUiFilePath() const { const QString fileName = mainUiFile(); if (fileName.isEmpty() || fileName.isNull()) { return {}; } return projectDirectory().pathAppended(fileName); } bool QmlBuildSystem::setMainFileInProjectFile(const FilePath &newMainFilePath) { return setFileSettingInProjectFile("mainFile", newMainFilePath, mainFile()); } bool QmlBuildSystem::setMainUiFileInProjectFile(const FilePath &newMainUiFilePath) { return setMainUiFileInMainFile(newMainUiFilePath) && setFileSettingInProjectFile("mainUiFile", newMainUiFilePath, m_projectItem->mainUiFile()); } bool QmlBuildSystem::setMainUiFileInMainFile(const FilePath &newMainUiFilePath) { Core::FileChangeBlocker fileChangeBlocker(mainFilePath()); const QList editors = Core::DocumentModel::editorsForFilePath(mainFilePath()); TextEditor::TextDocument *document = nullptr; if (!editors.isEmpty()) { document = qobject_cast(editors.first()->document()); if (document && document->isModified()) if (!Core::DocumentManager::saveDocument(document)) return false; } TextFileFormat textFileFormat; // qml files are defined to be utf-8 const TextFileFormat::ReadResult res = textFileFormat.readFile(mainFilePath(), TextEncoding::Utf8); if (res.code != TextFileFormat::ReadSuccess) qWarning() << "Failed to read file" << mainFilePath() << ":" << res.error; const QString currentMain = QString("%1 {").arg(mainUiFilePath().baseName()); const QString newMain = QString("%1 {").arg(newMainUiFilePath.baseName()); QString fileContent = res.content; if (fileContent.contains(currentMain)) fileContent.replace(currentMain, newMain); if (const Result<> res = textFileFormat.writeFile(mainFilePath(), fileContent); !res) qWarning() << "Failed to write file" << mainFilePath() << ":" << res.error(); return true; } FilePath QmlBuildSystem::targetDirectory() const { FilePath result; if (RunDeviceTypeKitAspect::deviceTypeId(kit()) == ProjectExplorer::Constants::DESKTOP_DEVICE_TYPE) { result = canonicalProjectDir(); } else if (IDevice::ConstPtr device = RunDeviceKitAspect::device(kit())) { if (m_projectItem) result = device->filePath(m_projectItem->targetDirectory()); } return result; } FilePath QmlBuildSystem::targetFile(const FilePath &sourceFile) const { const FilePath sourceDir = m_projectItem ? m_projectItem->sourceDirectory() : canonicalProjectDir(); const QString relative = sourceFile.relativePathFromDir(sourceDir); return targetDirectory().resolvePath(relative); } void QmlBuildSystem::setSupportedLanguages(QStringList languages) { m_projectItem->setSupportedLanguages(languages); } void QmlBuildSystem::setPrimaryLanguage(QString language) { m_projectItem->setPrimaryLanguage(language); } bool QmlBuildSystem::enableCMakeGeneration() const { return m_projectItem->enableCMakeGeneration(); } void QmlBuildSystem::setEnableCMakeGeneration(bool enable) { if (enable != enableCMakeGeneration()) m_projectItem->setEnableCMakeGeneration(enable); } bool QmlBuildSystem::enablePythonGeneration() const { return m_projectItem->enablePythonGeneration(); } void QmlBuildSystem::setEnablePythonGeneration(bool enable) { if (enable != enablePythonGeneration()) m_projectItem->setEnablePythonGeneration(enable); } bool QmlBuildSystem::standaloneApp() const { return m_projectItem->standaloneApp(); } void QmlBuildSystem::setStandaloneApp(bool value) { if (value != standaloneApp()) m_projectItem->setStandaloneApp(value); } void QmlBuildSystem::refreshFiles(const QSet & /*added*/, const QSet &removed) { if (m_blockFilesUpdate) { qCDebug(infoLogger) << "Auto files refresh blocked."; return; } refresh(RefreshOptions::Files); if (!removed.isEmpty()) { if (auto modelManager = QmlJS::ModelManagerInterface::instance()) { modelManager->removeFiles(Utils::transform(removed, [](const QString &s) { return FilePath::fromString(s); })); } } updateDeploymentData(); } QVariant QmlBuildSystem::additionalData(Id id) const { if (id == Constants::customFileSelectorsData) return fileSelectors(); if (id == Constants::supportedLanguagesData) return supportedLanguages(); if (id == Constants::primaryLanguageData) return primaryLanguage(); if (id == Constants::customForceFreeTypeData) return forceFreeType(); if (id == Constants::customQtForMCUs) return qtForMCUs(); if (id == Constants::customQt6Project) return qt6Project(); if (id == Constants::mainFilePath) return mainFilePath().toUrlishString(); if (id == Constants::canonicalProjectDir) return canonicalProjectDir().toUrlishString(); if (id == Constants::customDefaultFontFamilyMCU) return defaultFontFamilyMCU(); return {}; } bool QmlBuildSystem::supportsAction(Node *context, ProjectAction action, const Node *node) const { if (dynamic_cast(context)) { if (action == AddNewFile || action == EraseFile) return true; QTC_ASSERT(node, return false); if (action == Rename && node->asFileNode()) { const FileNode *fileNode = node->asFileNode(); QTC_ASSERT(fileNode, return false); return fileNode->fileType() != FileType::Project; } return false; } return BuildSystem::supportsAction(context, action, node); } bool QmlBuildSystem::addFiles(Node *context, const FilePaths &filePaths, FilePaths *) { if (!dynamic_cast(context)) return false; FilePaths toAdd; for (const FilePath &filePath : filePaths) { if (!m_projectItem->matchesFile(filePath.toUrlishString())) toAdd << filePaths; } return toAdd.isEmpty(); } bool QmlBuildSystem::deleteFiles(Node *context, const FilePaths &filePaths) { if (dynamic_cast(context)) return true; return BuildSystem::deleteFiles(context, filePaths); } bool QmlBuildSystem::renameFiles(Node *context, const FilePairs &filesToRename, FilePaths *notRenamed) { if (!dynamic_cast(context)) return BuildSystem::renameFiles(context, filesToRename, notRenamed); bool success = true; for (const auto &[oldFilePath, newFilePath] : filesToRename) { const auto fail = [&, oldFilePath = oldFilePath] { success = false; if (notRenamed) *notRenamed << oldFilePath; }; if (oldFilePath.endsWith(mainFile())) { if (!setMainFileInProjectFile(newFilePath)) fail(); continue; } if (oldFilePath.endsWith(m_projectItem->mainUiFile())) { if (!setMainUiFileInProjectFile(newFilePath)) fail(); continue; } // Why is this not an error? } return success; } QString QmlBuildSystem::mainFile() const { return m_projectItem->mainFile(); } QString QmlBuildSystem::mainUiFile() const { return m_projectItem->mainUiFile(); } bool QmlBuildSystem::qtForMCUs() const { return m_projectItem->isQt4McuProject(); } bool QmlBuildSystem::qt6Project() const { return m_projectItem->versionQt() == "6"; } EnvironmentItems QmlBuildSystem::environment() const { Utils::EnvironmentItems env = m_projectItem->environment(); if (qtForMCUs()) { const Utils::FilePath projectRoot = ProjectExplorer::ProjectManager::startupProject() ->projectFilePath() .parentDir(); env.append({Constants::QMLPUPPET_ENV_PROJECT_ROOT, projectRoot.toUserOutput()}); Utils::Result fontsDir = mcuFontsDir(); if (!fontsDir) { qWarning() << "Failed to locate MCU installation." << fontsDir.error(); return env; } env.append({Constants::QMLPUPPET_ENV_MCU_FONTS_DIR, fontsDir->toUserOutput()}); env.append({Constants::QMLPUPPET_ENV_DEFAULT_FONT_FAMILY, defaultFontFamilyMCU()}); } return env; } QStringList QmlBuildSystem::fileSelectors() const { return m_projectItem->fileSelectors(); } bool QmlBuildSystem::multilanguageSupport() const { return m_projectItem->multilanguageSupport(); } QStringList QmlBuildSystem::supportedLanguages() const { return m_projectItem->supportedLanguages(); } QString QmlBuildSystem::primaryLanguage() const { return m_projectItem->primaryLanguage(); } bool QmlBuildSystem::forceFreeType() const { return m_projectItem->forceFreeType(); } bool QmlBuildSystem::widgetApp() const { return m_projectItem->widgetApp(); } QStringList QmlBuildSystem::shaderToolArgs() const { return m_projectItem->shaderToolArgs(); } QStringList QmlBuildSystem::shaderToolFiles() const { return m_projectItem->shaderToolFiles(); } QStringList QmlBuildSystem::allImports() const { return m_projectItem->importPaths() + m_projectItem->mockImports(); } QStringList QmlBuildSystem::importPaths() const { return m_projectItem->importPaths(); } void QmlBuildSystem::addImportPath(const FilePath &path) { m_projectItem->addImportPath(path.toFSPathString()); } QStringList QmlBuildSystem::mockImports() const { return m_projectItem->mockImports(); } QStringList QmlBuildSystem::absoluteImportPaths() const { return Utils::transform(allImports(), [&](const QString &importPath) { FilePath filePath = FilePath::fromString(importPath); if (filePath.isAbsolutePath()) return projectDirectory().resolvePath(importPath).path(); return (projectDirectory() / importPath).path(); }); } QStringList QmlBuildSystem::targetImportPaths() const { return Utils::transform(allImports(), [&](const QString &importPath) { const FilePath filePath = FilePath::fromString(importPath); if (filePath.isAbsolutePath()) return importPath; return (targetDirectory() / importPath).path(); }); } QString QmlBuildSystem::versionQt() const { return m_projectItem->versionQt(); } QString QmlBuildSystem::versionQtQuick() const { return m_projectItem->versionQtQuick(); } QString QmlBuildSystem::versionDesignStudio() const { return m_projectItem->versionDesignStudio(); } class QmlBuildConfigurationFactory final : public BuildConfigurationFactory { public: QmlBuildConfigurationFactory() { registerBuildConfiguration("QmlBuildConfiguration"); setSupportedProjectType(QmlProjectManager::Constants::QML_PROJECT_ID); setSupportedProjectMimeTypeName(Utils::Constants::QMLPROJECT_MIMETYPE); setBuildGenerator( [](const Kit *, const FilePath &projectPath, bool /* forSetup */) -> QList { BuildInfo bi; bi.buildSystemName = QmlBuildSystem::name(); bi.buildDirectory = projectPath; bi.displayName = bi.typeName = msgBuildConfigurationDefault(); bi.showBuildConfigs = bi.showBuildDirConfigWidget = false; return {bi}; }); } }; void setupQmlBuildConfiguration() { static const QmlBuildConfigurationFactory theQmlBuildConfigurationFactory; } } // namespace QmlProjectManager