diff options
Diffstat (limited to 'tools/qmlimportscanner/main.cpp')
-rw-r--r-- | tools/qmlimportscanner/main.cpp | 553 |
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; } |