blob: 857786beb618e141ad40371d0f38d045118bc4e5 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Linguist 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$
**
****************************************************************************/
#include <profileevaluator.h>
#include <profileutils.h>
#include <qmakeparser.h>
#include <qmakevfs.h>
#include <qrcreader.h>
#include <QtCore/QCoreApplication>
#include <QtCore/QDebug>
#include <QtCore/QDir>
#include <QtCore/QDirIterator>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QRegExp>
#include <QtCore/QString>
#include <QtCore/QStringList>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <iostream>
static void printOut(const QString &out)
{
std::cout << qPrintable(out);
}
static void printErr(const QString &out)
{
std::cerr << qPrintable(out);
}
static QJsonValue toJsonValue(const QJsonValue &v)
{
return v;
}
static QJsonValue toJsonValue(const QString &s)
{
return QJsonValue(s);
}
static QJsonValue toJsonValue(const QStringList &lst)
{
return QJsonArray::fromStringList(lst);
}
template <class T>
void setValue(QJsonObject &obj, const char *key, T value)
{
obj[QLatin1String(key)] = toJsonValue(value);
}
class LD {
Q_DECLARE_TR_FUNCTIONS(LProDump)
};
static void printUsage()
{
printOut(LD::tr(
"Usage:\n"
" lprodump [options] project-file...\n"
"lprodump is part of Qt's Linguist tool chain. It extracts information\n"
"from qmake projects to a .json file. This file can be passed to\n"
"lupdate/lrelease using the -project option.\n\n"
"Options:\n"
" -help Display this information and exit.\n"
" -silent\n"
" Do not explain what is being done.\n"
" -pro <filename>\n"
" Name of a .pro file. Useful for files with .pro file syntax but\n"
" different file suffix. Projects are recursed into and merged.\n"
" -pro-out <directory>\n"
" Virtual output directory for processing subsequent .pro files.\n"
" -pro-debug\n"
" Trace processing .pro files. Specify twice for more verbosity.\n"
" -out <filename>\n"
" Name of the output file.\n"
" -version\n"
" Display the version of lprodump and exit.\n"
));
}
static void print(const QString &fileName, int lineNo, const QString &msg)
{
if (lineNo > 0)
printErr(QString::fromLatin1("WARNING: %1:%2: %3\n").arg(fileName, QString::number(lineNo), msg));
else if (lineNo)
printErr(QString::fromLatin1("WARNING: %1: %2\n").arg(fileName, msg));
else
printErr(QString::fromLatin1("WARNING: %1\n").arg(msg));
}
class EvalHandler : public QMakeHandler {
public:
virtual void message(int type, const QString &msg, const QString &fileName, int lineNo)
{
if (verbose && !(type & CumulativeEvalMessage) && (type & CategoryMask) == ErrorMessage)
print(fileName, lineNo, msg);
}
virtual void fileMessage(int type, const QString &msg)
{
if (verbose && !(type & CumulativeEvalMessage) && (type & CategoryMask) == ErrorMessage) {
// "Downgrade" errors, as we don't really care for them
printErr(QLatin1String("WARNING: ") + msg + QLatin1Char('\n'));
}
}
virtual void aboutToEval(ProFile *, ProFile *, EvalFileType) {}
virtual void doneWithEval(ProFile *) {}
bool verbose = true;
};
static EvalHandler evalHandler;
static bool isSupportedExtension(const QString &ext)
{
return ext == QLatin1String("qml")
|| ext == QLatin1String("js") || ext == QLatin1String("qs")
|| ext == QLatin1String("ui") || ext == QLatin1String("jui");
}
static QStringList getResources(const QString &resourceFile, QMakeVfs *vfs)
{
Q_ASSERT(vfs);
if (!vfs->exists(resourceFile, QMakeVfs::VfsCumulative))
return QStringList();
QString content;
QString errStr;
if (vfs->readFile(vfs->idForFileName(resourceFile, QMakeVfs::VfsCumulative),
&content, &errStr) != QMakeVfs::ReadOk) {
printErr(LD::tr("lprodump error: Cannot read %1: %2\n").arg(resourceFile, errStr));
return QStringList();
}
const ReadQrcResult rqr = readQrcFile(resourceFile, content);
if (rqr.hasError()) {
printErr(LD::tr("lprodump error: %1:%2: %3\n")
.arg(resourceFile, QString::number(rqr.line), rqr.errorString));
}
return rqr.files;
}
static QStringList getSources(const char *var, const char *vvar, const QStringList &baseVPaths,
const QString &projectDir, const ProFileEvaluator &visitor)
{
QStringList vPaths = visitor.absolutePathValues(QLatin1String(vvar), projectDir);
vPaths += baseVPaths;
vPaths.removeDuplicates();
return visitor.absoluteFileValues(QLatin1String(var), projectDir, vPaths, 0);
}
static QStringList getSources(const ProFileEvaluator &visitor, const QString &projectDir,
const QStringList &excludes, QMakeVfs *vfs)
{
QStringList baseVPaths;
baseVPaths += visitor.absolutePathValues(QLatin1String("VPATH"), projectDir);
baseVPaths << projectDir; // QMAKE_ABSOLUTE_SOURCE_PATH
baseVPaths.removeDuplicates();
QStringList sourceFiles;
// app/lib template
sourceFiles += getSources("SOURCES", "VPATH_SOURCES", baseVPaths, projectDir, visitor);
sourceFiles += getSources("HEADERS", "VPATH_HEADERS", baseVPaths, projectDir, visitor);
sourceFiles += getSources("FORMS", "VPATH_FORMS", baseVPaths, projectDir, visitor);
QStringList resourceFiles = getSources("RESOURCES", "VPATH_RESOURCES", baseVPaths, projectDir, visitor);
foreach (const QString &resource, resourceFiles)
sourceFiles += getResources(resource, vfs);
QStringList installs = visitor.values(QLatin1String("INSTALLS"))
+ visitor.values(QLatin1String("DEPLOYMENT"));
installs.removeDuplicates();
QDir baseDir(projectDir);
foreach (const QString inst, installs) {
foreach (const QString &file, visitor.values(inst + QLatin1String(".files"))) {
QFileInfo info(file);
if (!info.isAbsolute())
info.setFile(baseDir.absoluteFilePath(file));
QStringList nameFilter;
QString searchPath;
if (info.isDir()) {
nameFilter << QLatin1String("*");
searchPath = info.filePath();
} else {
nameFilter << info.fileName();
searchPath = info.path();
}
QDirIterator iterator(searchPath, nameFilter,
QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks,
QDirIterator::Subdirectories);
while (iterator.hasNext()) {
iterator.next();
QFileInfo cfi = iterator.fileInfo();
if (isSupportedExtension(cfi.suffix()))
sourceFiles << cfi.filePath();
}
}
}
sourceFiles.removeDuplicates();
sourceFiles.sort();
foreach (const QString &ex, excludes) {
// TODO: take advantage of the file list being sorted
QRegExp rx(ex, Qt::CaseSensitive, QRegExp::Wildcard);
for (QStringList::Iterator it = sourceFiles.begin(); it != sourceFiles.end(); ) {
if (rx.exactMatch(*it))
it = sourceFiles.erase(it);
else
++it;
}
}
return sourceFiles;
}
QStringList getExcludes(const ProFileEvaluator &visitor, const QString &projectDirPath)
{
const QStringList trExcludes = visitor.values(QLatin1String("TR_EXCLUDE"));
QStringList excludes;
excludes.reserve(trExcludes.size());
const QDir projectDir(projectDirPath);
for (const QString &ex : trExcludes)
excludes << QDir::cleanPath(projectDir.absoluteFilePath(ex));
return excludes;
}
static void excludeProjects(const ProFileEvaluator &visitor, QStringList *subProjects)
{
foreach (const QString &ex, visitor.values(QLatin1String("TR_EXCLUDE"))) {
QRegExp rx(ex, Qt::CaseSensitive, QRegExp::Wildcard);
for (QStringList::Iterator it = subProjects->begin(); it != subProjects->end(); ) {
if (rx.exactMatch(*it))
it = subProjects->erase(it);
else
++it;
}
}
}
static QJsonArray processProjects(bool topLevel, const QStringList &proFiles,
const QHash<QString, QString> &outDirMap,
ProFileGlobals *option, QMakeVfs *vfs, QMakeParser *parser,
bool *fail);
static QJsonObject processProject(const QString &proFile, ProFileGlobals *option, QMakeVfs *vfs,
QMakeParser *parser, ProFileEvaluator &visitor)
{
QJsonObject result;
QStringList tmp = visitor.values(QLatin1String("CODECFORSRC"));
if (!tmp.isEmpty())
result[QStringLiteral("codec")] = tmp.last();
QString proPath = QFileInfo(proFile).path();
if (visitor.templateType() == ProFileEvaluator::TT_Subdirs) {
QStringList subProjects = visitor.values(QLatin1String("SUBDIRS"));
excludeProjects(visitor, &subProjects);
QStringList subProFiles;
QDir proDir(proPath);
foreach (const QString &subdir, subProjects) {
QString realdir = visitor.value(subdir + QLatin1String(".subdir"));
if (realdir.isEmpty())
realdir = visitor.value(subdir + QLatin1String(".file"));
if (realdir.isEmpty())
realdir = subdir;
QString subPro = QDir::cleanPath(proDir.absoluteFilePath(realdir));
QFileInfo subInfo(subPro);
if (subInfo.isDir()) {
subProFiles << (subPro + QLatin1Char('/')
+ subInfo.fileName() + QLatin1String(".pro"));
} else {
subProFiles << subPro;
}
}
QJsonArray subResults = processProjects(false, subProFiles,
QHash<QString, QString>(), option, vfs, parser,
nullptr);
if (!subResults.isEmpty())
setValue(result, "subProjects", subResults);
} else {
const QStringList excludes = getExcludes(visitor, proPath);
const QStringList sourceFiles = getSources(visitor, proPath, excludes, vfs);
setValue(result, "includePaths",
visitor.absolutePathValues(QLatin1String("INCLUDEPATH"), proPath));
setValue(result, "excluded", excludes);
setValue(result, "sources", sourceFiles);
}
return result;
}
static QJsonArray processProjects(bool topLevel, const QStringList &proFiles,
const QHash<QString, QString> &outDirMap,
ProFileGlobals *option, QMakeVfs *vfs, QMakeParser *parser, bool *fail)
{
QJsonArray result;
foreach (const QString &proFile, proFiles) {
if (!outDirMap.isEmpty())
option->setDirectories(QFileInfo(proFile).path(), outDirMap[proFile]);
ProFile *pro;
if (!(pro = parser->parsedProFile(proFile, topLevel ? QMakeParser::ParseReportMissing
: QMakeParser::ParseDefault))) {
if (topLevel)
*fail = true;
continue;
}
ProFileEvaluator visitor(option, parser, vfs, &evalHandler);
visitor.setCumulative(true);
visitor.setOutputDir(option->shadowedPath(pro->directoryName()));
if (!visitor.accept(pro)) {
if (topLevel)
*fail = true;
pro->deref();
continue;
}
QJsonObject prj = processProject(proFile, option, vfs, parser, visitor);
setValue(prj, "projectFile", proFile);
if (visitor.contains(QLatin1String("TRANSLATIONS"))) {
QStringList tsFiles;
QDir proDir(QFileInfo(proFile).path());
const QStringList translations = visitor.values(QLatin1String("TRANSLATIONS"));
for (const QString &tsFile : translations)
tsFiles << proDir.filePath(tsFile);
setValue(prj, "translations", tsFiles);
}
result.append(prj);
pro->deref();
}
return result;
}
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
QStringList args = app.arguments();
QStringList proFiles;
QString outDir = QDir::currentPath();
QHash<QString, QString> outDirMap;
QString outputFilePath;
int proDebug = 0;
for (int i = 1; i < args.size(); ++i) {
QString arg = args.at(i);
if (arg == QLatin1String("-help")
|| arg == QLatin1String("--help")
|| arg == QLatin1String("-h")) {
printUsage();
return 0;
} else if (arg == QLatin1String("-out")) {
++i;
if (i == argc) {
printErr(LD::tr("The option -out requires a parameter.\n"));
return 1;
}
outputFilePath = args[i];
} else if (arg == QLatin1String("-silent")) {
evalHandler.verbose = false;
} else if (arg == QLatin1String("-pro-debug")) {
proDebug++;
} else if (arg == QLatin1String("-version")) {
printOut(LD::tr("lprodump version %1\n").arg(QLatin1String(QT_VERSION_STR)));
return 0;
} else if (arg == QLatin1String("-pro")) {
++i;
if (i == argc) {
printErr(LD::tr("The -pro option should be followed by a filename of .pro file.\n"));
return 1;
}
QString file = QDir::cleanPath(QFileInfo(args[i]).absoluteFilePath());
proFiles += file;
outDirMap[file] = outDir;
} else if (arg == QLatin1String("-pro-out")) {
++i;
if (i == argc) {
printErr(LD::tr("The -pro-out option should be followed by a directory name.\n"));
return 1;
}
outDir = QDir::cleanPath(QFileInfo(args[i]).absoluteFilePath());
} else if (arg.startsWith(QLatin1String("-")) && arg != QLatin1String("-")) {
printErr(LD::tr("Unrecognized option '%1'.\n").arg(arg));
return 1;
} else {
QFileInfo fi(arg);
if (!fi.exists()) {
printErr(LD::tr("lprodump error: File '%1' does not exist.\n").arg(arg));
return 1;
}
if (!isProOrPriFile(arg)) {
printErr(LD::tr("lprodump error: '%1' is neither a .pro nor a .pri file.\n")
.arg(arg));
return 1;
}
QString cleanFile = QDir::cleanPath(fi.absoluteFilePath());
proFiles << cleanFile;
outDirMap[cleanFile] = outDir;
}
} // for args
if (proFiles.isEmpty()) {
printUsage();
return 1;
}
bool fail = false;
ProFileGlobals option;
option.qmake_abslocation = QString::fromLocal8Bit(qgetenv("QMAKE"));
if (option.qmake_abslocation.isEmpty())
option.qmake_abslocation = app.applicationDirPath() + QLatin1String("/qmake");
option.debugLevel = proDebug;
option.initProperties();
option.setCommandLineArguments(QDir::currentPath(),
QStringList() << QLatin1String("CONFIG+=lupdate_run"));
QMakeVfs vfs;
QMakeParser parser(0, &vfs, &evalHandler);
QJsonArray results = processProjects(true, proFiles, outDirMap, &option, &vfs,
&parser, &fail);
if (fail)
return 1;
const QByteArray output = QJsonDocument(results).toJson(QJsonDocument::Compact);
if (outputFilePath.isEmpty()) {
puts(output.constData());
} else {
QFile f(outputFilePath);
if (!f.open(QIODevice::WriteOnly)) {
printErr(LD::tr("lprodump error: Cannot open %1 for writing.\n").arg(outputFilePath));
return 1;
}
f.write(output);
f.write("\n");
}
return 0;
}