/**************************************************************************** ** ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the tools applications of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** 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 Digia. For licensing terms and ** conditions see http://qt.digia.com/licensing. For further information ** use the contact form at http://qt.digia.com/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Digia gives you certain additional ** rights. These rights are described in the Digia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QT_USE_NAMESPACE QStringList g_qmlImportPaths; void printUsage(const QString &appName) { std::cerr << qPrintable(QString::fromLatin1( "Usage: %1 -rootPath path/to/app/qml/directory -importPath path/to/qt/qml/directory \n" " %1 -qmlFiles file1 file2 -importPath path/to/qt/qml/directory \n" "Example: %1 -rootPath . -importPath /home/user/dev/qt-install/qml \n").arg( appName)); } QVariantList findImportsInAst(QQmlJS::AST::UiHeaderItemList *headerItemList, const QString &code, const QString &path) { 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(headerItemIt->headerItem); if (!importNode) continue; // handle directory imports if (!importNode->fileName.isEmpty()) { QString name = importNode->fileName.toString(); import[QStringLiteral("name")] = name; if (name.endsWith(QStringLiteral(".js"))) { import[QStringLiteral("type")] = QStringLiteral("javascript"); } else { import[QStringLiteral("type")] = QStringLiteral("directory"); } import[QStringLiteral("path")] = QDir::cleanPath(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 (!name.isEmpty()) import[QStringLiteral("name")] = name; import[QStringLiteral("type")] = QStringLiteral("module"); import[QStringLiteral("version")] = code.mid(importNode->versionToken.offset, importNode->versionToken.length); } imports.append(import); } return imports; } // Read the qmldir file, extract a list of plugins by // parsing the "plugin" and "classname" lines. QVariantMap pluginsForModulePath(const QString &modulePath) { QFile qmldirFile(modulePath + QStringLiteral("/qmldir")); if (!qmldirFile.exists()) return QVariantMap(); qmldirFile.open(QIODevice::ReadOnly | QIODevice::Text); // a qml import may contain several plugins QString plugins; QString classnames; QByteArray line; do { line = qmldirFile.readLine(); if (line.startsWith("plugin")) { plugins += QString::fromUtf8(line.split(' ').at(1)); plugins += QStringLiteral(" "); } else if (line.startsWith("classname")) { classnames += QString::fromUtf8(line.split(' ').at(1)); classnames += QStringLiteral(" "); } } while (line.length() > 0); QVariantMap pluginInfo; pluginInfo[QStringLiteral("plugins")] = plugins.simplified(); pluginInfo[QStringLiteral("classnames")] = classnames.simplified(); return pluginInfo; } // Search for a given qml import in g_qmlImportPaths. QString resolveImportPath(const QString &uri, const QString &version) { // Create path from uri (QtQuick.Controls -> QtQuick/Controls) QString path = uri; path.replace(QLatin1Char('.'), QLatin1Char('/')); // search for the most spesifc version first QString versionedName = path + QLatin1Char('.') + version; while (true) { // look in all g_qmlImportPaths foreach (const QString &qmlImportPath, g_qmlImportPaths) { QString candidatePath = QDir::cleanPath(qmlImportPath + QLatin1Char('/') + versionedName); if (QDir(candidatePath).exists()) return candidatePath; // import found } // remove the last version digit; stop if there are none left int lastDot = versionedName.lastIndexOf(QLatin1Char('.')); if (lastDot == -1) break; versionedName = versionedName.mid(0, lastDot); } return QString(); // not found } // Find absolute file system paths and plugins for a list of modules. QVariantList findPathsForModuleImports(const QVariantList &imports) { QVariantList done; foreach (QVariant importVariant, imports) { QVariantMap import = qvariant_cast(importVariant); if (import[QStringLiteral("type")] == QStringLiteral("module")) { QString path = resolveImportPath(import.value(QStringLiteral("name")).toString(), import.value(QStringLiteral("version")).toString()); if (!path.isEmpty()) import[QStringLiteral("path")] = path; QVariantMap plugininfo = pluginsForModulePath(import.value(QStringLiteral("path")).toString()); QString plugins = plugininfo.value(QStringLiteral("plugins")).toString(); QString classnames = plugininfo.value(QStringLiteral("classnames")).toString(); if (!plugins.isEmpty()) import[QStringLiteral("plugin")] = plugins; if (!classnames.isEmpty()) import[QStringLiteral("classname")] = classnames; } done.append(import); } return done; } // Scan a single qml file for import statements QVariantList findQmlImportsInFile(const QString &qmlFilePath) { QFile qmlFile(qmlFilePath); if (!qmlFile.open(QIODevice::ReadOnly)) { std::cerr << "Cannot open input file " << QDir::toNativeSeparators(qmlFile.fileName()).toStdString() << ':' << qmlFile.errorString().toStdString() << std::endl; return QVariantList(); } QString code = QString::fromUtf8(qmlFile.readAll()); 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 foreach (const QQmlJS::DiagnosticMessage &m, parser.diagnosticMessages()) { std::cerr << QDir::toNativeSeparators(qmlFile.fileName()).toStdString() << ':' << m.loc.startLine << ':' << m.message.toStdString() << std::endl; } return QVariantList(); } return findPathsForModuleImports( findImportsInAst(parser.ast()->headers, code, QFileInfo(qmlFilePath).absolutePath())); } // Merge two lists of imports, discard duplicates. QVariantList mergeImports(const QVariantList &a, const QVariantList &b) { QVariantList merged = a; foreach (const QVariant &variant, b) { if (!merged.contains(variant)) merged.append(variant); } return merged; } // Scan all qml files in directory for import statements QVariantList findQmlImportsInDirectory(const QString &qmlDir) { QVariantList ret; if (qmlDir.isEmpty()) return ret; QDirIterator iterator(qmlDir, QDirIterator::Subdirectories); while (iterator.hasNext()) { iterator.next(); QString path = iterator.filePath(); if (!path.endsWith(QStringLiteral(".qml"))) continue; // skip obvious build output directories if (path.contains(QStringLiteral("Debug-iphoneos")) || path.contains(QStringLiteral("Release-iphoneos")) || path.contains(QStringLiteral("Debug-iphonesimulator")) || path.contains(QStringLiteral("Release-iphonesimulator")) #ifdef Q_OS_WIN || path.contains(QStringLiteral("/release/")) || path.contains(QStringLiteral("/debug/")) #endif ){ continue; } QVariantList imports = findQmlImportsInFile(path); ret = mergeImports(ret, imports); } return ret; } QSet importModulePaths(QVariantList imports) { QSet ret; foreach (const QVariant &importVariant, imports) { QVariantMap import = qvariant_cast(importVariant); QString path = import.value(QStringLiteral("path")).toString(); QString type = import.value(QStringLiteral("type")).toString(); if (type == QStringLiteral("module") && !path.isEmpty()) ret.insert(QDir(path).canonicalPath()); } 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 &qmlFiles) { QVariantList ret; // scan all app root qml directories for imports foreach (const QString &qmlDir, qmlDirs) { QVariantList imports = findQmlImportsInDirectory(qmlDir); ret = mergeImports(ret, imports); } // scan app qml files for imports foreach (const QString &qmlFile, qmlFiles) { QVariantList imports = findQmlImportsInFile(qmlFile); ret = mergeImports(ret, imports); } // get the paths to theimports found in the app qml QSet toVisit = importModulePaths(ret); // recursivly scan for import dependencies. QSet visited; while (!toVisit.isEmpty()) { QString qmlDir = *toVisit.begin(); toVisit.erase(toVisit.begin()); visited.insert(qmlDir); QVariantList imports = findQmlImportsInDirectory(qmlDir); ret = mergeImports(ret, imports); QSet candidatePaths = importModulePaths(ret); candidatePaths.subtract(visited); toVisit.unite(candidatePaths); } return ret; } int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QStringList args = app.arguments(); const QString appName = QFileInfo(app.applicationFilePath()).baseName(); if (args.size() < 2) { printUsage(appName); return 1; } QStringList qmlRootPaths; QStringList qmlFiles; QStringList qmlImportPaths; int i = 1; while (i < args.count()) { const QString &arg = args.at(i); ++i; QStringList *argReceiver = 0; if (!arg.startsWith(QLatin1Char('-'))) { qmlRootPaths += arg; } else if (arg == QLatin1String("-rootPath")) { if (i >= args.count()) std::cerr << "-rootPath requires an argument\n"; argReceiver = &qmlRootPaths; } else if (arg == QLatin1String("-qmlFiles")) { if (i >= args.count()) std::cerr << "-qmlFiles requires an argument\n"; argReceiver = &qmlFiles; } else if (arg == QLatin1String("-importPath")) { if (i >= args.count()) std::cerr << "-importPath requires an argument\n"; argReceiver = &qmlImportPaths; } else { std::cerr << "Invalid argument: \"" << qPrintable(arg) << "\"\n"; return 1; } while (i < args.count()) { const QString arg = args.at(i); if (arg.startsWith(QLatin1Char('-'))) break; ++i; *argReceiver += arg; } } g_qmlImportPaths = qmlImportPaths; // Find the imports! QVariantList imports = findQmlImportsRecursively(qmlRootPaths, qmlFiles); // Convert to JSON QByteArray json = QJsonDocument(QJsonArray::fromVariantList(imports)).toJson(); std::cout << json.constData() << std::endl; return 0; }