QML Puppet: Support for absolute QRC paths

Enabled support for absolute QRC paths, e.g.:
  * qrc:path/to/some/file
  * qrc:/path/to/some/file
  * /path/to/some/file

The QRC paths are mapped to the actual file system path in QML Puppet
via the environment variable QMLPUPPET_PROJECT_ROOT.

A bit of refactoring so that QML Puppet and QML Runtime share some
common pieces of code.

Fixes: QDS-15385
Change-Id: I3b549eae5700493a3ea654660dbe7d94b4e5b6de
Reviewed-by: Knud Dollereder <knud.dollereder@qt.io>
This commit is contained in:
Andrii Semkiv
2025-05-27 16:39:32 +02:00
parent 8abb142fcb
commit ed9a2dbbd1
11 changed files with 265 additions and 140 deletions

View File

@@ -11,6 +11,7 @@
#include <extensionsystem/pluginmanager.h>
#include <extensionsystem/pluginspec.h>
#include <projectexplorer/kit.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/target.h>
#include <qmlprojectmanager/qmlmultilanguageaspect.h>
#include <qmlprojectmanager/qmlproject.h>
@@ -55,7 +56,7 @@ QProcessEnvironment PuppetEnvironmentBuilder::processEnvironment() const
addCustomFileSelectors();
addDisableDeferredProperties();
addResolveUrlsOnAssignment();
addMcuFonts();
addMcuItems();
qCInfo(puppetEnvirmentBuild) << "Puppet environment:" << m_environment.toStringList();
@@ -254,6 +255,19 @@ void PuppetEnvironmentBuilder::addResolveUrlsOnAssignment() const
m_environment.set("QML_COMPAT_RESOLVE_URLS_ON_ASSIGNMENT", "true");
}
void PuppetEnvironmentBuilder::addMcuItems() const
{
if (QmlDesigner::DesignerMcuManager::instance().isMCUProject()) {
addMcuFonts();
const Utils::FilePath projectRoot = ProjectExplorer::ProjectManager::startupProject()
->projectFilePath()
.parentDir();
m_environment.set(QmlProjectManager::Constants::QMLPUPPET_ENV_PROJECT_ROOT,
projectRoot.toUserOutput());
}
}
void PuppetEnvironmentBuilder::addMcuFonts() const
{
const Utils::expected_str<Utils::FilePath> mcuFontsDir = QmlProjectManager::mcuFontsDir();
@@ -265,11 +279,9 @@ void PuppetEnvironmentBuilder::addMcuFonts() const
m_environment.set(QmlProjectManager::Constants::QMLPUPPET_ENV_MCU_FONTS_DIR,
mcuFontsDir->toUserOutput());
if (QmlDesigner::DesignerMcuManager::instance().isMCUProject()) {
const QString defaultFontFamily = DesignerMcuManager::defaultFontFamilyMCU();
m_environment.set(QmlProjectManager::Constants::QMLPUPPET_ENV_DEFAULT_FONT_FAMILY,
defaultFontFamily);
}
}
PuppetType PuppetEnvironmentBuilder::determinePuppetType() const

View File

@@ -51,6 +51,7 @@ private:
void addCustomFileSelectors() const;
void addDisableDeferredProperties() const;
void addResolveUrlsOnAssignment() const;
void addMcuItems() const;
void addMcuFonts() const;
private:

View File

@@ -702,6 +702,12 @@ Utils::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::expected_str<Utils::FilePath> fontsDir = mcuFontsDir();
if (!fontsDir) {
qWarning() << "Failed to locate MCU installation." << fontsDir.error();
@@ -709,7 +715,6 @@ Utils::EnvironmentItems QmlBuildSystem::environment() const
}
env.append({Constants::QMLPUPPET_ENV_MCU_FONTS_DIR, fontsDir->toUserOutput()});
if (qtForMCUs()) {
env.append({Constants::QMLPUPPET_ENV_DEFAULT_FONT_FAMILY, defaultFontFamilyMCU()});
}

View File

@@ -46,5 +46,6 @@ constexpr char FALLBACK_MCU_FONT_FAMILY[] = "DejaVu Sans";
// These constants should be kept in sync with their counterparts in qmlbase.h
constexpr char QMLPUPPET_ENV_MCU_FONTS_DIR[] = "QMLPUPPET_MCU_FONTS_DIR";
constexpr char QMLPUPPET_ENV_DEFAULT_FONT_FAMILY[] = "QMLPUPPET_DEFAULT_FONT_FAMILY";
constexpr char QMLPUPPET_ENV_PROJECT_ROOT[] = "QMLPUPPET_PROJECT_ROOT";
} // QmlProjectManager::Constants

