1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include <profileevaluator.h>
5#include <profileutils.h>
6#include <qmakeparser.h>
7#include <qmakevfs.h>
8#include <qrcreader.h>
9
10#include <QtCore/QCoreApplication>
11#include <QtCore/QDebug>
12#include <QtCore/QDir>
13#include <QtCore/QDirIterator>
14#include <QtCore/QFile>
15#include <QtCore/QFileInfo>
16#include <QtCore/QLibraryInfo>
17#include <QtCore/QRegularExpression>
18#include <QtCore/QString>
19#include <QtCore/QStringList>
20
21#include <QtCore/QJsonArray>
22#include <QtCore/QJsonDocument>
23#include <QtCore/QJsonObject>
24
25#include <iostream>
26
27using namespace Qt::StringLiterals;
28
29static void printOut(const QString &out)
30{
31 std::cout << qPrintable(out);
32}
33
34static void printErr(const QString &out)
35{
36 std::cerr << qPrintable(out);
37}
38
39static QJsonValue toJsonValue(const QJsonValue &v)
40{
41 return v;
42}
43
44static QJsonValue toJsonValue(const QString &s)
45{
46 return QJsonValue(s);
47}
48
49static QJsonValue toJsonValue(const QStringList &lst)
50{
51 return QJsonArray::fromStringList(list: lst);
52}
53
54template <class T>
55void setValue(QJsonObject &obj, const char *key, T value)
56{
57 obj[QLatin1String(key)] = toJsonValue(value);
58}
59
60static void printUsage()
61{
62 printOut(out: uR"(Usage:
63 lprodump [options] project-file...
64lprodump is part of Qt's Linguist tool chain. It extracts information
65from qmake projects to a .json file. This file can be passed to
66lupdate/lrelease using the -project option.
67
68Options:
69 -help Display this information and exit.
70 -silent
71 Do not explain what is being done.
72 -pro <filename>
73 Name of a .pro file. Useful for files with .pro file syntax but
74 different file suffix. Projects are recursed into and merged.
75 -pro-out <directory>
76 Virtual output directory for processing subsequent .pro files.
77 -pro-debug
78 Trace processing .pro files. Specify twice for more verbosity.
79 -out <filename>
80 Name of the output file.
81 -translations-variables <variable_1>[,<variable_2>,...]
82 Comma-separated list of QMake variables containing .ts files.
83 -version
84 Display the version of lprodump and exit.
85)"_s);
86}
87
88static void print(const QString &fileName, int lineNo, const QString &msg)
89{
90 if (lineNo > 0)
91 printErr(out: QString::fromLatin1(ba: "WARNING: %1:%2: %3\n").arg(args: fileName, args: QString::number(lineNo), args: msg));
92 else if (lineNo)
93 printErr(out: QString::fromLatin1(ba: "WARNING: %1: %2\n").arg(args: fileName, args: msg));
94 else
95 printErr(out: QString::fromLatin1(ba: "WARNING: %1\n").arg(a: msg));
96}
97
98class EvalHandler : public QMakeHandler {
99public:
100 void message(int type, const QString &msg, const QString &fileName, int lineNo) override
101 {
102 if (verbose && !(type & CumulativeEvalMessage) && (type & CategoryMask) == ErrorMessage)
103 print(fileName, lineNo, msg);
104 }
105
106 void fileMessage(int type, const QString &msg) override
107 {
108 if (verbose && !(type & CumulativeEvalMessage) && (type & CategoryMask) == ErrorMessage) {
109 // "Downgrade" errors, as we don't really care for them
110 printErr(out: QLatin1String("WARNING: ") + msg + QLatin1Char('\n'));
111 }
112 }
113
114 void aboutToEval(ProFile *, ProFile *, EvalFileType) override {}
115 void doneWithEval(ProFile *) override {}
116
117 bool verbose = true;
118};
119
120static EvalHandler evalHandler;
121
122static QStringList getResources(const QString &resourceFile, QMakeVfs *vfs)
123{
124 Q_ASSERT(vfs);
125 if (!vfs->exists(fn: resourceFile, flags: QMakeVfs::VfsCumulative))
126 return QStringList();
127 QString content;
128 QString errStr;
129 if (vfs->readFile(id: vfs->idForFileName(fn: resourceFile, flags: QMakeVfs::VfsCumulative),
130 contents: &content, errStr: &errStr) != QMakeVfs::ReadOk) {
131 printErr(QStringLiteral("lprodump error: Cannot read %1: %2\n").arg(args: resourceFile, args&: errStr));
132 return QStringList();
133 }
134 const ReadQrcResult rqr = readQrcFile(resourceFile, content);
135 if (rqr.hasError()) {
136 printErr(QStringLiteral("lprodump error: %1:%2: %3\n")
137 .arg(args: resourceFile, args: QString::number(rqr.line), args: rqr.errorString));
138 }
139 return rqr.files;
140}
141
142static QStringList getSources(const char *var, const char *vvar, const QStringList &baseVPaths,
143 const QString &projectDir, const ProFileEvaluator &visitor)
144{
145 QStringList vPaths = visitor.absolutePathValues(variable: QLatin1String(vvar), baseDirectory: projectDir);
146 vPaths += baseVPaths;
147 vPaths.removeDuplicates();
148 return visitor.absoluteFileValues(variable: QLatin1String(var), baseDirectory: projectDir, searchDirs: vPaths, pro: 0);
149}
150
151static QStringList getSources(const ProFileEvaluator &visitor, const QString &projectDir,
152 const QStringList &excludes, QMakeVfs *vfs)
153{
154 QStringList baseVPaths;
155 baseVPaths += visitor.absolutePathValues(variable: QLatin1String("VPATH"), baseDirectory: projectDir);
156 baseVPaths << projectDir; // QMAKE_ABSOLUTE_SOURCE_PATH
157 baseVPaths.removeDuplicates();
158
159 QStringList sourceFiles;
160
161 // app/lib template
162 sourceFiles += getSources(var: "SOURCES", vvar: "VPATH_SOURCES", baseVPaths, projectDir, visitor);
163 sourceFiles += getSources(var: "HEADERS", vvar: "VPATH_HEADERS", baseVPaths, projectDir, visitor);
164
165 sourceFiles += getSources(var: "FORMS", vvar: "VPATH_FORMS", baseVPaths, projectDir, visitor);
166
167 const QStringList resourceFiles = getSources(var: "RESOURCES", vvar: "VPATH_RESOURCES", baseVPaths, projectDir, visitor);
168 for (const QString &resource : resourceFiles)
169 sourceFiles += getResources(resourceFile: resource, vfs);
170
171 QStringList installs = visitor.values(variableName: QLatin1String("INSTALLS"))
172 + visitor.values(variableName: QLatin1String("DEPLOYMENT"));
173 installs.removeDuplicates();
174 QDir baseDir(projectDir);
175 for (const QString &inst : std::as_const(t&: installs)) {
176 for (const QString &file : visitor.values(variableName: inst + QLatin1String(".files"))) {
177 QFileInfo info(file);
178 if (!info.isAbsolute())
179 info.setFile(baseDir.absoluteFilePath(fileName: file));
180 QStringList nameFilter;
181 QString searchPath;
182 if (info.isDir()) {
183 nameFilter << QLatin1String("*");
184 searchPath = info.filePath();
185 } else {
186 nameFilter << info.fileName();
187 searchPath = info.path();
188 }
189
190 QDirIterator iterator(searchPath, nameFilter,
191 QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks,
192 QDirIterator::Subdirectories);
193 while (iterator.hasNext()) {
194 iterator.next();
195 QFileInfo cfi = iterator.fileInfo();
196 if (isSupportedExtension(ext: cfi.suffix()))
197 sourceFiles << cfi.filePath();
198 }
199 }
200 }
201
202 sourceFiles.removeDuplicates();
203 sourceFiles.sort();
204
205 for (const QString &ex : excludes) {
206 // TODO: take advantage of the file list being sorted
207 QRegularExpression rx(QRegularExpression::wildcardToRegularExpression(str: ex));
208 for (auto it = sourceFiles.begin(); it != sourceFiles.end(); ) {
209 if (rx.match(subject: *it).hasMatch())
210 it = sourceFiles.erase(pos: it);
211 else
212 ++it;
213 }
214 }
215
216 return sourceFiles;
217}
218
219QStringList getExcludes(const ProFileEvaluator &visitor, const QString &projectDirPath)
220{
221 const QStringList trExcludes = visitor.values(variableName: QLatin1String("TR_EXCLUDE"));
222 QStringList excludes;
223 excludes.reserve(asize: trExcludes.size());
224 const QDir projectDir(projectDirPath);
225 for (const QString &ex : trExcludes)
226 excludes << QDir::cleanPath(path: projectDir.absoluteFilePath(fileName: ex));
227 return excludes;
228}
229
230static void excludeProjects(const ProFileEvaluator &visitor, QStringList *subProjects)
231{
232 for (const QString &ex : visitor.values(variableName: QLatin1String("TR_EXCLUDE"))) {
233 QRegularExpression rx(QRegularExpression::wildcardToRegularExpression(str: ex));
234 for (auto it = subProjects->begin(); it != subProjects->end(); ) {
235 if (rx.match(subject: *it).hasMatch())
236 it = subProjects->erase(pos: it);
237 else
238 ++it;
239 }
240 }
241}
242
243static QJsonArray processProjects(bool topLevel, const QStringList &proFiles,
244 const QStringList &translationsVariables,
245 const QHash<QString, QString> &outDirMap,
246 ProFileGlobals *option, QMakeVfs *vfs, QMakeParser *parser,
247 bool *fail);
248
249static QJsonObject processProject(const QString &proFile, const QStringList &translationsVariables,
250 ProFileGlobals *option, QMakeVfs *vfs,
251 QMakeParser *parser, ProFileEvaluator &visitor)
252{
253 QJsonObject result;
254 QStringList tmp = visitor.values(variableName: QLatin1String("CODECFORSRC"));
255 if (!tmp.isEmpty())
256 result[QStringLiteral("codec")] = tmp.last();
257 QString proPath = QFileInfo(proFile).path();
258 if (visitor.templateType() == ProFileEvaluator::TT_Subdirs) {
259 QStringList subProjects = visitor.values(variableName: QLatin1String("SUBDIRS"));
260 excludeProjects(visitor, subProjects: &subProjects);
261 QStringList subProFiles;
262 QDir proDir(proPath);
263 for (const QString &subdir : std::as_const(t&: subProjects)) {
264 QString realdir = visitor.value(variableName: subdir + QLatin1String(".subdir"));
265 if (realdir.isEmpty())
266 realdir = visitor.value(variableName: subdir + QLatin1String(".file"));
267 if (realdir.isEmpty())
268 realdir = subdir;
269 QString subPro = QDir::cleanPath(path: proDir.absoluteFilePath(fileName: realdir));
270 QFileInfo subInfo(subPro);
271 if (subInfo.isDir()) {
272 subProFiles << (subPro + QLatin1Char('/')
273 + subInfo.fileName() + QLatin1String(".pro"));
274 } else {
275 subProFiles << subPro;
276 }
277 }
278 QJsonArray subResults = processProjects(topLevel: false, proFiles: subProFiles, translationsVariables,
279 outDirMap: QHash<QString, QString>(), option, vfs, parser,
280 fail: nullptr);
281 if (!subResults.isEmpty())
282 setValue(obj&: result, key: "subProjects", value: subResults);
283 } else {
284 const QStringList excludes = getExcludes(visitor, projectDirPath: proPath);
285 const QStringList sourceFiles = getSources(visitor, projectDir: proPath, excludes, vfs);
286 setValue(obj&: result, key: "includePaths",
287 value: visitor.absolutePathValues(variable: QLatin1String("INCLUDEPATH"), baseDirectory: proPath));
288 setValue(obj&: result, key: "excluded", value: excludes);
289 setValue(obj&: result, key: "sources", value: sourceFiles);
290 }
291 return result;
292}
293
294static QJsonArray processProjects(bool topLevel, const QStringList &proFiles,
295 const QStringList &translationsVariables,
296 const QHash<QString, QString> &outDirMap,
297 ProFileGlobals *option, QMakeVfs *vfs, QMakeParser *parser, bool *fail)
298{
299 QJsonArray result;
300 for (const QString &proFile : proFiles) {
301 if (!outDirMap.isEmpty())
302 option->setDirectories(input_dir: QFileInfo(proFile).path(), output_dir: outDirMap[proFile]);
303
304 ProFile *pro;
305 if (!(pro = parser->parsedProFile(fileName: proFile, flags: topLevel ? QMakeParser::ParseReportMissing
306 : QMakeParser::ParseDefault))) {
307 if (topLevel)
308 *fail = true;
309 continue;
310 }
311 ProFileEvaluator visitor(option, parser, vfs, &evalHandler);
312 visitor.setCumulative(true);
313 visitor.setOutputDir(option->shadowedPath(fileName: pro->directoryName()));
314 if (!visitor.accept(pro)) {
315 if (topLevel)
316 *fail = true;
317 pro->deref();
318 continue;
319 }
320
321 QJsonObject prj = processProject(proFile, translationsVariables, option, vfs, parser,
322 visitor);
323 setValue(obj&: prj, key: "projectFile", value: proFile);
324 QStringList tsFiles;
325 for (const QString &varName : translationsVariables) {
326 if (!visitor.contains(variableName: varName))
327 continue;
328 QDir proDir(QFileInfo(proFile).path());
329 const QStringList translations = visitor.values(variableName: varName);
330 for (const QString &tsFile : translations)
331 tsFiles << proDir.filePath(fileName: tsFile);
332 }
333 if (!tsFiles.isEmpty())
334 setValue(obj&: prj, key: "translations", value: tsFiles);
335 if (visitor.contains(variableName: QLatin1String("LUPDATE_COMPILE_COMMANDS_PATH"))) {
336 const QStringList thepathjson = visitor.values(
337 variableName: QLatin1String("LUPDATE_COMPILE_COMMANDS_PATH"));
338 setValue(obj&: prj, key: "compileCommands", value: thepathjson.value(i: 0));
339 }
340 result.append(value: prj);
341 pro->deref();
342 }
343 return result;
344}
345
346int main(int argc, char **argv)
347{
348 QCoreApplication app(argc, argv);
349 QStringList args = app.arguments();
350 QStringList proFiles;
351 QStringList translationsVariables = { u"TRANSLATIONS"_s };
352 QString outDir = QDir::currentPath();
353 QHash<QString, QString> outDirMap;
354 QString outputFilePath;
355 int proDebug = 0;
356
357 for (int i = 1; i < args.size(); ++i) {
358 QString arg = args.at(i);
359 if (arg == QLatin1String("-help")
360 || arg == QLatin1String("--help")
361 || arg == QLatin1String("-h")) {
362 printUsage();
363 return 0;
364 } else if (arg == QLatin1String("-out")) {
365 ++i;
366 if (i == argc) {
367 printErr(out: u"The option -out requires a parameter.\n"_s);
368 return 1;
369 }
370 outputFilePath = args[i];
371 } else if (arg == QLatin1String("-silent")) {
372 evalHandler.verbose = false;
373 } else if (arg == QLatin1String("-pro-debug")) {
374 proDebug++;
375 } else if (arg == QLatin1String("-version")) {
376 printOut(QStringLiteral("lprodump version %1\n").arg(a: QLatin1String(QT_VERSION_STR)));
377 return 0;
378 } else if (arg == QLatin1String("-pro")) {
379 ++i;
380 if (i == argc) {
381 printErr(QStringLiteral("The -pro option should be followed by a filename of .pro file.\n"));
382 return 1;
383 }
384 QString file = QDir::cleanPath(path: QFileInfo(args[i]).absoluteFilePath());
385 proFiles += file;
386 outDirMap[file] = outDir;
387 } else if (arg == QLatin1String("-pro-out")) {
388 ++i;
389 if (i == argc) {
390 printErr(QStringLiteral("The -pro-out option should be followed by a directory name.\n"));
391 return 1;
392 }
393 outDir = QDir::cleanPath(path: QFileInfo(args[i]).absoluteFilePath());
394 } else if (arg == u"-translations-variables"_s) {
395 ++i;
396 if (i == argc) {
397 printErr(out: u"The -translations-variables option must be followed by a "_s
398 u"comma-separated list of variable names.\n"_s);
399 return 1;
400 }
401 translationsVariables = args.at(i).split(sep: QLatin1Char(','));
402 } else if (arg.startsWith(s: QLatin1String("-")) && arg != QLatin1String("-")) {
403 printErr(QStringLiteral("Unrecognized option '%1'.\n").arg(a: arg));
404 return 1;
405 } else {
406 QFileInfo fi(arg);
407 if (!fi.exists()) {
408 printErr(QStringLiteral("lprodump error: File '%1' does not exist.\n").arg(a: arg));
409 return 1;
410 }
411 if (!isProOrPriFile(filePath: arg)) {
412 printErr(QStringLiteral("lprodump error: '%1' is neither a .pro nor a .pri file.\n")
413 .arg(a: arg));
414 return 1;
415 }
416 QString cleanFile = QDir::cleanPath(path: fi.absoluteFilePath());
417 proFiles << cleanFile;
418 outDirMap[cleanFile] = outDir;
419 }
420 } // for args
421
422 if (proFiles.isEmpty()) {
423 printUsage();
424 return 1;
425 }
426
427 bool fail = false;
428 ProFileGlobals option;
429 option.qmake_abslocation = QString::fromLocal8Bit(ba: qgetenv(varName: "QMAKE"));
430 if (option.qmake_abslocation.isEmpty()) {
431 option.qmake_abslocation = QLibraryInfo::path(p: QLibraryInfo::BinariesPath)
432 + QLatin1String("/qmake");
433 }
434 option.debugLevel = proDebug;
435 option.initProperties();
436 option.setCommandLineArguments(pwd: QDir::currentPath(),
437 args: QStringList() << QLatin1String("CONFIG+=lupdate_run"));
438 QMakeVfs vfs;
439 QMakeParser parser(0, &vfs, &evalHandler);
440
441 QJsonArray results = processProjects(topLevel: true, proFiles, translationsVariables, outDirMap, option: &option,
442 vfs: &vfs, parser: &parser, fail: &fail);
443 if (fail)
444 return 1;
445
446 const QByteArray output = QJsonDocument(results).toJson(format: QJsonDocument::Compact);
447 if (outputFilePath.isEmpty()) {
448 puts(s: output.constData());
449 } else {
450 QFile f(outputFilePath);
451 if (!f.open(flags: QIODevice::WriteOnly)) {
452 printErr(QStringLiteral("lprodump error: Cannot open %1 for writing.\n").arg(a: outputFilePath));
453 return 1;
454 }
455 f.write(data: output);
456 f.write(data: "\n");
457 }
458 return 0;
459}
460

source code of qttools/src/linguist/lprodump/main.cpp