1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qqmlpluginimporter_p.h"
5#include "qqmlimport_p.h"
6
7#include <private/qqmlextensionplugin_p.h>
8#include <private/qqmltypeloader_p.h>
9#include <private/qqmlglobal_p.h>
10
11#include <QtCore/qobject.h>
12#include <QtCore/qpluginloader.h>
13#include <QtCore/qdir.h>
14#include <QtCore/qloggingcategory.h>
15#include <QtCore/qjsonarray.h>
16
17#include <unordered_map>
18
19QT_BEGIN_NAMESPACE
20
21struct QmlPlugin {
22 std::unique_ptr<QPluginLoader> loader;
23};
24
25class PluginMap
26{
27 Q_DISABLE_COPY_MOVE(PluginMap)
28public:
29 PluginMap() = default;
30 ~PluginMap() = default;
31
32 // This is a std::unordered_map because QHash cannot handle move-only types.
33 using Container = std::unordered_map<QString, QmlPlugin>;
34
35private:
36 QBasicMutex mutex;
37 Container plugins;
38 friend class PluginMapPtr;
39};
40
41class PluginMapPtr
42{
43 Q_DISABLE_COPY_MOVE(PluginMapPtr)
44public:
45 PluginMapPtr(PluginMap *map) : map(map), locker(&map->mutex) {}
46 ~PluginMapPtr() = default;
47
48 PluginMap::Container &operator*() { return map->plugins; }
49 const PluginMap::Container &operator*() const { return map->plugins; }
50
51 PluginMap::Container *operator->() { return &map->plugins; }
52 const PluginMap::Container *operator->() const { return &map->plugins; }
53
54private:
55 PluginMap *map;
56 QMutexLocker<QBasicMutex> locker;
57};
58
59Q_GLOBAL_STATIC(PluginMap, qmlPluginsById); // stores the uri and the PluginLoaders
60
61struct StaticPluginMapping
62{
63 QStaticPlugin plugin;
64 QString metadataURI;
65};
66
67struct VersionedURI
68{
69 QString id;
70 QTypeRevision version;
71};
72
73/*
74 * This function verifies if staticPlugin is valid QML plugin by
75 * verifying the IID member of Metadata and
76 * checks whether the plugin instance has the correct parent plugin extension class.
77 * Checks the presence of URI member of Metadata and returns QJsonArray,
78 * which in majority cases contains only 1 element.
79 */
80static QJsonArray tryExtractQmlPluginURIs(const QStaticPlugin &plugin)
81{
82 const auto isQmlPlugin = [](const QStaticPlugin &plugin, auto &&pluginMetadata) -> bool {
83 const QString iid = pluginMetadata.value(QLatin1String("IID")).toString();
84 const bool isQmlExtensionIID = iid == QLatin1String(QQmlEngineExtensionInterface_iid)
85 || iid == QLatin1String(QQmlExtensionInterface_iid)
86 || iid == QLatin1String(QQmlExtensionInterface_iid_old);
87 if (Q_UNLIKELY(iid == QLatin1String(QQmlExtensionInterface_iid_old))) {
88 qWarning() << QQmlImports::tr(
89 sourceText: "Found plugin with old IID, this will be unsupported in upcoming Qt "
90 "releases:")
91 << pluginMetadata;
92 }
93 if (!isQmlExtensionIID) {
94 return false;
95 }
96
97 // plugin.instance must not be called earlier to avoid instantiating
98 // non-QML plugins, potentially from an unexpected thread(typeloader vs
99 // main thread)
100 const auto *pluginInstance = plugin.instance();
101 return qobject_cast<const QQmlEngineExtensionPlugin *>(object: pluginInstance)
102 || qobject_cast<const QQmlExtensionPlugin *>(object: pluginInstance);
103 };
104
105 const auto &pluginMetadata = plugin.metaData();
106 if (!isQmlPlugin(plugin, pluginMetadata)) {
107 return {};
108 }
109
110 const QJsonArray metadataUriList = pluginMetadata.value(QStringLiteral("uri")).toArray();
111 if (metadataUriList.isEmpty()) {
112 qWarning() << QQmlImports::tr(sourceText: "qml static plugin with name \"%2\" has no metadata URI")
113 .arg(a: plugin.instance()->metaObject()->className())
114 << pluginMetadata;
115 return {};
116 }
117 return metadataUriList;
118}
119
120static QVector<StaticPluginMapping> staticQmlPlugins()
121{
122 QVector<StaticPluginMapping> qmlPlugins;
123 const auto staticPlugins = QPluginLoader::staticPlugins();
124 qmlPlugins.reserve(asize: staticPlugins.size());
125
126 for (const auto &plugin : staticPlugins) {
127 // Filter out static plugins which are not Qml plugins
128 for (const QJsonValueConstRef &pluginURI : tryExtractQmlPluginURIs(plugin)) {
129 qmlPlugins.append(t: { .plugin: plugin, .metadataURI: pluginURI.toString() });
130 }
131 }
132 return qmlPlugins;
133}
134
135/*
136 Returns the list of possible versioned URI combinations. For example, if \a uri is
137 (id = QtQml.Models, (vmaj = 2, vmin = 0)), this method returns the following:
138 [QtQml.Models.2.0, QtQml.2.0.Models, QtQml.Models.2, QtQml.2.Models, QtQml.Models]
139 */
140static QStringList versionUriList(const VersionedURI &uri)
141{
142 QStringList result;
143 for (int mode = QQmlImports::FullyVersioned; mode <= QQmlImports::Unversioned; ++mode) {
144 int index = uri.id.size();
145 do {
146 QString versionUri = uri.id;
147 versionUri.insert(
148 i: index,
149 s: QQmlImports::versionString(version: uri.version, importVersion: QQmlImports::ImportVersion(mode)));
150 result += versionUri;
151
152 index = uri.id.lastIndexOf(ch: u'.', from: index - 1);
153 } while (index > 0 && mode != QQmlImports::Unversioned);
154 }
155 return result;
156}
157
158/*
159 Get all static plugins that are QML plugins and has a meta data URI that matches with one of
160 \a versionUris, which is a list of all possible versioned URI combinations - see
161 versionUriList() above.
162 */
163static QVector<StaticPluginMapping> staticQmlPluginsMatchingURI(const VersionedURI &uri)
164{
165 static const auto qmlPlugins = staticQmlPlugins();
166
167 // Since plugin metadata URIs can be anything from
168 // fully versioned to unversioned, we need to compare with differnt version strings.
169 // If a module has several plugins, they must all have the same version. Start by
170 // populating pluginPairs with relevant plugins to cut the list short early on:
171 const QStringList versionedURIs = versionUriList(uri);
172 QVector<StaticPluginMapping> matches;
173 std::copy_if(first: qmlPlugins.begin(), last: qmlPlugins.end(), result: std::back_inserter(x&: matches),
174 pred: [&](const auto &pluginMapping) {
175 return versionedURIs.contains(pluginMapping.metadataURI);
176 });
177 return matches;
178}
179
180static bool unloadPlugin(const std::pair<const QString, QmlPlugin> &plugin)
181{
182 const auto &loader = plugin.second.loader;
183 if (!loader)
184 return false;
185
186#if QT_CONFIG(library)
187 if (auto extensionPlugin = qobject_cast<QQmlExtensionPlugin *>(object: loader->instance()))
188 extensionPlugin->unregisterTypes();
189
190# ifndef Q_OS_MACOS
191 if (!loader->unload()) {
192 qWarning(msg: "Unloading %s failed: %s", qPrintable(plugin.first),
193 qPrintable(loader->errorString()));
194 return false;
195 }
196# endif
197#endif
198
199 return true;
200}
201
202/*!
203 \internal
204*/
205static QTypeRevision lockModule(const QString &uri, const QString &typeNamespace,
206 QTypeRevision version, QList<QQmlError> *errors)
207{
208 if (!version.hasMajorVersion()) {
209 version = QQmlMetaType::latestModuleVersion(uri);
210 if (!version.isValid())
211 errors->prepend(t: QQmlImports::moduleNotFoundError(uri, version));
212 }
213 if (version.hasMajorVersion() && !typeNamespace.isEmpty()
214 && !QQmlMetaType::protectModule(uri, version, weakProtectAllVersions: true)) {
215 // Not being able to protect the module means there are not types registered for it,
216 // means the plugin we loaded didn't provide any, means we didn't find the module.
217 // We output the generic error message as depending on the load order of imports we may
218 // hit this path or another one that only figures "plugin is already loaded but module
219 // unavailable" and doesn't try to protect it anymore.
220 errors->prepend(t: QQmlImports::moduleNotFoundError(uri, version));
221 return QTypeRevision();
222 }
223
224 return version;
225}
226
227
228void qmlClearEnginePlugins()
229{
230 PluginMapPtr plugins(qmlPluginsById());
231 for (const auto &plugin : std::as_const(t&: *plugins))
232 unloadPlugin(plugin);
233 plugins->clear();
234}
235
236bool QQmlPluginImporter::removePlugin(const QString &pluginId)
237{
238 PluginMapPtr plugins(qmlPluginsById());
239
240 auto it = plugins->find(x: pluginId);
241 if (it == plugins->end())
242 return false;
243
244 const bool success = unloadPlugin(plugin: *it);
245
246 plugins->erase(position: it);
247 return success;
248}
249
250QStringList QQmlPluginImporter::plugins()
251{
252 PluginMapPtr plugins(qmlPluginsById());
253 QStringList results;
254 for (auto it = plugins->cbegin(), end = plugins->cend(); it != end; ++it) {
255 if (it->second.loader != nullptr)
256 results.append(t: it->first);
257 }
258 return results;
259}
260
261QString QQmlPluginImporter::truncateToDirectory(const QString &qmldirFilePath)
262{
263 const int slash = qmldirFilePath.lastIndexOf(c: u'/');
264 return slash > 0 ? qmldirFilePath.left(n: slash) : qmldirFilePath;
265}
266
267void QQmlPluginImporter::finalizePlugin(QObject *instance, const QString &pluginId) {
268 // The plugin's per-engine initialization does not need lock protection, as this function is
269 // only called from the engine specific loader thread and importDynamicPlugin as well as
270 // importStaticPlugin are the only places of access.
271
272 typeLoader->setPluginInitialized(pluginId);
273 if (auto *extensionIface = qobject_cast<QQmlExtensionInterface *>(object: instance))
274 typeLoader->initializeEngine(extensionIface, uri.toUtf8().constData());
275 else if (auto *engineIface = qobject_cast<QQmlEngineExtensionInterface *>(object: instance))
276 typeLoader->initializeEngine(engineIface, uri.toUtf8().constData());
277}
278
279QTypeRevision QQmlPluginImporter::importStaticPlugin(QObject *instance, const QString &pluginId) {
280 // Dynamic plugins are differentiated by their filepath. For static plugins we
281 // don't have that information so we use their address as key instead.
282 QTypeRevision importVersion = version;
283 {
284 PluginMapPtr plugins(qmlPluginsById());
285
286 // Plugin types are global across all engines and should only be
287 // registered once. But each engine still needs to be initialized.
288 bool typesRegistered = plugins->find(x: pluginId) != plugins->end();
289
290 if (!typesRegistered) {
291 plugins->insert(x: std::make_pair(x: pluginId, y: QmlPlugin()));
292 if (QQmlMetaType::registerPluginTypes(
293 instance, basePath: QFileInfo(qmldirPath).absoluteFilePath(), uri,
294 typeNamespace: qmldir->typeNamespace(), version: importVersion, errors)
295 == QQmlMetaType::RegistrationResult::Failure) {
296 return QTypeRevision();
297 }
298
299 importVersion = lockModule(uri, typeNamespace: qmldir->typeNamespace(), version: importVersion, errors);
300 if (!importVersion.isValid())
301 return QTypeRevision();
302 }
303
304 // Release the lock on plugins early as we're done with the global part. Releasing the lock
305 // also allows other QML loader threads to acquire the lock while this thread is blocking
306 // in the initializeEngine call to the gui thread (which in turn may be busy waiting for
307 // other QML loader threads and thus not process the initializeEngine call).
308 }
309
310 if (!typeLoader->isPluginInitialized(plugin: pluginId))
311 finalizePlugin(instance, pluginId);
312
313 return QQmlImports::validVersion(version: importVersion);
314}
315
316QTypeRevision QQmlPluginImporter::importDynamicPlugin(
317 const QString &filePath, const QString &pluginId, bool optional)
318{
319 QObject *instance = nullptr;
320 QTypeRevision importVersion = version;
321
322 const bool engineInitialized = typeLoader->isPluginInitialized(plugin: pluginId);
323 {
324 PluginMapPtr plugins(qmlPluginsById());
325 bool typesRegistered = plugins->find(x: pluginId) != plugins->end();
326
327 if (!engineInitialized || !typesRegistered) {
328 const QFileInfo fileInfo(filePath);
329 if (!typesRegistered && optional) {
330 switch (QQmlMetaType::registerPluginTypes(
331 instance: nullptr, basePath: fileInfo.absolutePath(), uri, typeNamespace: qmldir->typeNamespace(),
332 version: importVersion, errors)) {
333 case QQmlMetaType::RegistrationResult::NoRegistrationFunction:
334 // try again with plugin
335 break;
336 case QQmlMetaType::RegistrationResult::Success:
337 importVersion = lockModule(uri, typeNamespace: qmldir->typeNamespace(), version: importVersion, errors);
338 if (!importVersion.isValid())
339 return QTypeRevision();
340 // instance and loader intentionally left at nullptr
341 plugins->insert(x: std::make_pair(x: pluginId, y: QmlPlugin()));
342 // Not calling initializeEngine with null instance
343 typeLoader->setPluginInitialized(pluginId);
344 return importVersion;
345 case QQmlMetaType::RegistrationResult::Failure:
346 return QTypeRevision();
347 }
348 }
349
350#if QT_CONFIG(library)
351 if (!typesRegistered) {
352
353 // Check original filePath. If that one is empty, not being able
354 // to load the plugin is not an error. We were just checking if
355 // the types are already available. absoluteFilePath can still be
356 // empty if filePath is not.
357 if (filePath.isEmpty())
358 return QTypeRevision();
359
360 const QString absoluteFilePath = fileInfo.absoluteFilePath();
361 if (!QQml_isFileCaseCorrect(fileName: absoluteFilePath)) {
362 if (errors) {
363 QQmlError error;
364 error.setDescription(
365 QQmlImports::tr(sourceText: "File name case mismatch for \"%1\"")
366 .arg(a: absoluteFilePath));
367 errors->prepend(t: error);
368 }
369 return QTypeRevision();
370 }
371
372 QmlPlugin plugin;
373 plugin.loader = std::make_unique<QPluginLoader>(args: absoluteFilePath);
374 if (!plugin.loader->load()) {
375 if (errors) {
376 QQmlError error;
377 error.setDescription(plugin.loader->errorString());
378 errors->prepend(t: error);
379 }
380 return QTypeRevision();
381 }
382
383 instance = plugin.loader->instance();
384 plugins->insert(x: std::make_pair(x: pluginId, y: std::move(plugin)));
385
386 // Continue with shared code path for dynamic and static plugins:
387 if (QQmlMetaType::registerPluginTypes(
388 instance, basePath: fileInfo.absolutePath(), uri, typeNamespace: qmldir->typeNamespace(),
389 version: importVersion, errors)
390 == QQmlMetaType::RegistrationResult::Failure) {
391 return QTypeRevision();
392 }
393
394 importVersion = lockModule(uri, typeNamespace: qmldir->typeNamespace(), version: importVersion, errors);
395 if (!importVersion.isValid())
396 return QTypeRevision();
397 } else {
398 auto it = plugins->find(x: pluginId);
399 if (it != plugins->end() && it->second.loader)
400 instance = it->second.loader->instance();
401 }
402#else
403 // Here plugin is not optional and NOT QT_CONFIG(library)
404 // Cannot finalize such plugin and return valid, because no types are registered.
405 // Just return invalid.
406 if (!optional)
407 return QTypeRevision();
408#endif // QT_CONFIG(library)
409 }
410
411 // Release the lock on plugins early as we're done with the global part. Releasing the lock
412 // also allows other QML loader threads to acquire the lock while this thread is blocking
413 // in the initializeEngine call to the gui thread (which in turn may be busy waiting for
414 // other QML loader threads and thus not process the initializeEngine call).
415 }
416
417 if (!engineInitialized)
418 finalizePlugin(instance, pluginId);
419
420 return QQmlImports::validVersion(version: importVersion);
421}
422
423/*!
424 \internal
425
426 Searches for a plugin called \a baseName in \a qmldirPluginPath, taking the
427 path of the qmldir file itself, and the plugin paths of the QQmlTypeLoader
428 into account.
429
430 The baseName is amended with a platform-dependent prefix and suffix to
431 construct the final plugin file name:
432
433 \table
434 \header \li Platform \li Prefix \li Valid suffixes
435 \row \li Windows \li \li \c .dll, \c .d.dll
436 \row \li Unix/Linux \li lib \li \c .so
437 \row \li \macos \li lib \li \c .dylib, \c _debug.dylib \c .bundle, \c .so
438 \row \li Android \li lib \li \c .so, \c _<ABI>.so
439 \endtable
440
441 If the \a qmldirPluginPath is absolute, it is searched first. Then each of the
442 filePluginPath entries in the QQmlTypeLoader is checked in turn. If the
443 entry is relative, it is resolved on top of the path of the qmldir file,
444 otherwise it is taken verbatim. If a "." is found in the filePluginPath, and
445 \a qmldirPluginPath is relative, then \a qmldirPluginPath is used in its
446 place.
447
448 TODO: Document the android special casing.
449
450 TODO: The above paragraph, as well as the code implementing it makes very
451 little sense and is mostly here for backwards compatibility.
452 */
453QString QQmlPluginImporter::resolvePlugin(const QString &qmldirPluginPath, const QString &baseName)
454{
455#if defined(Q_OS_WIN)
456 static const QString prefix;
457 static const QStringList suffixes = {
458 # ifdef QT_DEBUG
459 QLatin1String("d.dll"), // try a qmake-style debug build first
460 QLatin1String(".dll")
461 #else
462 QLatin1String(".dll"),
463 QLatin1String("d.dll") // try a qmake-style debug build after
464 # endif
465 };
466#elif defined(Q_OS_DARWIN)
467 static const QString prefix = QLatin1String("lib");
468 static const QStringList suffixes = {
469 # ifdef QT_DEBUG
470 QLatin1String("_debug.dylib"), // try a qmake-style debug build first
471 QLatin1String(".dylib"),
472 # else
473 QLatin1String(".dylib"),
474 QLatin1String("_debug.dylib"), // try a qmake-style debug build after
475 # endif
476 QLatin1String(".so"),
477 QLatin1String(".bundle")
478 };
479#else // Unix
480 static const QString prefix = QLatin1String("lib");
481 static const QStringList suffixes = {
482 # if defined(Q_OS_ANDROID)
483 QStringLiteral(LIBS_SUFFIX),
484 # endif
485 QLatin1String(".so")
486 };
487#endif
488
489 QStringList searchPaths = typeLoader->pluginPathList();
490 bool qmldirPluginPathIsRelative = QDir::isRelativePath(path: qmldirPluginPath);
491 if (!qmldirPluginPathIsRelative)
492 searchPaths.prepend(t: qmldirPluginPath);
493
494 for (const QString &pluginPath : std::as_const(t&: searchPaths)) {
495 QString resolvedBasePath;
496 if (pluginPath == QLatin1String(".")) {
497 if (qmldirPluginPathIsRelative && !qmldirPluginPath.isEmpty()
498 && qmldirPluginPath != QLatin1String(".")) {
499 resolvedBasePath = QDir::cleanPath(path: qmldirPath + u'/' + qmldirPluginPath);
500 } else {
501 resolvedBasePath = qmldirPath;
502 }
503 } else {
504 if (QDir::isRelativePath(path: pluginPath))
505 resolvedBasePath = QDir::cleanPath(path: qmldirPath + u'/' + pluginPath);
506 else
507 resolvedBasePath = pluginPath;
508 }
509
510 // hack for resources, should probably go away
511 if (resolvedBasePath.startsWith(c: u':'))
512 resolvedBasePath = QCoreApplication::applicationDirPath();
513
514 if (!resolvedBasePath.endsWith(c: u'/'))
515 resolvedBasePath += u'/';
516
517 QString resolvedPath = resolvedBasePath + prefix + baseName;
518 for (const QString &suffix : suffixes) {
519 QString absolutePath = typeLoader->absoluteFilePath(path: resolvedPath + suffix);
520 if (!absolutePath.isEmpty())
521 return absolutePath;
522 }
523
524#if defined(Q_OS_ANDROID)
525 if (qmldirPath.size() > 25 && qmldirPath.at(0) == QLatin1Char(':')
526 && qmldirPath.at(1) == QLatin1Char('/')
527 && qmldirPath.startsWith(QStringLiteral(":/android_rcc_bundle/qml/"),
528 Qt::CaseInsensitive)) {
529 QString pluginName = qmldirPath.mid(21) + u'/' + baseName;
530 pluginName.replace(QLatin1Char('/'), QLatin1Char('_'));
531 QString bundledPath = resolvedBasePath + QLatin1String("lib") + pluginName;
532 for (const QString &suffix : suffixes) {
533 const QString absolutePath = typeLoader->absoluteFilePath(bundledPath + suffix);
534 if (!absolutePath.isEmpty()) {
535 qWarning("The implicit resolving of Qml plugin locations using the URI "
536 "embedded in the filename has been deprecated. Please use the "
537 "modern CMake API to create QML modules or set the name of "
538 "QML plugin in qmldir file, that matches the name of plugin "
539 "on file system. The correct plugin name is '%s'.",
540 qPrintable(pluginName));
541 return absolutePath;
542 }
543 }
544 }
545#endif
546 }
547
548 qCDebug(lcQmlImport) << "resolvePlugin" << "Could not resolve dynamic plugin with base name"
549 << baseName << "in" << qmldirPath
550 << " file does not exist";
551
552 return QString();
553}
554
555QTypeRevision QQmlPluginImporter::importPlugins() {
556 const auto qmldirPlugins = qmldir->plugins();
557 const int qmldirPluginCount = qmldirPlugins.size();
558 QTypeRevision importVersion = version;
559
560 // If the path contains a version marker or if we have more than one plugin,
561 // we need to use paths. In that case we cannot fall back to other instances
562 // of the same module if a qmldir is rejected. However, as we don't generate
563 // such modules, it shouldn't be a problem.
564 const bool canUseUris = qmldirPluginCount == 1
565 && qmldirPath.endsWith(s: u'/' + QString(uri).replace(before: u'.', after: u'/'));
566 const QString moduleId = canUseUris ? uri : qmldir->qmldirLocation();
567
568 if (typeLoader->isModulePluginProcessingDone(module: moduleId)) {
569 return QQmlImports::validVersion(version: importVersion);
570 }
571
572 // First search for listed qmldir plugins dynamically. If we cannot resolve them all, we
573 // continue searching static plugins that has correct metadata uri. Note that since we
574 // only know the uri for a static plugin, and not the filename, we cannot know which
575 // static plugin belongs to which listed plugin inside qmldir. And for this reason,
576 // mixing dynamic and static plugins inside a single module is not recommended.
577
578 int dynamicPluginsFound = 0;
579 int staticPluginsLoaded = 0;
580
581 for (const QQmlDirParser::Plugin &plugin : qmldirPlugins) {
582 const QString resolvedFilePath = resolvePlugin(qmldirPluginPath: plugin.path, baseName: plugin.name);
583
584 if (!canUseUris && resolvedFilePath.isEmpty())
585 continue;
586
587 importVersion = importDynamicPlugin(
588 filePath: resolvedFilePath, pluginId: canUseUris ? uri : QFileInfo(resolvedFilePath).absoluteFilePath(),
589 optional: plugin.optional);
590 if (importVersion.isValid())
591 ++dynamicPluginsFound;
592 else if (!resolvedFilePath.isEmpty())
593 return QTypeRevision();
594 }
595
596 if (dynamicPluginsFound < qmldirPluginCount) {
597 // Check if the missing plugins can be resolved statically. We do this by looking at
598 // the URIs embedded in a plugins meta data.
599 const auto pluginPairs = staticQmlPluginsMatchingURI(uri: { .id: uri, .version: importVersion });
600
601 for (const auto &pluginWithURI : std::as_const(t: pluginPairs)) {
602 QObject *instance = pluginWithURI.plugin.instance();
603 importVersion = importStaticPlugin(
604 instance, pluginId: canUseUris ? uri : QString::asprintf(format: "%p", instance));
605 if (!importVersion.isValid()) {
606 continue;
607 }
608 staticPluginsLoaded++;
609 qCDebug(lcQmlImport) << "importExtension" << "loaded static plugin "
610 << pluginWithURI.metadataURI;
611 }
612 if (!pluginPairs.empty() && staticPluginsLoaded == 0) {
613 // found matched plugins, but failed to load, early exit to preserve the error
614 return QTypeRevision();
615 }
616 }
617
618 if ((dynamicPluginsFound + staticPluginsLoaded) < qmldirPluginCount) {
619 if (errors) {
620 QQmlError error;
621 if (qmldirPluginCount > 1 && staticPluginsLoaded > 0) {
622 error.setDescription(
623 QQmlImports::tr(sourceText: "could not resolve all plugins for module \"%1\"")
624 .arg(a: uri));
625 } else {
626 error.setDescription(QQmlImports::tr(sourceText: "module \"%1\" plugin \"%2\" not found")
627 .arg(args: uri, args: qmldirPlugins[dynamicPluginsFound].name));
628 }
629 error.setUrl(QUrl::fromLocalFile(localfile: qmldir->qmldirLocation()));
630 errors->prepend(t: error);
631 }
632 return QTypeRevision();
633 }
634
635 typeLoader->setModulePluginProcessingDone(moduleId);
636
637 return QQmlImports::validVersion(version: importVersion);
638}
639
640QT_END_NAMESPACE
641

source code of qtdeclarative/src/qml/qml/qqmlpluginimporter.cpp