qtdeclarative/tools/qmlplugindump/main.cpp

1390 lines
56 KiB
C++

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QtQml/qqmlengine.h>
#include <QtQml/private/qqmlengine_p.h>
#include <QtQml/private/qqmlmetatype_p.h>
#include <QtQml/private/qqmlopenmetaobject_p.h>
#include <QtQuick/private/qquickevents_p_p.h>
#include <QtQuick/private/qquickpincharea_p.h>
#ifdef QT_WIDGETS_LIB
#include <QApplication>
#endif // QT_WIDGETS_LIB
#include <QtGui/QGuiApplication>
#include <QtCore/QDir>
#include <QtCore/QFileInfo>
#include <QtCore/QSet>
#include <QtCore/QStringList>
#include <QtCore/QTimer>
#include <QtCore/QMetaObject>
#include <QtCore/QMetaProperty>
#include <QtCore/QDebug>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonParseError>
#include <QtCore/QJsonValue>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QProcess>
#include <QtCore/private/qobject_p.h>
#include <QtCore/private/qmetaobject_p.h>
#include <QtQmlTypeRegistrar/private/qqmljsstreamwriter_p.h>
#include <QtQml/private/qqmlsignalnames_p.h>
#include <QtQml/qqmlcomponent.h>
#include <QRegularExpression>
#include <iostream>
#include <algorithm>
#include "qmltypereader.h"
#ifdef QT_SIMULATOR
#include <QtGui/private/qsimulatorconnection_p.h>
#endif
#ifdef Q_OS_WIN
# if !defined(Q_CC_MINGW)
# include <crtdbg.h>
# endif
#include <qt_windows.h>
#endif
namespace {
const uint qtQmlMajorVersion = 2;
const uint qtQmlMinorVersion = 0;
const uint qtQuickMajorVersion = 2;
const uint qtQuickMinorVersion = 0;
const QString qtQuickQualifiedName = QString::fromLatin1("QtQuick %1.%2")
.arg(qtQuickMajorVersion)
.arg(qtQuickMinorVersion);
QString pluginImportPath;
bool verbose = false;
bool creatable = true;
QString currentProperty;
QString inObjectInstantiation;
}
struct QmlVersionInfo
{
QString pluginImportUri;
QTypeRevision version;
bool strict;
};
static bool matchingImportUri(const QQmlType &ty, const QmlVersionInfo& versionInfo) {
const QString &module = ty.module();
if (versionInfo.strict) {
return (versionInfo.pluginImportUri == module
&& (ty.version().majorVersion() == versionInfo.version.majorVersion()
|| !ty.version().hasMajorVersion()))
|| module.isEmpty();
}
return module.isEmpty()
|| versionInfo.pluginImportUri == module
|| module.startsWith(versionInfo.pluginImportUri + QLatin1Char('.'));
}
void collectReachableMetaObjects(const QMetaObject *meta, QSet<const QMetaObject *> *metas, const QmlVersionInfo &info, bool extended = false, bool alreadyChangedModule = false)
{
auto ty = QQmlMetaType::qmlType(meta);
if (! meta || metas->contains(meta))
return;
if (matchingImportUri(ty, info)) {
if (!alreadyChangedModule) {
// dynamic meta objects can break things badly
// but extended types are usually fine
const QMetaObjectPrivate *mop = reinterpret_cast<const QMetaObjectPrivate *>(meta->d.data);
if (extended || !(mop->flags & DynamicMetaObject))
metas->insert(meta);
} else if (!ty.module().isEmpty()) { // empty module (e.g. from an attached property) would cause a (false) match; do not warn about them
qWarning() << "Circular module dependency cannot be expressed in plugin.qmltypes file"
<< "Object was:" << meta->className()
<< ty.module() << info.pluginImportUri;
}
} else if (!ty.module().isEmpty()) {
alreadyChangedModule = true;
}
collectReachableMetaObjects(meta->superClass(), metas, info, /*extended=*/ false, alreadyChangedModule);
}
void collectReachableMetaObjects(QObject *object, QSet<const QMetaObject *> *metas, const QmlVersionInfo &info)
{
if (! object)
return;
const QMetaObject *meta = object->metaObject();
if (verbose)
std::cerr << "Processing object " << qPrintable( meta->className() ) << std::endl;
collectReachableMetaObjects(meta, metas, info);
for (int index = 0; index < meta->propertyCount(); ++index) {
QMetaProperty prop = meta->property(index);
if (prop.metaType().flags().testFlag(QMetaType::PointerToQObject)) {
if (verbose)
std::cerr << " Processing property " << qPrintable( prop.name() ) << std::endl;
currentProperty = QString("%1::%2").arg(meta->className(), prop.name());
// if the property was not initialized during construction,
// accessing a member of oo is going to cause a segmentation fault
QObject *oo = QQmlMetaType::toQObject(prop.read(object));
if (oo && !metas->contains(oo->metaObject()))
collectReachableMetaObjects(oo, metas, info);
currentProperty.clear();
}
}
}
void collectReachableMetaObjects(QQmlEnginePrivate *engine, const QQmlType &ty, QSet<const QMetaObject *> *metas, const QmlVersionInfo& info)
{
collectReachableMetaObjects(ty.baseMetaObject(), metas, info, ty.isExtendedType());
if (ty.attachedPropertiesType(engine) && matchingImportUri(ty, info)) {
collectReachableMetaObjects(ty.attachedPropertiesType(engine), metas, info);
}
}
/* When we dump a QMetaObject, we want to list all the types it is exported as.
To do this, we need to find the QQmlTypes associated with this
QMetaObject.
*/
static QHash<QByteArray, QSet<QQmlType> > qmlTypesByCppName;
static QHash<QByteArray, QByteArray> cppToId;
/* Takes a C++ type name, such as Qt::LayoutDirection or QString and
maps it to how it should appear in the description file.
These names need to be unique globally, so we don't change the C++ symbol's
name much. It is mostly used to for explicit translations such as
QString->string and translations for extended QML objects.
*/
QByteArray convertToId(const QByteArray &cppName)
{
return cppToId.value(cppName, cppName);
}
QByteArray convertToId(const QMetaObject *mo)
{
QByteArray className(mo->className());
if (!className.isEmpty())
return convertToId(className);
// likely a metaobject generated for an extended qml object
if (mo->superClass()) {
className = convertToId(mo->superClass());
className.append("_extended");
return className;
}
static QHash<const QMetaObject *, QByteArray> generatedNames;
className = generatedNames.value(mo);
if (!className.isEmpty())
return className;
std::cerr << "Found a QMetaObject without a className, generating a random name" << std::endl;
className = QByteArray("error-unknown-name-");
className.append(QByteArray::number(generatedNames.size()));
generatedNames.insert(mo, className);
return className;
}
// Collect all metaobjects for types registered with qmlRegisterType() without parameters
void collectReachableMetaObjectsWithoutQmlName(QQmlEnginePrivate *engine, QSet<const QMetaObject *>& metas,
QMap<QString, QList<QQmlType>> &compositeTypes, const QmlVersionInfo &info) {
const auto qmlAllTypes = QQmlMetaType::qmlAllTypes();
for (const QQmlType &ty : qmlAllTypes) {
if (!metas.contains(ty.baseMetaObject())) {
if (!ty.isComposite()) {
collectReachableMetaObjects(engine, ty, &metas, info);
} else if (matchingImportUri(ty, info)) {
compositeTypes[ty.elementName()].append(ty);
}
}
}
}
QSet<const QMetaObject *> collectReachableMetaObjects(QQmlEngine *engine,
QSet<const QMetaObject *> &noncreatables,
QSet<const QMetaObject *> &singletons,
QMap<QString, QList<QQmlType>> &compositeTypes,
const QmlVersionInfo &info,
const QList<QQmlType> &skip = QList<QQmlType>()
)
{
QSet<const QMetaObject *> metas;
metas.insert(&Qt::staticMetaObject);
const auto qmlTypes = QQmlMetaType::qmlTypes();
for (const QQmlType &ty : qmlTypes) {
if (!matchingImportUri(ty,info))
continue;
if (!ty.isCreatable())
noncreatables.insert(ty.baseMetaObject());
if (ty.isSingleton())
singletons.insert(ty.baseMetaObject());
if (!ty.isComposite()) {
if (const QMetaObject *mo = ty.baseMetaObject())
qmlTypesByCppName[mo->className()].insert(ty);
collectReachableMetaObjects(QQmlEnginePrivate::get(engine), ty, &metas, info);
} else {
compositeTypes[ty.elementName()].append(ty);
}
}
if (creatable) {
// find even more QMetaObjects by instantiating QML types and running
// over the instances
for (const QQmlType &ty : qmlTypes) {
if (!matchingImportUri(ty, info))
continue;
if (skip.contains(ty))
continue;
if (ty.isExtendedType())
continue;
if (!ty.isCreatable())
continue;
if (ty.typeName() == "QQmlComponent")
continue;
QString tyName = ty.qmlTypeName();
tyName = tyName.mid(tyName.lastIndexOf(QLatin1Char('/')) + 1);
if (tyName.isEmpty())
continue;
inObjectInstantiation = tyName;
QObject *object = nullptr;
if (ty.isSingleton()) {
QQmlType::SingletonInstanceInfo::ConstPtr siinfo = ty.singletonInstanceInfo();
if (!siinfo) {
std::cerr << "Internal error, " << qPrintable(tyName)
<< "(" << qPrintable( QString::fromUtf8(ty.typeName()) ) << ")"
<< " is singleton, but has no singletonInstanceInfo" << std::endl;
continue;
}
if (ty.isQObjectSingleton()) {
if (verbose)
std::cerr << "Trying to get singleton for " << qPrintable(tyName)
<< " (" << qPrintable( QString::fromUtf8(siinfo->typeName) )
<< ")" << std::endl;
collectReachableMetaObjects(object, &metas, info);
object = QQmlEnginePrivate::get(engine)->singletonInstance<QObject*>(ty);
} else {
inObjectInstantiation.clear();
continue; // we don't handle QJSValue singleton types.
}
} else {
if (verbose)
std::cerr << "Trying to create object " << qPrintable( tyName )
<< " (" << qPrintable( QString::fromUtf8(ty.typeName()) ) << ")" << std::endl;
object = ty.create();
}
inObjectInstantiation.clear();
if (object) {
if (verbose)
std::cerr << "Got " << qPrintable( tyName )
<< " (" << qPrintable( QString::fromUtf8(ty.typeName()) ) << ")" << std::endl;
collectReachableMetaObjects(object, &metas, info);
object->deleteLater();
} else {
std::cerr << "Could not create " << qPrintable(tyName) << std::endl;
}
}
}
collectReachableMetaObjectsWithoutQmlName(QQmlEnginePrivate::get(engine), metas, compositeTypes, info);
return metas;
}
class KnownAttributes {
QHash<QByteArray, QTypeRevision> m_properties;
QHash<QByteArray, QHash<int, QTypeRevision> > m_methods;
public:
bool knownMethod(const QByteArray &name, int nArgs, QTypeRevision revision)
{
if (m_methods.contains(name)) {
QHash<int, QTypeRevision> overloads = m_methods.value(name);
if (overloads.contains(nArgs) && overloads.value(nArgs).toEncodedVersion<quint16>() <= revision.toEncodedVersion<quint16>())
return true;
}
m_methods[name][nArgs] = revision;
return false;
}
bool knownProperty(const QByteArray &name, QTypeRevision revision)
{
if (m_properties.contains(name) && m_properties.value(name).toEncodedVersion<quint16>() <= revision.toEncodedVersion<quint16>())
return true;
m_properties[name] = revision;
return false;
}
};
class Dumper
{
QQmlJSStreamWriter *qml;
QString relocatableModuleUri;
public:
Dumper(QQmlJSStreamWriter *qml) : qml(qml) {}
void setRelocatableModuleUri(const QString &uri)
{
relocatableModuleUri = uri;
}
QByteArray getExportString(const QQmlType &type, const QmlVersionInfo &versionInfo)
{
const QString module = type.module().isEmpty() ? versionInfo.pluginImportUri
: type.module();
QTypeRevision version = QTypeRevision::fromVersion(
type.version().hasMajorVersion() ? type.version().majorVersion()
: versionInfo.version.majorVersion(),
type.version().hasMinorVersion() ? type.version().minorVersion()
: versionInfo.version.minorVersion());
const QByteArray versionedElement
= (type.elementName() + QString::fromLatin1(" %1.%2")
.arg(version.majorVersion())
.arg(version.minorVersion())).toUtf8();
return (module == relocatableModuleUri)
? versionedElement
: module.toUtf8() + '/' + versionedElement;
}
void writeMetaContent(const QMetaObject *meta, KnownAttributes *knownAttributes = nullptr)
{
QSet<QString> implicitSignals = dumpMetaProperties(meta, QTypeRevision::zero(), knownAttributes);
if (meta == &QObject::staticMetaObject) {
// for QObject, hide deleteLater() and onDestroyed
for (int index = meta->methodOffset(); index < meta->methodCount(); ++index) {
QMetaMethod method = meta->method(index);
QByteArray signature = method.methodSignature();
if (signature == "destroyed(QObject*)"
|| signature == "destroyed()"
|| signature == "deleteLater()") {
continue;
}
dump(method, implicitSignals, knownAttributes);
}
// and add toString(), destroy() and destroy(int)
if (!knownAttributes || !knownAttributes->knownMethod("toString", 0, QTypeRevision::zero())) {
qml->writeStartObject("Method");
qml->writeStringBinding("name", QLatin1String("toString"));
qml->writeEndObject();
}
if (!knownAttributes || !knownAttributes->knownMethod("destroy", 0, QTypeRevision::zero())) {
qml->writeStartObject("Method");
qml->writeStringBinding("name", QLatin1String("destroy"));
qml->writeEndObject();
}
if (!knownAttributes || !knownAttributes->knownMethod("destroy", 1, QTypeRevision::zero())) {
qml->writeStartObject("Method");
qml->writeStringBinding("name", QLatin1String("destroy"));
qml->writeStartObject("Parameter");
qml->writeStringBinding("name", QLatin1String("delay"));
qml->writeStringBinding("type", QLatin1String("int"));
qml->writeEndObject();
qml->writeEndObject();
}
} else {
for (int index = meta->methodOffset(); index < meta->methodCount(); ++index)
dump(meta->method(index), implicitSignals, knownAttributes);
}
}
QByteArray getPrototypeNameForCompositeType(
const QMetaObject *metaObject, QList<const QMetaObject *> *objectsToMerge,
const QmlVersionInfo &versionInfo)
{
auto ty = QQmlMetaType::qmlType(metaObject);
QByteArray prototypeName;
if (matchingImportUri(ty, versionInfo)) {
// dynamic meta objects can break things badly
// but extended types are usually fine
const QMetaObjectPrivate *mop = reinterpret_cast<const QMetaObjectPrivate *>(metaObject->d.data);
if (!(mop->flags & DynamicMetaObject) && objectsToMerge
&& !objectsToMerge->contains(metaObject))
objectsToMerge->append(metaObject);
const QMetaObject *superMetaObject = metaObject->superClass();
if (!superMetaObject) {
prototypeName = "QObject";
} else {
QQmlType superType = QQmlMetaType::qmlType(superMetaObject);
if (superType.isValid() && !superType.isComposite())
return convertToId(superMetaObject->className());
prototypeName = getPrototypeNameForCompositeType(
superMetaObject, objectsToMerge, versionInfo);
}
} else {
prototypeName = convertToId(metaObject->className());
}
return prototypeName;
}
void dumpComposite(QQmlEngine *engine, const QList<QQmlType> &compositeType, const QmlVersionInfo &versionInfo)
{
for (const QQmlType &type : compositeType)
dumpCompositeItem(engine, type, versionInfo);
}
void dumpCompositeItem(QQmlEngine *engine, const QQmlType &compositeType, const QmlVersionInfo &versionInfo)
{
QQmlComponent e(engine, compositeType.sourceUrl());
if (!e.isReady()) {
std::cerr << "WARNING: skipping module " << compositeType.elementName().toStdString()
<< std::endl << e.errorString().toStdString() << std::endl;
return;
}
QObject *object = e.create();
if (!object)
return;
qml->writeStartObject("Component");
const QMetaObject *mainMeta = object->metaObject();
QList<const QMetaObject *> objectsToMerge;
KnownAttributes knownAttributes;
// Get C++ base class name for the composite type
QByteArray prototypeName = getPrototypeNameForCompositeType(
mainMeta, &objectsToMerge, versionInfo);
qml->writeStringBinding("prototype", QUtf8StringView(prototypeName));
const QByteArray exportString = getExportString(compositeType, versionInfo);
// TODO: why don't we simply output the compositeType.elementName() here?
// That would make more sense, but it would change the format quite a bit.
qml->writeStringBinding("name", QUtf8StringView(exportString));
qml->writeStringListBinding(
"exports", QList<QAnyStringView> { QUtf8StringView(exportString) });
// TODO: shouldn't this be metaObjectRevision().value<quint16>()
// rather than version().minorVersion()
qml->writeArrayBinding(
"exportMetaObjectRevisions",
QByteArrayList() << QByteArray::number(compositeType.version().minorVersion()));
qml->writeBooleanBinding("isComposite", true);
if (compositeType.isSingleton()) {
qml->writeBooleanBinding("isCreatable", false);
qml->writeBooleanBinding("isSingleton", true);
}
for (int index = mainMeta->classInfoCount() - 1 ; index >= 0 ; --index) {
QMetaClassInfo classInfo = mainMeta->classInfo(index);
if (QUtf8StringView(classInfo.name()) == QUtf8StringView("DefaultProperty")) {
qml->writeStringBinding("defaultProperty", QUtf8StringView(classInfo.value()));
break;
}
}
for (const QMetaObject *meta : std::as_const(objectsToMerge)) {
for (int index = meta->enumeratorOffset(); index < meta->enumeratorCount(); ++index)
dump(meta->enumerator(index));
writeMetaContent(meta, &knownAttributes);
}
qml->writeEndObject();
}
QByteArray getDefaultProperty(const QMetaObject *meta)
{
for (int index = meta->classInfoCount() - 1; index >= 0; --index) {
QMetaClassInfo classInfo = meta->classInfo(index);
if (QLatin1String(classInfo.name()) == QLatin1String("DefaultProperty")) {
return QByteArray(classInfo.value());
}
}
return QByteArray();
}
struct QmlTypeInfo {
QmlTypeInfo() {}
QmlTypeInfo(
const QByteArray &exportString, QTypeRevision revision,
const QMetaObject *extendedObject, QByteArray attachedTypeId)
: exportString(exportString)
, revision(revision)
, extendedObject(extendedObject)
, attachedTypeId(attachedTypeId)
{}
QByteArray exportString;
QTypeRevision revision = QTypeRevision::zero();
const QMetaObject *extendedObject = nullptr;
QByteArray attachedTypeId;
};
void dump(QQmlEnginePrivate *engine, const QMetaObject *meta, bool isUncreatable, bool isSingleton)
{
qml->writeStartObject("Component");
QByteArray id = convertToId(meta);
qml->writeStringBinding("name", QUtf8StringView(id));
// collect type information
QVector<QmlTypeInfo> typeInfo;
const auto types = qmlTypesByCppName.value(meta->className());
for (const QQmlType &type : types) {
const QMetaObject *extendedObject = type.extensionFunction() ? type.metaObject() : nullptr;
QByteArray attachedTypeId;
if (const QMetaObject *attachedType = type.attachedPropertiesType(engine)) {
// Can happen when a type is registered that returns itself as attachedPropertiesType()
// because there is no creatable type to attach to.
if (attachedType != meta)
attachedTypeId = convertToId(attachedType);
}
const QByteArray exportString = getExportString(
type, { QString(), QTypeRevision(), false });
QTypeRevision metaObjectRevision = type.metaObjectRevision();
if (extendedObject) {
// emulate custom metaobjectrevision out of import
metaObjectRevision = type.version();
}
QmlTypeInfo info = { exportString, metaObjectRevision, extendedObject, attachedTypeId };
typeInfo.append(info);
}
// sort to ensure stable output
std::sort(typeInfo.begin(), typeInfo.end(), [](const QmlTypeInfo &i1, const QmlTypeInfo &i2) {
return i1.revision.toEncodedVersion<quint16>() < i2.revision.toEncodedVersion<quint16>();
});
// determine default property
// TODO: support revisioning of default property
QByteArray defaultProperty = getDefaultProperty(meta);
if (defaultProperty.isEmpty()) {
for (const QmlTypeInfo &iter : typeInfo) {
if (iter.extendedObject) {
defaultProperty = getDefaultProperty(iter.extendedObject);
if (!defaultProperty.isEmpty())
break;
}
}
}
if (!defaultProperty.isEmpty())
qml->writeStringBinding("defaultProperty", defaultProperty);
if (meta->superClass())
qml->writeStringBinding("prototype", convertToId(meta->superClass()));
if (!typeInfo.isEmpty()) {
QMap<QAnyStringView, QByteArray> exports; // sort exports
for (const QmlTypeInfo &iter : typeInfo) {
exports.insert(
QUtf8StringView(iter.exportString),
QByteArray::number(iter.revision.toEncodedVersion<quint16>()));
}
QByteArrayList metaObjectRevisions = exports.values();
qml->writeStringListBinding("exports", exports.keys());
if (isUncreatable)
qml->writeBooleanBinding("isCreatable", false);
if (isSingleton)
qml->writeBooleanBinding("isSingleton", true);
qml->writeArrayBinding("exportMetaObjectRevisions", metaObjectRevisions);
for (const QmlTypeInfo &iter : typeInfo) {
if (!iter.attachedTypeId.isEmpty()) {
qml->writeStringBinding("attachedType", iter.attachedTypeId);
break;
}
}
}
for (int index = meta->enumeratorOffset(); index < meta->enumeratorCount(); ++index)
dump(meta->enumerator(index));
writeMetaContent(meta);
// dump properties from extended metaobjects last
for (const auto &iter : typeInfo) {
if (iter.extendedObject)
dumpMetaProperties(iter.extendedObject, iter.revision);
}
qml->writeEndObject();
}
private:
/* Removes pointer and list annotations from a type name, returning
what was removed in isList and isPointer
*/
static void removePointerAndList(QByteArray *typeName, bool *isList, bool *isPointer)
{
static QByteArray declListPrefix = "QQmlListProperty<";
if (typeName->endsWith('*')) {
*isPointer = true;
typeName->truncate(typeName->size() - 1);
removePointerAndList(typeName, isList, isPointer);
} else if (typeName->startsWith(declListPrefix)) {
*isList = true;
typeName->truncate(typeName->size() - 1); // get rid of the suffix '>'
*typeName = typeName->mid(declListPrefix.size());
removePointerAndList(typeName, isList, isPointer);
}
*typeName = convertToId(*typeName);
}
void writeTypeProperties(QByteArray typeName, bool isWritable)
{
bool isList = false, isPointer = false;
removePointerAndList(&typeName, &isList, &isPointer);
qml->writeStringBinding("type", QUtf8StringView(typeName));
if (isList)
qml->writeBooleanBinding("isList", true);
if (!isWritable)
qml->writeBooleanBinding("isReadonly", true);
if (isPointer)
qml->writeBooleanBinding("isPointer", true);
}
void dump(const QMetaProperty &prop, QTypeRevision metaRevision = QTypeRevision(),
KnownAttributes *knownAttributes = nullptr)
{
// TODO: should that not be metaRevision.isValid() rather than comparing to zero()?
QTypeRevision revision = (metaRevision == QTypeRevision::zero())
? QTypeRevision::fromEncodedVersion(prop.revision())
: metaRevision;
QByteArray propName = prop.name();
if (knownAttributes && knownAttributes->knownProperty(propName, revision))
return;
qml->writeStartObject("Property");
qml->writeStringBinding("name", QUtf8StringView(prop.name()));
if (revision != QTypeRevision::zero())
qml->writeNumberBinding("revision", revision.toEncodedVersion<quint16>());
writeTypeProperties(prop.typeName(), prop.isWritable());
qml->writeEndObject();
}
QSet<QString> dumpMetaProperties(const QMetaObject *meta, QTypeRevision metaRevision = QTypeRevision(),
KnownAttributes *knownAttributes = nullptr)
{
QSet<QString> implicitSignals;
for (int index = meta->propertyOffset(); index < meta->propertyCount(); ++index) {
const QMetaProperty &property = meta->property(index);
dump(property, metaRevision, knownAttributes);
const QByteArray changedSignalName =
QQmlSignalNames::propertyNameToChangedSignalName(property.name());
if (knownAttributes)
knownAttributes->knownMethod(
changedSignalName, 0,
QTypeRevision::fromEncodedVersion(property.revision()));
implicitSignals.insert(changedSignalName);
}
return implicitSignals;
}
void dump(const QMetaMethod &meth, const QSet<QString> &implicitSignals,
KnownAttributes *knownAttributes = nullptr)
{
if (meth.methodType() == QMetaMethod::Signal) {
if (meth.access() != QMetaMethod::Public)
return; // nothing to do.
} else if (meth.access() != QMetaMethod::Public) {
return; // nothing to do.
}
QByteArray name = meth.name();
const QByteArray typeName = convertToId(meth.typeName());
if (implicitSignals.contains(name)
&& !meth.revision()
&& meth.methodType() == QMetaMethod::Signal
&& meth.parameterNames().isEmpty()
&& typeName == "void") {
// don't mention implicit signals
return;
}
QTypeRevision revision = QTypeRevision::fromEncodedVersion(meth.revision());
if (knownAttributes && knownAttributes->knownMethod(name, meth.parameterNames().size(), revision))
return;
if (meth.methodType() == QMetaMethod::Signal)
qml->writeStartObject("Signal");
else
qml->writeStartObject("Method");
qml->writeStringBinding("name", QUtf8StringView(name));
if (revision != QTypeRevision::zero())
qml->writeNumberBinding("revision", revision.toEncodedVersion<quint16>());
if (typeName != "void")
qml->writeStringBinding("type", QUtf8StringView(typeName));
for (int i = 0; i < meth.parameterTypes().size(); ++i) {
QByteArray argName = meth.parameterNames().at(i);
qml->writeStartObject("Parameter");
if (!argName.isEmpty())
qml->writeStringBinding("name", QUtf8StringView(argName));
writeTypeProperties(meth.parameterTypes().at(i), true);
qml->writeEndObject();
}
qml->writeEndObject();
}
void dump(const QMetaEnum &e)
{
qml->writeStartObject("Enum");
qml->writeStringBinding("name", QUtf8StringView(e.name()));
QList<QPair<QAnyStringView, int>> namesValues;
const int keyCount = e.keyCount();
namesValues.reserve(keyCount);
for (int index = 0; index < keyCount; ++index)
namesValues.append(qMakePair(QUtf8StringView(e.key(index)), e.value(index)));
qml->writeEnumObjectLiteralBinding("values", namesValues);
qml->writeEndObject();
}
};
enum ExitCode {
EXIT_INVALIDARGUMENTS = 1,
EXIT_SEGV = 2,
EXIT_IMPORTERROR = 3
};
void printUsage(const QString &appName)
{
std::cerr << qPrintable(QString(
"Usage: %1 [-v] [-qapp] [-noinstantiate] [-defaultplatform] [-[non]relocatable] [-dependencies <dependencies.json>] [-merge <file-to-merge.qmltypes>] [-output <output-file.qmltypes>] [-noforceqtquick] module.uri version [module/import/path]\n"
" %1 [-v] [-qapp] [-noinstantiate] -path path/to/qmldir/directory [version]\n"
" %1 [-v] -builtins\n"
"Example: %1 Qt.labs.folderlistmodel 2.0 /home/user/dev/qt-install/imports").arg(
appName)) << std::endl;
}
static bool readDependenciesData(QString dependenciesFile, const QByteArray &fileData,
QStringList *dependencies, const QStringList &urisToSkip,
bool forceQtQuickDependency = true) {
if (verbose) {
std::cerr << "parsing "
<< qPrintable( dependenciesFile ) << " skipping";
for (const QString &uriToSkip : urisToSkip)
std::cerr << ' ' << qPrintable(uriToSkip);
std::cerr << std::endl;
}
QJsonParseError parseError;
parseError.error = QJsonParseError::NoError;
QJsonDocument doc = QJsonDocument::fromJson(fileData, &parseError);
if (parseError.error != QJsonParseError::NoError) {
std::cerr << "Error parsing dependencies file " << dependenciesFile.toStdString()
<< ":" << parseError.errorString().toStdString() << " at " << parseError.offset
<< std::endl;
return false;
}
if (doc.isArray()) {
const QStringList requiredKeys = QStringList() << QStringLiteral("name")
<< QStringLiteral("type");
const auto deps = doc.array();
for (const QJsonValue dep : deps) {
if (dep.isObject()) {
QJsonObject obj = dep.toObject();
for (const QString &requiredKey : requiredKeys)
if (!obj.contains(requiredKey) || obj.value(requiredKey).isString())
continue;
if (obj.value(QStringLiteral("type")).toString() != QLatin1String("module"))
continue;
QString name = obj.value((QStringLiteral("name"))).toString();
QString version = obj.value(QStringLiteral("version")).toString();
if (name.isEmpty() || urisToSkip.contains(name))
continue;
if (name.contains(QLatin1String("Private"), Qt::CaseInsensitive)) {
if (verbose)
std::cerr << "skipping private dependency "
<< qPrintable( name ) << " " << qPrintable(version) << std::endl;
continue;
}
if (verbose)
std::cerr << "appending dependency "
<< qPrintable( name ) << " " << qPrintable(version) << std::endl;
dependencies->append(version.isEmpty() ? name
: (name + QLatin1Char(' ') + version));
}
}
} else {
std::cerr << "Error parsing dependencies file " << dependenciesFile.toStdString()
<< ": expected an array" << std::endl;
return false;
}
// Workaround for avoiding conflicting types when no dependency has been found.
//
// qmlplugindump used to import QtQuick, so all types defined in QtQuick used to be skipped when dumping.
// Now that it imports only Qt, it is no longer the case: if no dependency is found all the types defined
// in QtQuick will be dumped, causing conflicts.
if (forceQtQuickDependency && dependencies->isEmpty())
dependencies->push_back(qtQuickQualifiedName);
return true;
}
static bool readDependenciesFile(const QString &dependenciesFile, QStringList *dependencies,
const QStringList &urisToSkip) {
if (!QFileInfo::exists(dependenciesFile)) {
std::cerr << "non existing dependencies file " << dependenciesFile.toStdString()
<< std::endl;
return false;
}
QFile f(dependenciesFile);
if (!f.open(QFileDevice::ReadOnly)) {
std::cerr << "non existing dependencies file " << dependenciesFile.toStdString()
<< ", " << f.errorString().toStdString() << std::endl;
return false;
}
QByteArray fileData = f.readAll();
return readDependenciesData(dependenciesFile, fileData, dependencies, urisToSkip, false);
}
static bool getDependencies(const QQmlEngine &engine, const QString &pluginImportUri,
const QString &pluginImportVersion, QStringList *dependencies,
bool forceQtQuickDependency)
{
QString importScannerExe = QLatin1String("qmlimportscanner");
QFileInfo selfExe(QCoreApplication::applicationFilePath());
if (!selfExe.suffix().isEmpty())
importScannerExe += QLatin1String(".") + selfExe.suffix();
QString command = selfExe.absoluteDir().filePath(importScannerExe);
QStringList commandArgs = QStringList()
<< QLatin1String("-qmlFiles")
<< QLatin1String("-");
QStringList importPathList = engine.importPathList();
importPathList.removeOne(QStringLiteral("qrc:/qt-project.org/imports"));
for (const QString &path : importPathList)
commandArgs << QLatin1String("-importPath") << path;
QProcess importScanner;
importScanner.start(command, commandArgs, QProcess::ReadWrite);
if (!importScanner.waitForStarted())
return false;
importScanner.write("import ");
importScanner.write(pluginImportUri.toUtf8());
importScanner.write(" ");
importScanner.write(pluginImportVersion.toUtf8());
importScanner.write("\nQtObject{}\n");
importScanner.closeWriteChannel();
if (!importScanner.waitForFinished()) {
std::cerr << "failure to start " << qPrintable(command);
for (const QString &arg : std::as_const(commandArgs))
std::cerr << ' ' << qPrintable(arg);
std::cerr << std::endl;
return false;
}
QByteArray depencenciesData = importScanner.readAllStandardOutput();
if (!readDependenciesData(QLatin1String("<outputOfQmlimportscanner>"), depencenciesData,
dependencies, QStringList(pluginImportUri), forceQtQuickDependency)) {
std::cerr << "failed to process output of qmlimportscanner" << std::endl;
if (importScanner.exitCode() != 0)
std::cerr << importScanner.readAllStandardError().toStdString();
return false;
}
return true;
}
bool dependencyBetter(const QString &lhs, const QString &rhs)
{
QStringList leftSegments = lhs.split(QLatin1Char(' '), Qt::SkipEmptyParts);
QStringList rightSegments = rhs.split(QLatin1Char(' '), Qt::SkipEmptyParts);
if (leftSegments.isEmpty())
return false;
if (rightSegments.isEmpty())
return true;
const QString leftModule = leftSegments.first();
const QString rightModule = rightSegments.first();
if (leftModule < rightModule)
return true;
if (leftModule > rightModule)
return false;
if (leftSegments.size() == 1)
return false;
if (rightSegments.size() == 1)
return true;
const QStringList leftVersion = leftSegments.at(1).split(QLatin1Char('.'));
const QStringList rightVersion = rightSegments.at(1).split(QLatin1Char('.'));
auto compareSegment = [&](int segmentIndex) {
if (leftVersion.size() <= segmentIndex)
return rightVersion.size() > segmentIndex ? 1 : 0;
if (rightVersion.size() <= segmentIndex)
return -1;
bool leftOk = false;
bool rightOk = false;
const int leftSegment = leftSegments[segmentIndex].toUShort(&leftOk);
const int rightSegment = rightSegments[segmentIndex].toUShort(&rightOk);
if (!leftOk)
return rightOk ? 1 : 0;
if (!rightOk)
return -1;
return rightSegment - leftSegment;
};
const int major = compareSegment(0);
return (major == 0) ? compareSegment(1) < 0 : major < 0;
}
void compactDependencies(QStringList *dependencies)
{
std::sort(dependencies->begin(), dependencies->end(), dependencyBetter);
QString currentModule;
for (auto it = dependencies->begin(); it != dependencies->end();) {
QStringList segments = it->split(QLatin1Char(' '), Qt::SkipEmptyParts);
if (segments.isEmpty() || segments.first() == currentModule) {
it = dependencies->erase(it);
} else {
currentModule = segments.first();
++it;
}
}
}
void printDebugMessage(QtMsgType, const QMessageLogContext &, const QString &msg)
{
std::cerr << msg.toStdString() << std::endl;
// In case of QtFatalMsg the calling code will abort() when appropriate.
}
QT_BEGIN_NAMESPACE
static bool operator<(const QQmlType &a, const QQmlType &b)
{
return a.qmlTypeName() < b.qmlTypeName()
|| (a.qmlTypeName() == b.qmlTypeName()
&& ((a.version().majorVersion() < b.version().majorVersion())
|| (a.version().majorVersion() == b.version().majorVersion()
&& a.version().minorVersion() < b.version().minorVersion())));
}
QT_END_NAMESPACE
int main(int argc, char *argv[])
{
#if defined(Q_OS_WIN) && !defined(Q_CC_MINGW)
// we do not want windows popping up if the module loaded triggers an assert
SetErrorMode(SEM_NOGPFAULTERRORBOX);
_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_DEBUG);
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG);
_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG);
#endif // Q_OS_WIN && !Q_CC_MINGW
// The default message handler might not print to console on some systems. Enforce this.
qInstallMessageHandler(printDebugMessage);
#ifdef QT_SIMULATOR
// Running this application would bring up the Qt Simulator (since it links Qt GUI), avoid that!
QtSimulatorPrivate::SimulatorConnection::createStubInstance();
#endif
// don't require a window manager even though we're a QGuiApplication
bool requireWindowManager = false;
for (int index = 1; index < argc; ++index) {
if (QString::fromLocal8Bit(argv[index]) == "--defaultplatform"
|| QString::fromLocal8Bit(argv[index]) == "-defaultplatform") {
requireWindowManager = true;
break;
}
}
if (!requireWindowManager && qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM"))
qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("minimal"));
else
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts, true);
// Check which kind of application should be instantiated.
bool useQApplication = false;
for (int i = 0; i < argc; ++i) {
QString arg = QLatin1String(argv[i]);
if (arg == QLatin1String("--qapp") || arg == QLatin1String("-qapp"))
useQApplication = true;
}
#ifdef QT_WIDGETS_LIB
QScopedPointer<QCoreApplication> app(useQApplication
? new QApplication(argc, argv)
: new QGuiApplication(argc, argv));
#else
Q_UNUSED(useQApplication);
QScopedPointer<QCoreApplication> app(new QGuiApplication(argc, argv));
#endif // QT_WIDGETS_LIB
QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR));
QStringList args = app->arguments();
const QString appName = QFileInfo(app->applicationFilePath()).baseName();
if (args.size() < 2) {
printUsage(appName);
return EXIT_INVALIDARGUMENTS;
}
QString outputFilename;
QString pluginImportUri;
QString pluginImportVersion;
bool relocatable = true;
QString dependenciesFile;
QString mergeFile;
bool forceQtQuickDependency = true;
bool strict = false;
enum Action { Uri, Path, Builtins };
Action action = Uri;
{
QStringList positionalArgs;
for (int iArg = 0; iArg < args.size(); ++iArg) {
const QString &arg = args.at(iArg);
if (!arg.startsWith(QLatin1Char('-'))) {
positionalArgs.append(arg);
continue;
}
if (arg == QLatin1String("--dependencies")
|| arg == QLatin1String("-dependencies")) {
if (++iArg == args.size()) {
std::cerr << "missing dependencies file" << std::endl;
return EXIT_INVALIDARGUMENTS;
}
dependenciesFile = args.at(iArg);
// Remove absolute path so that it does not show up in the
// printed command line inside the plugins.qmltypes file.
args[iArg] = QFileInfo(args.at(iArg)).fileName();
} else if (arg == QLatin1String("--merge")
|| arg == QLatin1String("-merge")) {
if (++iArg == args.size()) {
std::cerr << "missing merge file" << std::endl;
return EXIT_INVALIDARGUMENTS;
}
mergeFile = args.at(iArg);
} else if (arg == QLatin1String("--notrelocatable")
|| arg == QLatin1String("-notrelocatable")
|| arg == QLatin1String("--nonrelocatable")
|| arg == QLatin1String("-nonrelocatable")) {
relocatable = false;
} else if (arg == QLatin1String("--relocatable")
|| arg == QLatin1String("-relocatable")) {
relocatable = true;
} else if (arg == QLatin1String("--noinstantiate")
|| arg == QLatin1String("-noinstantiate")) {
creatable = false;
} else if (arg == QLatin1String("--path")
|| arg == QLatin1String("-path")) {
action = Path;
} else if (arg == QLatin1String("--builtins")
|| arg == QLatin1String("-builtins")) {
action = Builtins;
} else if (arg == QLatin1String("-v")) {
verbose = true;
} else if (arg == QLatin1String("--noforceqtquick")
|| arg == QLatin1String("-noforceqtquick")){
forceQtQuickDependency = false;
} else if (arg == QLatin1String("--output")
|| arg == QLatin1String("-output")) {
if (++iArg == args.size()) {
std::cerr << "missing output file" << std::endl;
return EXIT_INVALIDARGUMENTS;
}
outputFilename = args.at(iArg);
} else if (arg == QLatin1String("--defaultplatform")
|| arg == QLatin1String("-defaultplatform")) {
continue;
} else if (arg == QLatin1String("--qapp")
|| arg == QLatin1String("-qapp")) {
continue;
} else if (arg == QLatin1String("--strict")
|| arg == QLatin1String("-strict")) {
strict = true;
continue;
} else {
std::cerr << "Invalid argument: " << qPrintable(arg) << std::endl;
return EXIT_INVALIDARGUMENTS;
}
}
std::cerr << "qmlplugindump is deprecated.\n"
<< "Please declare your types using QML_ELEMENT and related macros.\n"
<< "Then utilize the build system to invoke qmltyperegistrar in order to\n"
<< "generate qmltypes files.\n";
if (action == Uri) {
if (positionalArgs.size() != 3 && positionalArgs.size() != 4) {
std::cerr << "Incorrect number of positional arguments" << std::endl;
return EXIT_INVALIDARGUMENTS;
}
pluginImportUri = positionalArgs.at(1);
pluginImportVersion = positionalArgs[2];
if (positionalArgs.size() >= 4)
pluginImportPath = positionalArgs.at(3);
} else if (action == Path) {
if (positionalArgs.size() != 2 && positionalArgs.size() != 3) {
std::cerr << "Incorrect number of positional arguments" << std::endl;
return EXIT_INVALIDARGUMENTS;
}
pluginImportPath = QDir::fromNativeSeparators(positionalArgs.at(1));
if (positionalArgs.size() == 3)
pluginImportVersion = positionalArgs.at(2);
} else if (action == Builtins) {
if (positionalArgs.size() != 1) {
std::cerr << "Incorrect number of positional arguments" << std::endl;
return EXIT_INVALIDARGUMENTS;
}
}
}
QQmlEngine engine;
if (!pluginImportPath.isEmpty()) {
QDir cur = QDir::current();
cur.cd(pluginImportPath);
pluginImportPath = cur.canonicalPath();
if (!QDir::setCurrent(pluginImportPath)) {
std::cerr << "Cannot set current directory to import path "
<< qPrintable(pluginImportPath) << std::endl;
}
engine.addImportPath(pluginImportPath);
}
// Merge file.
QStringList mergeDependencies;
QByteArray mergeComponents;
if (!mergeFile.isEmpty()) {
const QStringList merge = readQmlTypes(mergeFile);
if (!merge.isEmpty()) {
static const QRegularExpression re("(\\w+\\.*\\w*\\s*\\d+\\.\\d+)");
QRegularExpressionMatchIterator i = re.globalMatch(merge[1]);
while (i.hasNext()) {
QRegularExpressionMatch m = i.next();
mergeDependencies << m.captured(1);
}
mergeComponents = merge[2].toUtf8();
}
}
// Dependencies.
bool calculateDependencies = !pluginImportUri.isEmpty() && !pluginImportVersion.isEmpty();
QStringList dependencies;
if (!dependenciesFile.isEmpty())
calculateDependencies = !readDependenciesFile(dependenciesFile, &dependencies,
QStringList(pluginImportUri)) && calculateDependencies;
if (calculateDependencies)
getDependencies(engine, pluginImportUri, pluginImportVersion, &dependencies,
forceQtQuickDependency);
compactDependencies(&dependencies);
QString qtQmlImportString = QString::fromLatin1("import QtQml %1.%2")
.arg(qtQmlMajorVersion)
.arg(qtQmlMinorVersion);
// load the QtQml builtins and the dependencies
{
QByteArray code(qtQmlImportString.toUtf8());
for (const QString &moduleToImport : std::as_const(dependencies)) {
code.append("\nimport ");
code.append(moduleToImport.toUtf8());
}
code.append("\nQtObject {}");
QQmlComponent c(&engine);
c.setData(code, QUrl::fromLocalFile(pluginImportPath + "/loaddependencies.qml"));
c.create();
const auto errors = c.errors();
if (!errors.isEmpty()) {
for (const QQmlError &error : errors)
std::cerr << qPrintable( error.toString() ) << std::endl;
return EXIT_IMPORTERROR;
}
}
// find all QMetaObjects reachable from the builtin module
QSet<const QMetaObject *> uncreatableMetas;
QSet<const QMetaObject *> singletonMetas;
// this will hold the meta objects we want to dump information of
QSet<const QMetaObject *> metas;
// composite types we want to dump information of
QMap<QString, QList<QQmlType>> compositeTypes;
QTypeRevision version = QTypeRevision::fromVersion(qtQmlMajorVersion, qtQmlMinorVersion);
QmlVersionInfo info;
if (action == Builtins) {
QMap<QString, QList<QQmlType>> defaultCompositeTypes;
QSet<const QMetaObject *> builtins = collectReachableMetaObjects(
&engine, uncreatableMetas, singletonMetas, defaultCompositeTypes,
{QLatin1String("Qt"), version, strict});
Q_ASSERT(builtins.size() == 1);
metas.insert(*builtins.begin());
} else {
auto versionSplitted = pluginImportVersion.split(".");
bool ok = versionSplitted.size() == 2;
if (!ok)
qCritical("Invalid version number");
else {
const int majorVersion = versionSplitted.at(0).toInt(&ok);
if (!ok)
qCritical("Invalid major version");
const int minorVersion = versionSplitted.at(1).toInt(&ok);
if (!ok)
qCritical("Invalid minor version");
version = QTypeRevision::fromVersion(majorVersion, minorVersion);
}
QList<QQmlType> defaultTypes = QQmlMetaType::qmlTypes();
// find a valid QtQuick import
QByteArray importCode;
QQmlType qtObjectType = QQmlMetaType::qmlType(&QObject::staticMetaObject);
if (!qtObjectType.isValid()) {
std::cerr << "Could not find QtObject type" << std::endl;
importCode = qtQmlImportString.toUtf8();
} else {
QString module = qtObjectType.qmlTypeName();
module = module.mid(0, module.lastIndexOf(QLatin1Char('/')));
importCode = QString("import %1 %2.%3").arg(
module, QString::number(qtObjectType.version().majorVersion()),
QString::number(qtObjectType.version().minorVersion())).toUtf8();
}
// avoid importing dependencies?
for (const QString &moduleToImport : std::as_const(dependencies)) {
importCode.append("\nimport ");
importCode.append(moduleToImport.toUtf8());
}
// find all QMetaObjects reachable when the specified module is imported
if (action != Path) {
importCode += QString("\nimport %0 %1\n").arg(pluginImportUri, pluginImportVersion).toLatin1();
} else {
// pluginImportVersion can be empty
importCode += QString("\nimport \".\" %2\n").arg(pluginImportVersion).toLatin1();
}
// create a component with these imports to make sure the imports are valid
// and to populate the declarative meta type system
{
QByteArray code = importCode;
code += "\nQtObject {}";
QQmlComponent c(&engine);
c.setData(code, QUrl::fromLocalFile(pluginImportPath + "/typelist.qml"));
c.create();
const auto errors = c.errors();
if (!errors.isEmpty()) {
for (const QQmlError &error : errors)
std::cerr << qPrintable( error.toString() ) << std::endl;
return EXIT_IMPORTERROR;
}
}
info = {pluginImportUri, version, strict};
QSet<const QMetaObject *> candidates = collectReachableMetaObjects(&engine, uncreatableMetas, singletonMetas, compositeTypes, info, defaultTypes);
for (auto it = compositeTypes.begin(), end = compositeTypes.end(); it != end; ++it) {
std::sort(it->begin(), it->end());
it->erase(std::unique(it->begin(), it->end()), it->end());
}
for (const QMetaObject *mo : std::as_const(candidates)) {
if (mo->className() != QLatin1String("Qt"))
metas.insert(mo);
}
}
// setup static rewrites of type names
cppToId.insert("QString", "string");
// start dumping data
QByteArray bytes;
QQmlJSStreamWriter qml(&bytes);
qml.writeStartDocument();
qml.writeLibraryImport("QtQuick.tooling", 1, 2);
qml.write("\n"
"// This file describes the plugin-supplied types contained in the library.\n"
"// It is used for QML tooling purposes only.\n"
"//\n"
"// This file was auto-generated by:\n"
"// '");
qml.write(QFileInfo(args.at(0)).baseName().toUtf8());
qml.write(" ");
qml.write(args.mid(1).join(QLatin1Char(' ')).toUtf8());
qml.write("'\n"
"//\n"
"// qmlplugindump is deprecated! You should use qmltyperegistrar instead.\n"
"\n");
qml.writeStartObject("Module");
// put the metaobjects into a map so they are always dumped in the same order
QMap<QString, const QMetaObject *> nameToMeta;
for (const QMetaObject *meta : std::as_const(metas))
nameToMeta.insert(convertToId(meta), meta);
Dumper dumper(&qml);
if (relocatable)
dumper.setRelocatableModuleUri(pluginImportUri);
for (const QMetaObject *meta : std::as_const(nameToMeta)) {
dumper.dump(QQmlEnginePrivate::get(&engine), meta, uncreatableMetas.contains(meta), singletonMetas.contains(meta));
}
QMap<QString, QList<QQmlType>>::const_iterator iter = compositeTypes.constBegin();
for (; iter != compositeTypes.constEnd(); ++iter)
dumper.dumpComposite(&engine, iter.value(), info);
// Insert merge file.
qml.write(mergeComponents);
qml.writeEndObject();
qml.writeEndDocument();
if (!outputFilename.isEmpty()) {
QFile file(outputFilename);
if (file.open(QIODevice::WriteOnly)) {
QTextStream stream(&file);
stream << bytes.constData();
}
} else {
std::cout << bytes.constData() << std::flush;
}
// workaround to avoid crashes on exit
QTimer timer;
timer.setSingleShot(true);
timer.setInterval(0);
QObject::connect(&timer, SIGNAL(timeout()), app.data(), SLOT(quit()));
timer.start();
return app->exec();
}