aboutsummaryrefslogtreecommitdiffstats
path: root/tools/qmlimportscanner/main.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tools/qmlimportscanner/main.cpp')
-rw-r--r--tools/qmlimportscanner/main.cpp553
1 files changed, 444 insertions, 109 deletions
diff --git a/tools/qmlimportscanner/main.cpp b/tools/qmlimportscanner/main.cpp
index ea17df921f..7c8f1bc98e 100644
--- a/tools/qmlimportscanner/main.cpp
+++ b/tools/qmlimportscanner/main.cpp
@@ -1,61 +1,47 @@
-/****************************************************************************
-**
-** Copyright (C) 2016 The Qt Company Ltd.
-** Contact: https://www.qt.io/licensing/
-**
-** This file is part of the tools applications of the Qt Toolkit.
-**
-** $QT_BEGIN_LICENSE:GPL-EXCEPT$
-** Commercial License Usage
-** Licensees holding valid commercial Qt licenses may use this file in
-** accordance with the commercial license agreement provided with the
-** Software or, alternatively, in accordance with the terms contained in
-** a written agreement between you and The Qt Company. For licensing terms
-** and conditions see https://www.qt.io/terms-conditions. For further
-** information use the contact form at https://www.qt.io/contact-us.
-**
-** GNU General Public License Usage
-** Alternatively, this file may be used under the terms of the GNU
-** General Public License version 3 as published by the Free Software
-** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
-** included in the packaging of this file. Please review the following
-** information to ensure the GNU General Public License requirements will
-** be met: https://www.gnu.org/licenses/gpl-3.0.html.
-**
-** $QT_END_LICENSE$
-**
-****************************************************************************/
+// 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 <private/qqmljslexer_p.h>
#include <private/qqmljsparser_p.h>
#include <private/qqmljsast_p.h>
-#include <private/qv4codegen_p.h>
-#include <private/qv4staticvalue_p.h>
-#include <private/qqmlirbuilder_p.h>
#include <private/qqmljsdiagnosticmessage_p.h>
#include <private/qqmldirparser_p.h>
#include <private/qqmljsresourcefilemapper_p.h>
#include <QtCore/QCoreApplication>
+#include <QtCore/QDebug>
+#include <QtCore/QDateTime>
#include <QtCore/QDir>
#include <QtCore/QDirIterator>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
+#include <QtCore/QHash>
#include <QtCore/QSet>
#include <QtCore/QStringList>
#include <QtCore/QMetaObject>
#include <QtCore/QMetaProperty>
#include <QtCore/QVariant>
+#include <QtCore/QVariantMap>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QLibraryInfo>
+#include <QtCore/QLoggingCategory>
#include <iostream>
#include <algorithm>
+#include <unordered_map>
+#include <unordered_set>
QT_USE_NAMESPACE
+using namespace Qt::StringLiterals;
+
+Q_LOGGING_CATEGORY(lcImportScanner, "qt.qml.import.scanner");
+Q_LOGGING_CATEGORY(lcImportScannerFiles, "qt.qml.import.scanner.files");
+
+using FileImportsWithoutDepsCache = QHash<QString, QVariantList>;
+
namespace {
QStringList g_qmlImportPaths;
@@ -65,27 +51,31 @@ inline QString versionLiteral() { return QStringLiteral("version"); }
inline QString nameLiteral() { return QStringLiteral("name"); }
inline QString relativePathLiteral() { return QStringLiteral("relativePath"); }
inline QString pluginsLiteral() { return QStringLiteral("plugins"); }
+inline QString pluginIsOptionalLiteral() { return QStringLiteral("pluginIsOptional"); }
inline QString pathLiteral() { return QStringLiteral("path"); }
inline QString classnamesLiteral() { return QStringLiteral("classnames"); }
inline QString dependenciesLiteral() { return QStringLiteral("dependencies"); }
inline QString moduleLiteral() { return QStringLiteral("module"); }
inline QString javascriptLiteral() { return QStringLiteral("javascript"); }
inline QString directoryLiteral() { return QStringLiteral("directory"); }
+inline QString linkTargetLiteral()
+{
+ return QStringLiteral("linkTarget");
+}
+inline QString componentsLiteral() { return QStringLiteral("components"); }
+inline QString scriptsLiteral() { return QStringLiteral("scripts"); }
+inline QString preferLiteral() { return QStringLiteral("prefer"); }
void printUsage(const QString &appNameIn)
{
- const std::wstring appName = appNameIn.toStdWString();
-#ifndef QT_BOOTSTRAPPED
+ const std::string appName = appNameIn.toStdString();
const QString qmlPath = QLibraryInfo::path(QLibraryInfo::QmlImportsPath);
-#else
- const QString qmlPath = QStringLiteral("/home/user/dev/qt-install/qml");
-#endif
- std::wcerr
+ std::cerr
<< "Usage: " << appName << " -rootPath path/to/app/qml/directory -importPath path/to/qt/qml/directory\n"
" " << appName << " -qmlFiles file1 file2 -importPath path/to/qt/qml/directory\n"
" " << appName << " -qrcFiles file1.qrc file2.qrc -importPath path/to/qt/qml/directory\n\n"
"Example: " << appName << " -rootPath . -importPath "
- << QDir::toNativeSeparators(qmlPath).toStdWString()
+ << QDir::toNativeSeparators(qmlPath).toStdString()
<< '\n';
}
@@ -142,8 +132,9 @@ QVariantList findImportsInAst(QQmlJS::AST::UiHeaderItemList *headerItemList, con
return imports;
}
-QVariantList findQmlImportsInQmlFile(const QString &filePath);
-QVariantList findQmlImportsInJavascriptFile(const QString &filePath);
+QVariantList findQmlImportsInFileWithoutDeps(const QString &filePath,
+ FileImportsWithoutDepsCache
+ &fileImportsWithoutDepsCache);
static QString versionSuffix(QTypeRevision version)
{
@@ -153,7 +144,18 @@ static QString versionSuffix(QTypeRevision version)
// Read the qmldir file, extract a list of plugins by
// parsing the "plugin", "import", and "classname" directives.
-QVariantMap pluginsForModulePath(const QString &modulePath, const QString &version) {
+QVariantMap pluginsForModulePath(const QString &modulePath,
+ const QString &version,
+ FileImportsWithoutDepsCache
+ &fileImportsWithoutDepsCache) {
+ using Cache = QHash<QPair<QString, QString>, QVariantMap>;
+ static Cache pluginsCache;
+ const QPair<QString, QString> cacheKey = std::make_pair(modulePath, version);
+ const Cache::const_iterator it = pluginsCache.find(cacheKey);
+ if (it != pluginsCache.end()) {
+ return *it;
+ }
+
QFile qmldirFile(modulePath + QLatin1String("/qmldir"));
if (!qmldirFile.exists()) {
qWarning() << "qmldir file not found at" << modulePath;
@@ -177,11 +179,27 @@ QVariantMap pluginsForModulePath(const QString &modulePath, const QString &versi
QVariantMap pluginInfo;
QStringList pluginNameList;
+ bool isOptional = false;
const auto plugins = parser.plugins();
- for (const auto &plugin : plugins)
+ for (const auto &plugin : plugins) {
pluginNameList.append(plugin.name);
+ isOptional = plugin.optional;
+ }
pluginInfo[pluginsLiteral()] = pluginNameList.join(QLatin1Char(' '));
+ if (plugins.size() > 1) {
+ qWarning() << QStringLiteral("Warning: \"%1\" contains multiple plugin entries. This is discouraged and does not support marking plugins as optional.").arg(modulePath);
+ isOptional = false;
+ }
+
+ if (isOptional) {
+ pluginInfo[pluginIsOptionalLiteral()] = true;
+ }
+
+ if (!parser.linkTarget().isEmpty()) {
+ pluginInfo[linkTargetLiteral()] = parser.linkTarget();
+ }
+
pluginInfo[classnamesLiteral()] = parser.classNames().join(QLatin1Char(' '));
QStringList importsAndDependencies;
@@ -203,15 +221,23 @@ QVariantMap pluginsForModulePath(const QString &modulePath, const QString &versi
}
QVariantList importsFromFiles;
+ QStringList componentFiles;
+ QStringList scriptFiles;
const auto components = parser.components();
for (const auto &component : components) {
+ const QString componentFullPath = modulePath + QLatin1Char('/') + component.fileName;
+ componentFiles.append(componentFullPath);
importsFromFiles
- += findQmlImportsInQmlFile(modulePath + QLatin1Char('/') + component.fileName);
+ += findQmlImportsInFileWithoutDeps(componentFullPath,
+ fileImportsWithoutDepsCache);
}
const auto scripts = parser.scripts();
for (const auto &script : scripts) {
+ const QString scriptFullPath = modulePath + QLatin1Char('/') + script.fileName;
+ scriptFiles.append(scriptFullPath);
importsFromFiles
- += findQmlImportsInJavascriptFile(modulePath + QLatin1Char('/') + script.fileName);
+ += findQmlImportsInFileWithoutDeps(scriptFullPath,
+ fileImportsWithoutDepsCache);
}
for (const QVariant &import : importsFromFiles) {
@@ -224,8 +250,23 @@ QVariantMap pluginsForModulePath(const QString &modulePath, const QString &versi
version.isEmpty() ? name : (name + QLatin1Char(' ') + version));
}
- if (!importsAndDependencies.isEmpty())
+ if (!importsAndDependencies.isEmpty()) {
+ importsAndDependencies.removeDuplicates();
pluginInfo[dependenciesLiteral()] = importsAndDependencies;
+ }
+ if (!componentFiles.isEmpty()) {
+ componentFiles.sort();
+ pluginInfo[componentsLiteral()] = componentFiles;
+ }
+ if (!scriptFiles.isEmpty()) {
+ scriptFiles.sort();
+ pluginInfo[scriptsLiteral()] = scriptFiles;
+ }
+
+ if (!parser.preferredPath().isEmpty())
+ pluginInfo[preferLiteral()] = parser.preferredPath();
+
+ pluginsCache.insert(cacheKey, pluginInfo);
return pluginInfo;
}
@@ -238,8 +279,9 @@ QPair<QString, QString> resolveImportPath(const QString &uri, const QString &ver
const QStringList parts = uri.split(dot, Qt::SkipEmptyParts);
QString ver = version;
+ QPair<QString, QString> candidate;
while (true) {
- for (const QString &qmlImportPath : qAsConst(g_qmlImportPaths)) {
+ for (const QString &qmlImportPath : std::as_const(g_qmlImportPaths)) {
// Search for the most specific version first, and search
// also for the version in parent modules. For example:
// - qml/QtQml/Models.2.0
@@ -252,17 +294,30 @@ QPair<QString, QString> resolveImportPath(const QString &uri, const QString &ver
if (relativePath.endsWith(slash))
relativePath.chop(1);
const QString candidatePath = QDir::cleanPath(qmlImportPath + slash + relativePath);
- if (QDir(candidatePath).exists())
- return qMakePair(candidatePath, relativePath); // import found
+ const QDir candidateDir(candidatePath);
+ if (candidateDir.exists()) {
+ const auto newCandidate = qMakePair(candidatePath, relativePath); // import found
+ if (candidateDir.exists(u"qmldir"_s)) // if it has a qmldir, we are fine
+ return newCandidate;
+ else if (candidate.first.isEmpty())
+ candidate = newCandidate;
+ // otherwise we keep looking if we can find the module again (with a qmldir this time)
+ }
} else {
- for (int index = parts.count() - 1; index >= 0; --index) {
+ for (int index = parts.size() - 1; index >= 0; --index) {
QString relativePath = parts.mid(0, index + 1).join(slash)
+ dot + ver + slash + parts.mid(index + 1).join(slash);
if (relativePath.endsWith(slash))
relativePath.chop(1);
const QString candidatePath = QDir::cleanPath(qmlImportPath + slash + relativePath);
- if (QDir(candidatePath).exists())
- return qMakePair(candidatePath, relativePath); // import found
+ const QDir candidateDir(candidatePath);
+ if (candidateDir.exists()) {
+ const auto newCandidate = qMakePair(candidatePath, relativePath); // import found
+ if (candidateDir.exists(u"qmldir"_s))
+ return newCandidate;
+ else if (candidate.first.isEmpty())
+ candidate = newCandidate;
+ }
}
}
}
@@ -278,58 +333,201 @@ QPair<QString, QString> resolveImportPath(const QString &uri, const QString &ver
ver = ver.mid(0, lastDot);
}
- return QPair<QString, QString>(); // not found
+ return candidate;
}
-// Find absolute file system paths and plugins for a list of modules.
-QVariantList findPathsForModuleImports(const QVariantList &imports)
+// Provides a hasher for module details stored in a QVariantMap disguised as a QVariant..
+// Only supports a subset of types.
+struct ImportVariantHasher {
+ std::size_t operator()(const QVariant &importVariant) const
+ {
+ size_t computedHash = 0;
+ QVariantMap importMap = qvariant_cast<QVariantMap>(importVariant);
+ for (auto it = importMap.constKeyValueBegin(); it != importMap.constKeyValueEnd(); ++it) {
+ const QString &key = it->first;
+ const QVariant &value = it->second;
+
+ if (!value.isValid() || value.isNull()) {
+ computedHash = qHashMulti(computedHash, key, 0);
+ continue;
+ }
+
+ const auto valueTypeId = value.typeId();
+ switch (valueTypeId) {
+ case QMetaType::QString:
+ computedHash = qHashMulti(computedHash, key, value.toString());
+ break;
+ case QMetaType::Bool:
+ computedHash = qHashMulti(computedHash, key, value.toBool());
+ break;
+ case QMetaType::QStringList:
+ computedHash = qHashMulti(computedHash, key, value.toStringList());
+ break;
+ default:
+ Q_ASSERT_X(valueTypeId, "ImportVariantHasher", "Invalid variant type detected");
+ break;
+ }
+ }
+
+ return computedHash;
+ }
+};
+
+using ImportDetailsAndDeps = QPair<QVariantMap, QStringList>;
+
+// Returns the import information as it will be written out to the json / .cmake file.
+// The dependencies are not stored in the same QVariantMap because we don't currently need that
+// information in the output file.
+ImportDetailsAndDeps
+getImportDetails(const QVariant &inputImport,
+ FileImportsWithoutDepsCache &fileImportsWithoutDepsCache) {
+
+ using Cache = std::unordered_map<QVariant, ImportDetailsAndDeps, ImportVariantHasher>;
+ static Cache cache;
+
+ const Cache::const_iterator it = cache.find(inputImport);
+ if (it != cache.end()) {
+ return it->second;
+ }
+
+ QVariantMap import = qvariant_cast<QVariantMap>(inputImport);
+ QStringList dependencies;
+ if (import.value(typeLiteral()) == moduleLiteral()) {
+ const QString version = import.value(versionLiteral()).toString();
+ const QPair<QString, QString> paths =
+ resolveImportPath(import.value(nameLiteral()).toString(), version);
+ QVariantMap plugininfo;
+ if (!paths.first.isEmpty()) {
+ import.insert(pathLiteral(), paths.first);
+ import.insert(relativePathLiteral(), paths.second);
+ plugininfo = pluginsForModulePath(paths.first,
+ version,
+ fileImportsWithoutDepsCache);
+ }
+ QString linkTarget = plugininfo.value(linkTargetLiteral()).toString();
+ QString plugins = plugininfo.value(pluginsLiteral()).toString();
+ bool isOptional = plugininfo.value(pluginIsOptionalLiteral(), QVariant(false)).toBool();
+ QString classnames = plugininfo.value(classnamesLiteral()).toString();
+ QStringList components = plugininfo.value(componentsLiteral()).toStringList();
+ QStringList scripts = plugininfo.value(scriptsLiteral()).toStringList();
+ QString prefer = plugininfo.value(preferLiteral()).toString();
+ if (!linkTarget.isEmpty())
+ import.insert(linkTargetLiteral(), linkTarget);
+ if (!plugins.isEmpty())
+ import.insert(QStringLiteral("plugin"), plugins);
+ if (isOptional)
+ import.insert(pluginIsOptionalLiteral(), true);
+ if (!classnames.isEmpty())
+ import.insert(QStringLiteral("classname"), classnames);
+ if (plugininfo.contains(dependenciesLiteral())) {
+ dependencies = plugininfo.value(dependenciesLiteral()).toStringList();
+ }
+ if (!components.isEmpty()) {
+ components.removeDuplicates();
+ import.insert(componentsLiteral(), components);
+ }
+ if (!scripts.isEmpty()) {
+ scripts.removeDuplicates();
+ import.insert(scriptsLiteral(), scripts);
+ }
+ if (!prefer.isEmpty()) {
+ import.insert(preferLiteral(), prefer);
+ }
+ }
+ import.remove(versionLiteral());
+
+ const ImportDetailsAndDeps result = {import, dependencies};
+ cache.insert({inputImport, result});
+ return result;
+}
+
+// Parse a dependency string line into a QVariantMap, to be used as a key when processing imports
+// in getGetDetailedModuleImportsIncludingDependencies.
+QVariantMap dependencyStringToImport(const QString &line) {
+ const auto dep = QStringView{line}.split(QLatin1Char(' '), Qt::SkipEmptyParts);
+ const QString name = dep[0].toString();
+ QVariantMap depImport;
+ depImport[typeLiteral()] = moduleLiteral();
+ depImport[nameLiteral()] = name;
+ if (dep.size() > 1)
+ depImport[versionLiteral()] = dep[1].toString();
+ return depImport;
+}
+
+// Returns details of given input import and its recursive module dependencies.
+// The details include absolute file system paths for the the module plugin, components,
+// etc.
+// An internal cache is used to prevent repeated computation for the same input module.
+QVariantList getGetDetailedModuleImportsIncludingDependencies(
+ const QVariant &inputImport,
+ FileImportsWithoutDepsCache &fileImportsWithoutDepsCache)
{
+ using Cache = std::unordered_map<QVariant, QVariantList, ImportVariantHasher>;
+ static Cache importsCacheWithDeps;
+
+ const Cache::const_iterator it = importsCacheWithDeps.find(inputImport);
+ if (it != importsCacheWithDeps.end()) {
+ return it->second;
+ }
+
QVariantList done;
- QVariantList importsCopy(imports);
-
- for (int i = 0; i < importsCopy.length(); ++i) {
- QVariantMap import = qvariant_cast<QVariantMap>(importsCopy.at(i));
- if (import.value(typeLiteral()) == moduleLiteral()) {
- const QString version = import.value(versionLiteral()).toString();
- const QPair<QString, QString> paths =
- resolveImportPath(import.value(nameLiteral()).toString(), version);
- QVariantMap plugininfo;
- if (!paths.first.isEmpty()) {
- import.insert(pathLiteral(), paths.first);
- import.insert(relativePathLiteral(), paths.second);
- plugininfo = pluginsForModulePath(paths.first, version);
- }
- QString plugins = plugininfo.value(pluginsLiteral()).toString();
- QString classnames = plugininfo.value(classnamesLiteral()).toString();
- if (!plugins.isEmpty())
- import.insert(QStringLiteral("plugin"), plugins);
- if (!classnames.isEmpty())
- import.insert(QStringLiteral("classname"), classnames);
- if (plugininfo.contains(dependenciesLiteral())) {
- const QStringList dependencies = plugininfo.value(dependenciesLiteral()).toStringList();
- for (const QString &line : dependencies) {
- const auto dep = QStringView{line}.split(QLatin1Char(' '), Qt::SkipEmptyParts);
- const QString name = dep[0].toString();
- QVariantMap depImport;
- depImport[typeLiteral()] = moduleLiteral();
- depImport[nameLiteral()] = name;
- if (dep.length() > 1)
- depImport[versionLiteral()] = dep[1].toString();
-
- if (!importsCopy.contains(depImport))
- importsCopy.append(depImport);
+ QVariantList importsToProcess;
+ std::unordered_set<QVariant, ImportVariantHasher> importsSeen;
+ importsToProcess.append(inputImport);
+
+ for (int i = 0; i < importsToProcess.size(); ++i) {
+ const QVariant importToProcess = importsToProcess.at(i);
+ auto [details, deps] = getImportDetails(importToProcess, fileImportsWithoutDepsCache);
+ if (details.value(typeLiteral()) == moduleLiteral()) {
+ for (const QString &line : deps) {
+ const QVariantMap depImport = dependencyStringToImport(line);
+
+ // Skip self-dependencies.
+ if (depImport == importToProcess)
+ continue;
+
+ if (importsSeen.find(depImport) == importsSeen.end()) {
+ importsToProcess.append(depImport);
+ importsSeen.insert(depImport);
}
}
}
- import.remove(versionLiteral());
- done.append(import);
+ done.append(details);
}
+
+ importsCacheWithDeps.insert({inputImport, done});
return done;
}
+QVariantList mergeImports(const QVariantList &a, const QVariantList &b);
+
+// Returns details of given input imports and their recursive module dependencies.
+QVariantList getGetDetailedModuleImportsIncludingDependencies(
+ const QVariantList &inputImports,
+ FileImportsWithoutDepsCache &fileImportsWithoutDepsCache)
+{
+ QVariantList result;
+
+ // Get rid of duplicates in input module list.
+ QVariantList inputImportsCopy;
+ inputImportsCopy = mergeImports(inputImportsCopy, inputImports);
+
+ // Collect recursive dependencies for each input module and merge into result, discarding
+ // duplicates.
+ for (auto it = inputImportsCopy.begin(); it != inputImportsCopy.end(); ++it) {
+ QVariantList imports = getGetDetailedModuleImportsIncludingDependencies(
+ *it, fileImportsWithoutDepsCache);
+ result = mergeImports(result, imports);
+ }
+ return result;
+}
+
// Scan a single qml file for import statements
QVariantList findQmlImportsInQmlCode(const QString &filePath, const QString &code)
{
+ qCDebug(lcImportScannerFiles) << "Parsing code and finding imports in" << filePath
+ << "TS:" << QDateTime::currentMSecsSinceEpoch();
+
QQmlJS::Engine engine;
QQmlJS::Lexer lexer(&engine);
lexer.setCode(code, /*line = */ 1);
@@ -425,9 +623,17 @@ QVariantList findQmlImportsInJavascriptFile(const QString &filePath)
return collector.imports;
}
-// Scan a single qml or js file for import statements
-QVariantList findQmlImportsInFile(const QString &filePath)
+// Scan a single qml or js file for import statements without resolving dependencies.
+QVariantList findQmlImportsInFileWithoutDeps(const QString &filePath,
+ FileImportsWithoutDepsCache
+ &fileImportsWithoutDepsCache)
{
+ const FileImportsWithoutDepsCache::const_iterator it =
+ fileImportsWithoutDepsCache.find(filePath);
+ if (it != fileImportsWithoutDepsCache.end()) {
+ return *it;
+ }
+
QVariantList imports;
if (filePath == QLatin1String("-")) {
QFile f;
@@ -437,12 +643,47 @@ QVariantList findQmlImportsInFile(const QString &filePath)
imports = findQmlImportsInQmlFile(filePath);
} else if (filePath.endsWith(QLatin1String(".js"))) {
imports = findQmlImportsInJavascriptFile(filePath);
+ } else {
+ qCDebug(lcImportScanner) << "Skipping file because it's not a .qml/.js file";
+ return imports;
}
- return findPathsForModuleImports(imports);
+ fileImportsWithoutDepsCache.insert(filePath, imports);
+ return imports;
+}
+
+// Scan a single qml or js file for import statements, resolve dependencies and return the full
+// list of modules the file depends on.
+QVariantList findQmlImportsInFile(const QString &filePath,
+ FileImportsWithoutDepsCache
+ &fileImportsWithoutDepsCache) {
+ const auto fileProcessTimeBegin = QDateTime::currentDateTime();
+
+ QVariantList imports = findQmlImportsInFileWithoutDeps(filePath,
+ fileImportsWithoutDepsCache);
+ if (imports.empty())
+ return imports;
+
+ const auto pathsTimeBegin = QDateTime::currentDateTime();
+
+ qCDebug(lcImportScanner) << "Finding module paths for imported modules in" << filePath
+ << "TS:" << pathsTimeBegin.toMSecsSinceEpoch();
+ QVariantList importPaths = getGetDetailedModuleImportsIncludingDependencies(
+ imports, fileImportsWithoutDepsCache);
+
+ const auto pathsTimeEnd = QDateTime::currentDateTime();
+ const auto duration = pathsTimeBegin.msecsTo(pathsTimeEnd);
+ const auto fileProcessingDuration = fileProcessTimeBegin.msecsTo(pathsTimeEnd);
+ qCDebug(lcImportScanner) << "Found module paths:" << importPaths.size()
+ << "TS:" << pathsTimeEnd.toMSecsSinceEpoch()
+ << "Path resolution duration:" << duration << "msecs";
+ qCDebug(lcImportScanner) << "Scan duration:" << fileProcessingDuration << "msecs";
+ return importPaths;
}
// Merge two lists of imports, discard duplicates.
+// Empirical tests show that for a small amount of values, the n^2 QVariantList comparison
+// is still faster than using an unordered_set + hashing a complex QVariantMap.
QVariantList mergeImports(const QVariantList &a, const QVariantList &b)
{
QVariantList merged = a;
@@ -472,7 +713,9 @@ struct pathStartsWith {
// Scan all qml files in directory for import statements
-QVariantList findQmlImportsInDirectory(const QString &qmlDir)
+QVariantList findQmlImportsInDirectory(const QString &qmlDir,
+ FileImportsWithoutDepsCache
+ &fileImportsWithoutDepsCache)
{
QVariantList ret;
if (qmlDir.isEmpty())
@@ -506,8 +749,15 @@ QVariantList findQmlImportsInDirectory(const QString &qmlDir)
}
for (const QFileInfo &x : entries)
- if (x.isFile())
- ret = mergeImports(ret, findQmlImportsInFile(x.absoluteFilePath()));
+ if (x.isFile()) {
+ const auto entryAbsolutePath = x.absoluteFilePath();
+ qCDebug(lcImportScanner) << "Scanning file" << entryAbsolutePath
+ << "TS:" << QDateTime::currentMSecsSinceEpoch();
+ ret = mergeImports(ret,
+ findQmlImportsInFile(
+ entryAbsolutePath,
+ fileImportsWithoutDepsCache));
+ }
}
return ret;
}
@@ -515,19 +765,29 @@ QVariantList findQmlImportsInDirectory(const QString &qmlDir)
// Find qml imports recursively from a root set of qml files.
// The directories in qmlDirs are searched recursively.
// The files in qmlFiles parsed directly.
-QVariantList findQmlImportsRecursively(const QStringList &qmlDirs, const QStringList &scanFiles)
+QVariantList findQmlImportsRecursively(const QStringList &qmlDirs,
+ const QStringList &scanFiles,
+ FileImportsWithoutDepsCache
+ &fileImportsWithoutDepsCache)
{
QVariantList ret;
+ qCDebug(lcImportScanner) << "Scanning" << qmlDirs.size() << "root directories and"
+ << scanFiles.size() << "files.";
+
// Scan all app root qml directories for imports
for (const QString &qmlDir : qmlDirs) {
- QVariantList imports = findQmlImportsInDirectory(qmlDir);
+ qCDebug(lcImportScanner) << "Scanning root" << qmlDir
+ << "TS:" << QDateTime::currentMSecsSinceEpoch();
+ QVariantList imports = findQmlImportsInDirectory(qmlDir, fileImportsWithoutDepsCache);
ret = mergeImports(ret, imports);
}
// Scan app qml files for imports
for (const QString &file : scanFiles) {
- QVariantList imports = findQmlImportsInFile(file);
+ qCDebug(lcImportScanner) << "Scanning file" << file
+ << "TS:" << QDateTime::currentMSecsSinceEpoch();
+ QVariantList imports = findQmlImportsInFile(file, fileImportsWithoutDepsCache);
ret = mergeImports(ret, imports);
}
@@ -550,8 +810,20 @@ QString generateCmakeIncludeFileContent(const QVariantList &importList) {
const QMap<QString, QVariant> &importDict = importVariant.toMap();
for (auto it = importDict.cbegin(); it != importDict.cend(); ++it) {
- s << it.key().toUpper() << QLatin1Char(';')
- << it.value().toString() << QLatin1Char(';');
+ s << it.key().toUpper() << QLatin1Char(';');
+ // QVariant can implicitly convert QString to the QStringList with the single
+ // element, let's use this.
+ QStringList args = it.value().toStringList();
+ if (args.isEmpty()) {
+ // This should not happen, but if it does, the result of the
+ // 'cmake_parse_arguments' call will be incorrect, so follow up semicolon
+ // indicates that the single-/multiarg option is empty.
+ s << QLatin1Char(';');
+ } else {
+ for (auto arg : args) {
+ s << arg << QLatin1Char(';');
+ }
+ }
}
s << QStringLiteral("\")\n");
++importsCount;
@@ -564,59 +836,102 @@ QString generateCmakeIncludeFileContent(const QVariantList &importList) {
return content;
}
+bool argumentsFromCommandLineAndFile(QStringList &allArguments, const QStringList &arguments)
+{
+ allArguments.reserve(arguments.size());
+ for (const QString &argument : arguments) {
+ // "@file" doesn't start with a '-' so we can't use QCommandLineParser for it
+ if (argument.startsWith(QLatin1Char('@'))) {
+ QString optionsFile = argument;
+ optionsFile.remove(0, 1);
+ if (optionsFile.isEmpty()) {
+ fprintf(stderr, "The @ option requires an input file");
+ return false;
+ }
+ QFile f(optionsFile);
+ if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ fprintf(stderr, "Cannot open options file specified with @");
+ return false;
+ }
+ while (!f.atEnd()) {
+ QString line = QString::fromLocal8Bit(f.readLine().trimmed());
+ if (!line.isEmpty())
+ allArguments << line;
+ }
+ } else {
+ allArguments << argument;
+ }
+ }
+ return true;
+}
+
} // namespace
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR));
- QStringList args = app.arguments();
+ QStringList args;
+ if (!argumentsFromCommandLineAndFile(args, app.arguments()))
+ return EXIT_FAILURE;
const QString appName = QFileInfo(app.applicationFilePath()).baseName();
if (args.size() < 2) {
printUsage(appName);
return 1;
}
+ // QQmlDirParser returnes QMultiHashes. Ensure deterministic output.
+ QHashSeed::setDeterministicGlobalSeed();
+
QStringList qmlRootPaths;
QStringList scanFiles;
QStringList qmlImportPaths;
QStringList qrcFiles;
bool generateCmakeContent = false;
+ QString outputFile;
int i = 1;
- while (i < args.count()) {
+ while (i < args.size()) {
const QString &arg = args.at(i);
++i;
QStringList *argReceiver = nullptr;
if (!arg.startsWith(QLatin1Char('-')) || arg == QLatin1String("-")) {
qmlRootPaths += arg;
} else if (arg == QLatin1String("-rootPath")) {
- if (i >= args.count())
+ if (i >= args.size())
std::cerr << "-rootPath requires an argument\n";
argReceiver = &qmlRootPaths;
} else if (arg == QLatin1String("-qmlFiles")) {
- if (i >= args.count())
+ if (i >= args.size())
std::cerr << "-qmlFiles requires an argument\n";
argReceiver = &scanFiles;
} else if (arg == QLatin1String("-jsFiles")) {
- if (i >= args.count())
+ if (i >= args.size())
std::cerr << "-jsFiles requires an argument\n";
argReceiver = &scanFiles;
} else if (arg == QLatin1String("-importPath")) {
- if (i >= args.count())
+ if (i >= args.size())
std::cerr << "-importPath requires an argument\n";
argReceiver = &qmlImportPaths;
} else if (arg == QLatin1String("-cmake-output")) {
generateCmakeContent = true;
} else if (arg == QLatin1String("-qrcFiles")) {
argReceiver = &qrcFiles;
+ } else if (arg == QLatin1String("-output-file")) {
+ if (i >= args.size()) {
+ std::cerr << "-output-file requires an argument\n";
+ return 1;
+ }
+ outputFile = args.at(i);
+ ++i;
+ continue;
} else {
std::cerr << qPrintable(appName) << ": Invalid argument: \""
<< qPrintable(arg) << "\"\n";
return 1;
}
- while (i < args.count()) {
+ while (i < args.size()) {
const QString arg = args.at(i);
if (arg.startsWith(QLatin1Char('-')) && arg != QLatin1String("-"))
break;
@@ -625,8 +940,12 @@ int main(int argc, char *argv[])
std::cerr << qPrintable(appName) << ": No such file or directory: \""
<< qPrintable(arg) << "\"\n";
return 1;
- } else {
+ } else if (argReceiver) {
*argReceiver += arg;
+ } else {
+ std::cerr << qPrintable(appName) << ": Invalid argument: \""
+ << qPrintable(arg) << "\"\n";
+ return 1;
}
}
}
@@ -638,8 +957,13 @@ int main(int argc, char *argv[])
g_qmlImportPaths = qmlImportPaths;
+ FileImportsWithoutDepsCache fileImportsWithoutDepsCache;
+
// Find the imports!
- QVariantList imports = findQmlImportsRecursively(qmlRootPaths, scanFiles);
+ QVariantList imports = findQmlImportsRecursively(qmlRootPaths,
+ scanFiles,
+ fileImportsWithoutDepsCache
+ );
QByteArray content;
if (generateCmakeContent) {
@@ -650,6 +974,17 @@ int main(int argc, char *argv[])
content = QJsonDocument(QJsonArray::fromVariantList(imports)).toJson();
}
- std::cout << content.constData() << std::endl;
+ if (outputFile.isEmpty()) {
+ std::cout << content.constData() << std::endl;
+ } else {
+ QFile f(outputFile);
+ if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
+ std::cerr << qPrintable(appName) << ": Unable to write to output file: \""
+ << qPrintable(outputFile) << "\"\n";
+ return 1;
+ }
+ QTextStream out(&f);
+ out << content << "\n";
+ }
return 0;
}