Files
RedBear-OS/local/recipes/qt/qtdeclarative/source/tools/qmlimportscanner/main.cpp
T
vasilito f31522130f fix: comprehensive boot warnings and exceptions — fixable silenced, unfixable diagnosed
Build system (5 gaps hardened):
- COOKBOOK_OFFLINE defaults to true (fork-mode)
- normalize_patch handles diff -ruN format
- New 'repo validate-patches' command (25/25 relibc patches)
- 14 patched Qt/Wayland/display recipes added to protected list
- relibc archive regenerated with current patch chain

Boot fixes (fixable):
- Full ISO EFI partition: 16 MiB → 1 MiB (matches mini, BIOS hardcoded 2 MiB offset)
- D-Bus system bus: absolute /usr/bin/dbus-daemon path (was skipped)
- redbear-sessiond: absolute /usr/bin/redbear-sessiond path (was skipped)
- daemon framework: silenced spurious INIT_NOTIFY warnings for oneshot_async services (P0-daemon-silence-init-notify.patch)
- udev-shim: demoted INIT_NOTIFY warning to INFO (expected for oneshot_async)
- relibc: comprehensive named semaphores (sem_open/close/unlink) replacing upstream todo!() stubs
- greeterd: Wayland socket timeout 15s → 30s (compositor DRM wait)
- greeter-ui: built and linked (header guard unification, sem_compat stubs removed)
- mc: un-ignored in both configs, fixed glib/libiconv/pcre2 transitive deps
- greeter config: removed stale keymapd dependency from display/greeter services
- prefix toolchain: relibc headers synced, _RELIBC_STDLIB_H guard unified

Unfixable (diagnosed, upstream):
- i2c-hidd: abort on no-I2C-hardware (QEMU) — process::exit → relibc abort
- kded6/greeter-ui: page fault 0x8 — Qt library null deref
- Thread panics fd != -1 — Rust std library on Redox
- DHCP timeout / eth0 MAC — QEMU user-mode networking
- hwrngd/thermald — no hardware RNG/thermal in VM
- live preload allocation — BIOS memory fragmentation, continues on demand
2026-05-05 20:20:37 +01:00