View File

@@ -40,7 +40,7 @@ add_qtc_executable(qmlpuppet
${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}
SOURCES
qmlpuppet/qmlpuppetmain.cpp
qmlpuppet/qmlbase.h
qmlpuppet/qmlbase.h qmlpuppet/qmlbase.cpp
qmlpuppet/qmlpuppet.h qmlpuppet/qmlpuppet.cpp
qmlpuppet/configcrashpad.h
qmlpuppet.qrc

View File

@@ -3,8 +3,9 @@
#include "qmlprivategate.h"
#include <objectnodeinstance.h>
#include <nodeinstanceserver.h>
#include <objectnodeinstance.h>
#include <qmlpuppet/qmlbase.h>
#include <QQuickItem>
#include <QQmlComponent>
@@ -43,7 +44,89 @@
#include <private/qquick3drepeater_p.h>
#endif
#include <filesystem>
using namespace Qt::Literals::StringLiterals;
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
using EngineHandler = std::unique_ptr<QAbstractFileEngine>;
#else
using EngineHandler = QAbstractFileEngine *;
#endif
namespace {
QString qmlDesignerRCPath()
{
static const QString qmlDesignerRcPathsString = QString::fromLocal8Bit(
qgetenv("QMLDESIGNER_RC_PATHS"));
return qmlDesignerRcPathsString;
}
QString fixResourcePath(QString path, const QString &before, const QString &after)
{
path.replace(path.indexOf(before), before.length(), after);
return path;
}
EngineHandler makeNormalizedPathEngineHandler(QString path)
{
path.replace("//", "/");
path.replace('\\', '/');
return EngineHandler{new QFSFileEngine(path)};
}
std::optional<EngineHandler> tryMakeEngineHandler(QString fixedPath)
{
// It turns out that `QFileInfo::exists` uses `QAbstractFileHandler` internally, so we cannot
// call it inside `QAbstractFileHandler::create` (directly or indirectly), otherwise it creates
// infinite recursion. `std::filesystem::exists` is fine though.
if (std::filesystem::exists(fixedPath.toStdString())) {
return makeNormalizedPathEngineHandler(std::move(fixedPath));
}
return {};
}
std::optional<EngineHandler> tryMakeEngineHandlerWithProjectRoot(
const QString &fileName, const QString &prefix)
{
if (const auto projectRoot = QString::fromLocal8Bit(
qgetenv(QmlBase::QMLPUPPET_ENV_PROJECT_ROOT));
!projectRoot.isEmpty()) {
QString fixedPath = fixResourcePath(fileName, prefix, projectRoot + '/');
if (auto handler = tryMakeEngineHandler(std::move(fixedPath))) {
return std::move(*handler);
}
}
return {};
}
EngineHandler makeQrcResourceHandler(const QString &fileName, const QString &prefix)
{
const QStringList searchPaths = qmlDesignerRCPath().split(';', Qt::SkipEmptyParts);
for (const QString &qrcPath : searchPaths) {
const QStringList qrcDefintion = qrcPath.split('=');
if (qrcDefintion.count() == 2) {
QString fixedPath
= fixResourcePath(fileName, prefix + qrcDefintion.first(), qrcDefintion.last() + '/');
if (auto handler = tryMakeEngineHandler(std::move(fixedPath))) {
return std::move(*handler);
}
}
}
// If none of the user-defined mappings above worked, let's try the path relative
// to the project root.
if (auto handler = tryMakeEngineHandlerWithProjectRoot(fileName, prefix)) {
return std::move(*handler);
}
return {};
};
} // namespace
namespace QmlDesigner {
@@ -568,13 +651,6 @@ QObject *createPrimitive(const QString &typeName, int majorNumber, int minorNumb
return QQuickDesignerSupportItems::createPrimitive(typeName, revision, context);
}
static QString qmlDesignerRCPath()
{
static const QString qmlDesignerRcPathsString = QString::fromLocal8Bit(
qgetenv("QMLDESIGNER_RC_PATHS"));
return qmlDesignerRcPathsString;
}
QVariant fixResourcePaths(const QVariant &value)
{
if (value.typeId() == QMetaType::QUrl) {
@@ -582,13 +658,20 @@ QVariant fixResourcePaths(const QVariant &value)
if (url.scheme() == QLatin1String("qrc")) {
const QString path = QLatin1String("qrc:") + url.path();
if (!qmlDesignerRCPath().isEmpty()) {
const QStringList searchPaths = qmlDesignerRCPath().split(QLatin1Char(';'));
const QStringList searchPaths
= qmlDesignerRCPath().split(QLatin1Char(';'), Qt::SkipEmptyParts);
for (const QString &qrcPath : searchPaths) {
const QStringList qrcDefintion = qrcPath.split(QLatin1Char('='));
if (qrcDefintion.count() == 2) {
QString fixedPath = path;
fixedPath.replace(QLatin1String("qrc:") + qrcDefintion.first(), qrcDefintion.last() + QLatin1Char('/'));
if (QFileInfo::exists(fixedPath)) {
fixedPath.replace(
QLatin1String("qrc:") + qrcDefintion.first(),
qrcDefintion.last() + QLatin1Char('/'));
// It turns out that `QFileInfo::exists` uses `QAbstractFileHandler`
// internally, so we cannot call it inside `QAbstractFileHandler::create`
// (directly or indirectly), otherwise it creates infinite recursion.
// `std::filesystem::exists` is fine though.
if (std::filesystem::exists(fixedPath.toStdString())) {
fixedPath.replace(QLatin1String("//"), QLatin1String("/"));
fixedPath.replace(QLatin1Char('\\'), QLatin1Char('/'));
return QUrl::fromLocalFile(fixedPath);
@@ -936,40 +1019,35 @@ public:
#endif
};
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
std::unique_ptr<QAbstractFileEngine>
#else
QAbstractFileEngine *
#endif
QrcEngineHandler::create(const QString &fileName) const
EngineHandler QrcEngineHandler::create(const QString &fileName) const
{
if (fileName.startsWith(":/qt-project.org"))
if (fileName.startsWith(":/qt-project.org") || fileName.startsWith("qrc:/qt-project.org"))
return {};
if (fileName.startsWith(":/qtquickplugin"))
if (fileName.startsWith(":/qtquickplugin") || fileName.startsWith("qrc:/qtquickplugin"))
return {};
if (fileName.startsWith(":/")) {
const QStringList searchPaths = qmlDesignerRCPath().split(';');
for (const QString &qrcPath : searchPaths) {
const QStringList qrcDefintion = qrcPath.split('=');
if (qrcDefintion.count() == 2) {
QString fixedPath = fileName;
fixedPath.replace(":" + qrcDefintion.first(), qrcDefintion.last() + '/');
for (const auto &scheme : {QString::fromLatin1(":"), QString::fromLatin1("qrc:")}) {
for (const auto &prefix : {QString{scheme + '/'}, scheme}) {
if (fileName.startsWith(prefix)) {
return makeQrcResourceHandler(fileName, prefix);
}
}
}
if (fileName == fixedPath)
if (fileName.startsWith('/')) {
// On UNIX it might be a valid filesystem path
#if !defined(Q_OS_WIN)
// It turns out that `QFileInfo::exists` uses `QAbstractFileHandler` internally,
// so we cannot call it inside `QAbstractFileHandler::create` (directly or indirectly),
// otherwise it creates infinite recursion. `std::filesystem::exists` is fine though.
if (std::filesystem::exists(fileName.toStdString())) {
return {};
if (QFileInfo::exists(fixedPath)) {
fixedPath.replace("//", "/");
fixedPath.replace('\\', '/');
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
return std::make_unique<QFSFileEngine>(fixedPath);
#else
return new QFSFileEngine(fixedPath);
}
#endif
}
}
if (auto handler = tryMakeEngineHandlerWithProjectRoot(fileName, "/")) {
return std::move(*handler);
}
}

View File

@@ -313,10 +313,6 @@ void NodeInstanceServer::stopRenderTimer()
void NodeInstanceServer::createScene(const CreateSceneCommand &command)
{
initializeView();
if (const QString mcuFontsFolder = qEnvironmentVariable(QmlBase::QMLPUPPET_ENV_MCU_FONTS_DIR);
!mcuFontsFolder.isEmpty()) {
registerFonts(QUrl::fromLocalFile(mcuFontsFolder));
}
registerFonts(command.resourceUrl);
setTranslationLanguage(command.language);
@@ -1581,11 +1577,7 @@ void NodeInstanceServer::registerFonts(const QUrl &resourceUrl) const
if (!resourceUrl.isValid())
return;
// Autoregister all fonts found inside the project
QDirIterator it {QFileInfo(resourceUrl.toLocalFile()).absoluteFilePath(),
{"*.ttf", "*.otf"}, QDir::Files, QDirIterator::Subdirectories};
while (it.hasNext())
QFontDatabase::addApplicationFont(it.next());
QmlBase::registerFonts(resourceUrl.toLocalFile());
}
bool NodeInstanceServer::isInformationServer() const

View File

@@ -0,0 +1,101 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "qmlbase.h"
#include "qmlprivategate/qmlprivategate.h"
#include <QCommandLineOption>
#include <QCoreApplication>
#include <QDir>
#include <QDirIterator>
#include <QFileInfo>
#include <QFont>
#include <QFontDatabase>
#include <QGuiApplication>
#include <QObject>
#include <QUrl>
#include <QtEnvironmentVariables>
#include <QtLogging>
#include <cstdlib>
#include <iostream>
QmlBase::QmlBase(int &argc, char **argv, QObject *parent)
: QObject{parent}
, m_args({.argc = argc, .argv = argv})
{
m_argParser.setApplicationDescription("QML Runtime Provider for QDS");
m_argParser.addOption({"qml-puppet", "Run QML Puppet (default)"});
m_argParser.addOption({"qml-renderer", "Run QML Renderer"});
#ifdef ENABLE_INTERNAL_QML_RUNTIME
m_argParser.addOption({"qml-runtime", "Run QML Runtime"});
#endif
m_argParser.addOption({"test", "Run test mode"});
}
int QmlBase::startTestMode()
{
qDebug() << "Test mode is not implemented for this type of runner";
return 0;
}
void QmlBase::initQmlRunner()
{
QmlDesigner::Internal::QmlPrivateGate::registerFixResourcePathsForObjectCallBack();
if (const QString defaultFontFamily = qEnvironmentVariable(QMLPUPPET_ENV_DEFAULT_FONT_FAMILY);
!defaultFontFamily.isEmpty()) {
if (qobject_cast<QGuiApplication *>(QCoreApplication::instance()) != nullptr) {
QGuiApplication::setFont(QFont{defaultFontFamily});
}
}
if (const QString mcuFontsFolder = qEnvironmentVariable(QMLPUPPET_ENV_MCU_FONTS_DIR);
!mcuFontsFolder.isEmpty()) {
registerFonts(mcuFontsFolder);
}
}
int QmlBase::run()
{
populateParser();
initCoreApp();
if (!m_coreApp) { //default to QGuiApplication
createCoreApp<QGuiApplication>();
qWarning() << "CoreApp is not initialized! Falling back to QGuiApplication!";
}
initParser();
initQmlRunner();
return QCoreApplication::exec();
}
void QmlBase::initParser()
{
const QCommandLineOption optHelp = m_argParser.addHelpOption();
if (!m_argParser.parse(QCoreApplication::arguments())) {
std::cout << "Error: " << m_argParser.errorText().toStdString() << "\n";
if (m_argParser.errorText().contains("qml-runtime")) {
std::cout << "Note: --qml-runtime is only available when Qt is 6.4.x or higher\n";
}
std::cout << "\n";
m_argParser.showHelp(1);
} else if (m_argParser.isSet(optHelp)) {
m_argParser.showHelp(0);
} else if (m_argParser.isSet("test")) {
exit(startTestMode());
}
}
void QmlBase::registerFonts(const QDir &dir)
{
// Autoregister all fonts found inside the dir
QDirIterator
itr{dir.absolutePath(), {"*.ttf", "*.otf"}, QDir::Files, QDirIterator::Subdirectories};
while (itr.hasNext()) {
QFontDatabase::addApplicationFont(itr.next());
}
}

View File

@@ -5,11 +5,10 @@
#include <QApplication>
#include <QCommandLineParser>
#include <QDir>
#include <QFont>
#include <QQmlApplicationEngine>
#include <iostream>
class QmlBase : public QObject
{
Q_OBJECT
@@ -17,6 +16,9 @@ public:
// These constants should be kept in sync with their counterparts in qmlprojectconstants.h
static constexpr char QMLPUPPET_ENV_MCU_FONTS_DIR[] = "QMLPUPPET_MCU_FONTS_DIR";
static constexpr char QMLPUPPET_ENV_DEFAULT_FONT_FAMILY[] = "QMLPUPPET_DEFAULT_FONT_FAMILY";
static constexpr char QMLPUPPET_ENV_PROJECT_ROOT[] = "QMLPUPPET_PROJECT_ROOT";
static void registerFonts(const QDir &dir);
struct AppArgs
{
@@ -25,58 +27,22 @@ public:
char **argv;
};
QmlBase(int &argc, char **argv, QObject *parent = nullptr)
: QObject{parent}
, m_args({argc, argv})
{
m_argParser.setApplicationDescription("QML Runtime Provider for QDS");
m_argParser.addOption({"qml-puppet", "Run QML Puppet (default)"});
m_argParser.addOption({"qml-renderer", "Run QML Renderer"});
#ifdef ENABLE_INTERNAL_QML_RUNTIME
m_argParser.addOption({"qml-runtime", "Run QML Runtime"});
#endif
m_argParser.addOption({"test", "Run test mode"});
}
QmlBase(int &argc, char **argv, QObject *parent = nullptr);
int run()
{
populateParser();
initCoreApp();
if (!m_coreApp) { //default to QGuiApplication
createCoreApp<QGuiApplication>();
qWarning() << "CoreApp is not initialized! Falling back to QGuiApplication!";
}
initParser();
initQmlRunner();
return m_coreApp->exec();
}
int run();
QSharedPointer<QCoreApplication> coreApp() const { return m_coreApp; }
protected:
virtual void initCoreApp() = 0;
virtual void populateParser() = 0;
virtual void initQmlRunner() = 0;
virtual int startTestMode()
{
qDebug() << "Test mode is not implemented for this type of runner";
return 0;
}
virtual void initQmlRunner();
virtual int startTestMode();
template<typename T>
void createCoreApp()
{
m_coreApp.reset(new T(m_args.argc, m_args.argv));
if (const QString defaultFontFamily = qEnvironmentVariable(
QMLPUPPET_ENV_DEFAULT_FONT_FAMILY);
!defaultFontFamily.isEmpty()) {
if (qobject_cast<QGuiApplication *>(QCoreApplication::instance()) != nullptr) {
QGuiApplication::setFont(QFont{defaultFontFamily});
}
}
}
QSharedPointer<QCoreApplication> m_coreApp;
@@ -86,23 +52,5 @@ protected:
AppArgs m_args;
private:
void initParser()
{
QCommandLineOption optHelp = m_argParser.addHelpOption();
if (!m_argParser.parse(m_coreApp->arguments())) {
std::cout << "Error: " << m_argParser.errorText().toStdString() << std::endl;
if (m_argParser.errorText().contains("qml-runtime")) {
std::cout << "Note: --qml-runtime is only availabe when Qt is 6.4.x or higher"
<< std::endl;
}
std::cout << std::endl;
m_argParser.showHelp(1);
} else if (m_argParser.isSet(optHelp)) {
m_argParser.showHelp(0);
} else if (m_argParser.isSet("test")) {
exit(startTestMode());
}
}
void initParser();
};

View File

@@ -90,6 +90,8 @@ QString crashReportsPath()
void QmlPuppet::initQmlRunner()
{
QmlBase::initQmlRunner();
if (m_coreApp->arguments().count() < 2
|| (m_argParser.isSet("readcapturedstream") && m_coreApp->arguments().count() < 3)
|| (m_argParser.isSet("import3dAsset") && m_coreApp->arguments().count() < 6)

View File

@@ -21,18 +21,6 @@
#define FILE_OPEN_EVENT_WAIT_TIME 3000 // ms
#define QSL QStringLiteral
static void registerFonts(const QDir &projectDir)
{
// Autoregister all fonts found inside the project
QDirIterator it{projectDir.absolutePath(),
{"*.ttf", "*.otf"},
QDir::Files,
QDirIterator::Subdirectories};
while (it.hasNext()) {
QFontDatabase::addApplicationFont(it.next());
}
}
static QDir findProjectFolder(const QDir &currentDir, int ret = 0)
{
if (ret > 2)
@@ -161,12 +149,9 @@ void QmlRuntime::initCoreApp()
void QmlRuntime::initQmlRunner()
{
registerFonts(findProjectFolder(QDir::current()));
QmlBase::initQmlRunner();
if (const QString mcuFontsFolder = qEnvironmentVariable(QmlBase::QMLPUPPET_ENV_MCU_FONTS_DIR);
!mcuFontsFolder.isEmpty()) {
registerFonts(mcuFontsFolder);
}
registerFonts(findProjectFolder(QDir::current()));
m_qmlEngine.reset(new QQmlApplicationEngine());