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

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