1012 lines
39 KiB
C++

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <private/qqmljslexer_p.h>
#include <private/qqmljsparser_p.h>
#include <private/qqmljsast_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;
bool g_addImportVersion = false;
inline QString typeLiteral() { return QStringLiteral("type"); }
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::string appName = appNameIn.toStdString();
const QString qmlPath = QLibraryInfo::path(QLibraryInfo::QmlImportsPath);
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).toStdString()
<< "\n\nOptions:\n"
<< " -exclude <directory>: Exclude directory\n"
<< '\n';
}
QVariantList findImportsInAst(QQmlJS::AST::UiHeaderItemList *headerItemList, const QString &filePath)
{
QVariantList imports;
// Extract uri and version from the imports (which look like "import Foo.Bar 1.2.3")
for (QQmlJS::AST::UiHeaderItemList *headerItemIt = headerItemList; headerItemIt; headerItemIt = headerItemIt->next) {
QVariantMap import;
QQmlJS::AST::UiImport *importNode = QQmlJS::AST::cast<QQmlJS::AST::UiImport *>(headerItemIt->headerItem);
if (!importNode)
continue;
// Handle directory imports
if (!importNode->fileName.isEmpty()) {
QString name = importNode->fileName.toString();
import[nameLiteral()] = name;
if (name.endsWith(QLatin1String(".js"))) {
import[typeLiteral()] = javascriptLiteral();
} else {
import[typeLiteral()] = directoryLiteral();
}
import[pathLiteral()] = QDir::cleanPath(
QFileInfo(filePath).path() + QLatin1Char('/') + name);
} else {
// Walk the id chain ("Foo" -> "Bar" -> etc)
QString name;
QQmlJS::AST::UiQualifiedId *uri = importNode->importUri;
while (uri) {
name.append(uri->name);
name.append(QLatin1Char('.'));
uri = uri->next;
}
name.chop(1); // remove trailing "."
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
if (name.startsWith(QLatin1String("QtQuick.Controls")) && name.endsWith(QLatin1String("impl")))
continue;
#endif
if (!name.isEmpty())
import[nameLiteral()] = name;
import[typeLiteral()] = moduleLiteral();
auto versionString = importNode->version
? QString::number(importNode->version->version.majorVersion())
+ QLatin1Char('.')
+ QString::number(importNode->version->version.minorVersion())
: QString();
if (!versionString.isEmpty())
import[versionLiteral()] = versionString;
}
imports.append(import);
}
return imports;
}
QVariantList findQmlImportsInFileWithoutDeps(const QString &filePath,
FileImportsWithoutDepsCache
&fileImportsWithoutDepsCache);
static QString versionSuffix(QTypeRevision version)
{
return QLatin1Char(' ') + QString::number(version.majorVersion()) + QLatin1Char('.')
+ QString::number(version.minorVersion());
}
// 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,
FileImportsWithoutDepsCache
&fileImportsWithoutDepsCache) {
using Cache = QHash<std::pair<QString, QString>, QVariantMap>;
static Cache pluginsCache;
const std::pair<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;
return QVariantMap();
}
if (!qmldirFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "qmldir file not found at" << modulePath;
return QVariantMap();
}
QQmlDirParser parser;
parser.parse(QString::fromUtf8(qmldirFile.readAll()));
if (parser.hasError()) {
qWarning() << "qmldir file malformed at" << modulePath;
for (const auto &error : parser.errors(QLatin1String("qmldir")))
qWarning() << error.message;
return QVariantMap();
}
QVariantMap pluginInfo;
QStringList pluginNameList;
bool isOptional = false;
const auto plugins = parser.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;
const auto dependencies = parser.dependencies();
for (const auto &dependency : dependencies)
importsAndDependencies.append(dependency.module + versionSuffix(dependency.version));
const auto imports = parser.imports();
for (const auto &import : imports) {
if (import.flags & QQmlDirParser::Import::Auto) {
importsAndDependencies.append(
import.module + QLatin1Char(' ')
+ (version.isEmpty() ? QString::fromLatin1("auto") : version));
} else if (import.version.isValid()) {
importsAndDependencies.append(import.module + versionSuffix(import.version));
} else {
importsAndDependencies.append(import.module);
}
}
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
+= findQmlImportsInFileWithoutDeps(componentFullPath,
fileImportsWithoutDepsCache);
}
const auto scripts = parser.scripts();
for (const auto &script : scripts) {
const QString scriptFullPath = modulePath + QLatin1Char('/') + script.fileName;
scriptFiles.append(scriptFullPath);
importsFromFiles
+= findQmlImportsInFileWithoutDeps(scriptFullPath,
fileImportsWithoutDepsCache);
}
for (const QVariant &import : importsFromFiles) {
const QVariantMap details = qvariant_cast<QVariantMap>(import);
if (details.value(typeLiteral()) != moduleLiteral())
continue;
const QString name = details.value(nameLiteral()).toString();
const QString version = details.value(versionLiteral()).toString();
importsAndDependencies.append(
version.isEmpty() ? name : (name + QLatin1Char(' ') + version));
}
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;
}
// Search for a given qml import in g_qmlImportPaths and return a pair
// of absolute / relative paths (for deployment).
std::pair<QString, QString> resolveImportPath(const QString &uri, const QString &version)
{
const QLatin1Char dot('.');
const QLatin1Char slash('/');
const QStringList parts = uri.split(dot, Qt::SkipEmptyParts);
QString ver = version;
std::pair<QString, QString> candidate;
while (true) {
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
// - qml/QtQml.2.0/Models
// - qml/QtQml/Models.2
// - qml/QtQml.2/Models
// - qml/QtQml/Models
if (ver.isEmpty()) {
QString relativePath = parts.join(slash);
if (relativePath.endsWith(slash))
relativePath.chop(1);
const QString candidatePath = QDir::cleanPath(qmlImportPath + slash + relativePath);
const QDir candidateDir(candidatePath);
if (candidateDir.exists()) {
const auto newCandidate = std::make_pair(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.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);
const QDir candidateDir(candidatePath);
if (candidateDir.exists()) {
const auto newCandidate = std::make_pair(candidatePath, relativePath); // import found
if (candidateDir.exists(u"qmldir"_s))
return newCandidate;
else if (candidate.first.isEmpty())
candidate = newCandidate;
}
}
}
}
// Remove the last version digit; stop if there are none left
if (ver.isEmpty())
break;
int lastDot = ver.lastIndexOf(dot);
if (lastDot == -1)
ver.clear();
else
ver = ver.mid(0, lastDot);
}
return candidate;
}
// 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 = std::pair<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 std::pair<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);
}
}
if (!g_addImportVersion)
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 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);
}
}
}
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);
QQmlJS::Parser parser(&engine);
if (!parser.parse() || !parser.diagnosticMessages().isEmpty()) {
// Extract errors from the parser
const auto diagnosticMessages = parser.diagnosticMessages();
for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages) {
std::cerr << QDir::toNativeSeparators(filePath).toStdString() << ':'
<< m.loc.startLine << ':' << m.message.toStdString() << std::endl;
}
return QVariantList();
}
return findImportsInAst(parser.ast()->headers, filePath);
}
// Scan a single qml file for import statements
QVariantList findQmlImportsInQmlFile(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
std::cerr << "Cannot open input file " << QDir::toNativeSeparators(file.fileName()).toStdString()
<< ':' << file.errorString().toStdString() << std::endl;
return QVariantList();
}
QString code = QString::fromUtf8(file.readAll());
return findQmlImportsInQmlCode(filePath, code);
}
struct ImportCollector : public QQmlJS::Directives
{
QVariantList imports;
void importFile(const QString &jsfile, const QString &module, int line, int column) override
{
QVariantMap entry;
entry[typeLiteral()] = javascriptLiteral();
entry[pathLiteral()] = jsfile;
imports << entry;
Q_UNUSED(module);
Q_UNUSED(line);
Q_UNUSED(column);
}
void importModule(const QString &uri, const QString &version, const QString &module, int line, int column) override
{
QVariantMap entry;
if (uri.contains(QLatin1Char('/'))) {
entry[typeLiteral()] = directoryLiteral();
entry[nameLiteral()] = uri;
} else {
entry[typeLiteral()] = moduleLiteral();
entry[nameLiteral()] = uri;
if (!version.isEmpty())
entry[versionLiteral()] = version;
}
imports << entry;
Q_UNUSED(module);
Q_UNUSED(line);
Q_UNUSED(column);
}
};
// Scan a single javascrupt file for import statements
QVariantList findQmlImportsInJavascriptFile(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
std::cerr << "Cannot open input file " << QDir::toNativeSeparators(file.fileName()).toStdString()
<< ':' << file.errorString().toStdString() << std::endl;
return QVariantList();
}
QString sourceCode = QString::fromUtf8(file.readAll());
file.close();
QQmlJS::Engine ee;
ImportCollector collector;
ee.setDirectives(&collector);
QQmlJS::Lexer lexer(&ee);
lexer.setCode(sourceCode, /*line*/1, /*qml mode*/false);
QQmlJS::Parser parser(&ee);
parser.parseProgram();
const auto diagnosticMessages = parser.diagnosticMessages();
for (const QQmlJS::DiagnosticMessage &m : diagnosticMessages)
if (m.isError())
return QVariantList();
return collector.imports;
}
// 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;
if (f.open(stdin, QIODevice::ReadOnly))
imports = findQmlImportsInQmlCode(QLatin1String("<stdin>"), QString::fromUtf8(f.readAll()));
} else if (filePath.endsWith(QLatin1String(".qml"))) {
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;
}
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;
for (const QVariant &variant : b) {
if (!merged.contains(variant))
merged.append(variant);
}
return merged;
}
// Predicates needed by findQmlImportsInDirectory.
struct isMetainfo {
bool operator() (const QFileInfo &x) const {
return x.suffix() == QLatin1String("metainfo");
}
};
struct pathStartsWith {
pathStartsWith(const QString &path) : _path(path) {}
bool operator() (const QString &x) const {
return _path.startsWith(x);
}
const QString _path;
};
static QStringList excludedDirectories = {
".qtcreator"_L1, ".qtc_clangd"_L1, // Windows does not consider these hidden
#ifdef Q_OS_WIN
"release"_L1, "debug"_L1
#endif
};
static bool isExcluded(const QFileInfo &dir)
{
if (excludedDirectories.contains(dir.fileName()))
return true;
const QString &path = dir.absoluteFilePath();
// Skip obvious build output directories
return path.contains("Debug-iphoneos"_L1) || path.contains("Release-iphoneos"_L1)
|| path.contains("Debug-iphonesimulator"_L1) || path.contains("Release-iphonesimulator"_L1);
}
// Scan all qml files in directory for import statements
QVariantList findQmlImportsInDirectory(const QString &qmlDir,
FileImportsWithoutDepsCache
&fileImportsWithoutDepsCache)
{
QVariantList ret;
if (qmlDir.isEmpty())
return ret;
QDirIterator iterator(qmlDir, QDir::AllDirs | QDir::NoDotDot, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
QStringList blacklist;
while (iterator.hasNext()) {
iterator.next();
if (isExcluded(iterator.fileInfo()))
continue;
const QString path = iterator.filePath();
const QFileInfoList entries = QDir(path).entryInfoList();
// Skip designer related stuff
if (std::find_if(entries.cbegin(), entries.cend(), isMetainfo()) != entries.cend()) {
blacklist << path;
continue;
}
if (std::find_if(blacklist.cbegin(), blacklist.cend(), pathStartsWith(path)) != blacklist.cend())
continue;
for (const QFileInfo &x : entries)
if (x.isFile()) {
const auto entryAbsolutePath = x.absoluteFilePath();
qCDebug(lcImportScanner) << "Scanning file" << entryAbsolutePath
<< "TS:" << QDateTime::currentMSecsSinceEpoch();
ret = mergeImports(ret,
findQmlImportsInFile(
entryAbsolutePath,
fileImportsWithoutDepsCache));
}
}
return ret;
}
// 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,
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) {
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) {
qCDebug(lcImportScanner) << "Scanning file" << file
<< "TS:" << QDateTime::currentMSecsSinceEpoch();
QVariantList imports = findQmlImportsInFile(file, fileImportsWithoutDepsCache);
ret = mergeImports(ret, imports);
}
return ret;
}
QString generateCmakeIncludeFileContent(const QVariantList &importList) {
// The function assumes that "list" is a QVariantList with 0 or more QVariantMaps, where
// each map contains QString -> QVariant<QString> mappings. This matches with the structure
// that qmake parses for static qml plugin auto imporitng.
// So: [ {"a": "a","b": "b"}, {"c": "c"} ]
QString content;
QTextStream s(&content);
int importsCount = 0;
for (const QVariant &importVariant: importList) {
if (static_cast<QMetaType::Type>(importVariant.userType()) == QMetaType::QVariantMap) {
s << QStringLiteral("set(qml_import_scanner_import_") << importsCount
<< QStringLiteral(" \"");
const QMap<QString, QVariant> &importDict = importVariant.toMap();
for (auto it = importDict.cbegin(); it != importDict.cend(); ++it) {
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;
}
}
if (importsCount >= 0) {
content.prepend(QString(QStringLiteral("set(qml_import_scanner_imports_count %1)\n"))
.arg(importsCount));
}
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;
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.size()) {
bool checkDirExists = true;
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.size())
std::cerr << "-rootPath requires an argument\n";
argReceiver = &qmlRootPaths;
} else if (arg == QLatin1String("-qmlFiles")) {
if (i >= args.size())
std::cerr << "-qmlFiles requires an argument\n";
argReceiver = &scanFiles;
} else if (arg == QLatin1String("-jsFiles")) {
if (i >= args.size())
std::cerr << "-jsFiles requires an argument\n";
argReceiver = &scanFiles;
} else if (arg == QLatin1String("-importPath")) {
if (i >= args.size())
std::cerr << "-importPath requires an argument\n";
argReceiver = &qmlImportPaths;
} else if (arg == "-exclude"_L1) {
if (i >= args.size())
std::cerr << "-exclude Path requires an argument\n";
checkDirExists = false;
argReceiver = &excludedDirectories;
} 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 if (arg == QLatin1String("-add-version")) {
g_addImportVersion = true;
} else {
std::cerr << qPrintable(appName) << ": Invalid argument: \""
<< qPrintable(arg) << "\"\n";
return 1;
}
while (i < args.size()) {
const QString arg = args.at(i);
if (arg.startsWith(QLatin1Char('-')) && arg != QLatin1String("-"))
break;
++i;
if (arg != QLatin1String("-") && checkDirExists && !QFile::exists(arg)) {
std::cerr << qPrintable(appName) << ": No such file or directory: \""
<< qPrintable(arg) << "\"\n";
return 1;
} else if (argReceiver) {
*argReceiver += arg;
} else {
std::cerr << qPrintable(appName) << ": Invalid argument: \""
<< qPrintable(arg) << "\"\n";
return 1;
}
}
}
if (!qrcFiles.isEmpty()) {
scanFiles << QQmlJSResourceFileMapper(qrcFiles).filePaths(
QQmlJSResourceFileMapper::allQmlJSFilter());
}
g_qmlImportPaths = qmlImportPaths;
FileImportsWithoutDepsCache fileImportsWithoutDepsCache;
// Find the imports!
QVariantList imports = findQmlImportsRecursively(qmlRootPaths,
scanFiles,
fileImportsWithoutDepsCache
);
QByteArray content;
if (generateCmakeContent) {
// Convert to CMake code
content = generateCmakeIncludeFileContent(imports).toUtf8();
} else {
// Convert to JSON
content = QJsonDocument(QJsonArray::fromVariantList(imports)).toJson();
}
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;
}