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 | |
27 | using namespace Qt::StringLiterals; |
28 | |
29 | static void printOut(const QString &out) |
30 | { |
31 | std::cout << qPrintable(out); |
32 | } |
33 | |
34 | static void printErr(const QString &out) |
35 | { |
36 | std::cerr << qPrintable(out); |
37 | } |
38 | |
39 | static QJsonValue toJsonValue(const QJsonValue &v) |
40 | { |
41 | return v; |
42 | } |
43 | |
44 | static QJsonValue toJsonValue(const QString &s) |
45 | { |
46 | return QJsonValue(s); |
47 | } |
48 | |
49 | static QJsonValue toJsonValue(const QStringList &lst) |
50 | { |
51 | return QJsonArray::fromStringList(list: lst); |
52 | } |
53 | |
54 | template <class T> |
55 | void setValue(QJsonObject &obj, const char *key, T value) |
56 | { |
57 | obj[QLatin1String(key)] = toJsonValue(value); |
58 | } |
59 | |
60 | static void printUsage() |
61 | { |
62 | printOut(out: uR"(Usage: |
63 | lprodump [options] project-file... |
64 | lprodump is part of Qt's Linguist tool chain. It extracts information |
65 | from qmake projects to a .json file. This file can be passed to |
66 | lupdate/lrelease using the -project option. |
67 | |
68 | Options: |
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 | |
88 | static 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 | |
98 | class EvalHandler : public QMakeHandler { |
99 | public: |
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 | |
120 | static EvalHandler evalHandler; |
121 | |
122 | static 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 | |
142 | static 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 | |
151 | static 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 | |
219 | QStringList 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 | |
230 | static 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 | |
243 | static 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 | |
249 | static 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 | |
294 | static 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 | |
346 | int 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 | |