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 | 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 | return sourceFiles; |
205 | } |
206 | |
207 | QStringList getExcludes(const ProFileEvaluator &visitor, const QString &projectDirPath) |
208 | { |
209 | const QStringList trExcludes = visitor.values(variableName: QLatin1String("TR_EXCLUDE" )); |
210 | QStringList excludes; |
211 | excludes.reserve(asize: trExcludes.size()); |
212 | const QDir projectDir(projectDirPath); |
213 | for (const QString &ex : trExcludes) |
214 | excludes << QDir::cleanPath(path: projectDir.absoluteFilePath(fileName: ex)); |
215 | return excludes; |
216 | } |
217 | |
218 | static void excludeProjects(const ProFileEvaluator &visitor, QStringList *subProjects) |
219 | { |
220 | for (const QString &ex : visitor.values(variableName: QLatin1String("TR_EXCLUDE" ))) { |
221 | QRegularExpression rx(QRegularExpression::wildcardToRegularExpression(str: ex)); |
222 | for (auto it = subProjects->begin(); it != subProjects->end(); ) { |
223 | if (rx.match(subject: *it).hasMatch()) |
224 | it = subProjects->erase(pos: it); |
225 | else |
226 | ++it; |
227 | } |
228 | } |
229 | } |
230 | |
231 | static QJsonArray processProjects(bool topLevel, const QStringList &proFiles, |
232 | const QStringList &translationsVariables, |
233 | const QHash<QString, QString> &outDirMap, |
234 | ProFileGlobals *option, QMakeVfs *vfs, QMakeParser *parser, |
235 | bool *fail); |
236 | |
237 | static QJsonObject processProject(const QString &proFile, const QStringList &translationsVariables, |
238 | ProFileGlobals *option, QMakeVfs *vfs, |
239 | QMakeParser *parser, ProFileEvaluator &visitor) |
240 | { |
241 | QJsonObject result; |
242 | QStringList tmp = visitor.values(variableName: QLatin1String("CODECFORSRC" )); |
243 | if (!tmp.isEmpty()) |
244 | result[QStringLiteral("codec" )] = tmp.last(); |
245 | QString proPath = QFileInfo(proFile).path(); |
246 | if (visitor.templateType() == ProFileEvaluator::TT_Subdirs) { |
247 | QStringList subProjects = visitor.values(variableName: QLatin1String("SUBDIRS" )); |
248 | excludeProjects(visitor, subProjects: &subProjects); |
249 | QStringList subProFiles; |
250 | QDir proDir(proPath); |
251 | for (const QString &subdir : std::as_const(t&: subProjects)) { |
252 | QString realdir = visitor.value(variableName: subdir + QLatin1String(".subdir" )); |
253 | if (realdir.isEmpty()) |
254 | realdir = visitor.value(variableName: subdir + QLatin1String(".file" )); |
255 | if (realdir.isEmpty()) |
256 | realdir = subdir; |
257 | QString subPro = QDir::cleanPath(path: proDir.absoluteFilePath(fileName: realdir)); |
258 | QFileInfo subInfo(subPro); |
259 | if (subInfo.isDir()) { |
260 | subProFiles << (subPro + QLatin1Char('/') |
261 | + subInfo.fileName() + QLatin1String(".pro" )); |
262 | } else { |
263 | subProFiles << subPro; |
264 | } |
265 | } |
266 | QJsonArray subResults = processProjects(topLevel: false, proFiles: subProFiles, translationsVariables, |
267 | outDirMap: QHash<QString, QString>(), option, vfs, parser, |
268 | fail: nullptr); |
269 | if (!subResults.isEmpty()) |
270 | setValue(obj&: result, key: "subProjects" , value: subResults); |
271 | } else { |
272 | const QStringList sourceFiles = getSources(visitor, projectDir: proPath, vfs); |
273 | setValue(obj&: result, key: "includePaths" , |
274 | value: visitor.absolutePathValues(variable: QLatin1String("INCLUDEPATH" ), baseDirectory: proPath)); |
275 | setValue(obj&: result, key: "excluded" , value: getExcludes(visitor, projectDirPath: proPath)); |
276 | setValue(obj&: result, key: "sources" , value: sourceFiles); |
277 | } |
278 | return result; |
279 | } |
280 | |
281 | static QJsonArray processProjects(bool topLevel, const QStringList &proFiles, |
282 | const QStringList &translationsVariables, |
283 | const QHash<QString, QString> &outDirMap, |
284 | ProFileGlobals *option, QMakeVfs *vfs, QMakeParser *parser, bool *fail) |
285 | { |
286 | QJsonArray result; |
287 | for (const QString &proFile : proFiles) { |
288 | if (!outDirMap.isEmpty()) |
289 | option->setDirectories(input_dir: QFileInfo(proFile).path(), output_dir: outDirMap[proFile]); |
290 | |
291 | ProFile *pro; |
292 | if (!(pro = parser->parsedProFile(fileName: proFile, flags: topLevel ? QMakeParser::ParseReportMissing |
293 | : QMakeParser::ParseDefault))) { |
294 | if (topLevel) |
295 | *fail = true; |
296 | continue; |
297 | } |
298 | ProFileEvaluator visitor(option, parser, vfs, &evalHandler); |
299 | visitor.setCumulative(true); |
300 | visitor.setOutputDir(option->shadowedPath(fileName: pro->directoryName())); |
301 | if (!visitor.accept(pro)) { |
302 | if (topLevel) |
303 | *fail = true; |
304 | pro->deref(); |
305 | continue; |
306 | } |
307 | |
308 | QJsonObject prj = processProject(proFile, translationsVariables, option, vfs, parser, |
309 | visitor); |
310 | setValue(obj&: prj, key: "projectFile" , value: proFile); |
311 | QStringList tsFiles; |
312 | for (const QString &varName : translationsVariables) { |
313 | if (!visitor.contains(variableName: varName)) |
314 | continue; |
315 | QDir proDir(QFileInfo(proFile).path()); |
316 | const QStringList translations = visitor.values(variableName: varName); |
317 | for (const QString &tsFile : translations) |
318 | tsFiles << proDir.filePath(fileName: tsFile); |
319 | } |
320 | if (!tsFiles.isEmpty()) |
321 | setValue(obj&: prj, key: "translations" , value: tsFiles); |
322 | if (visitor.contains(variableName: QLatin1String("LUPDATE_COMPILE_COMMANDS_PATH" ))) { |
323 | const QStringList thepathjson = visitor.values( |
324 | variableName: QLatin1String("LUPDATE_COMPILE_COMMANDS_PATH" )); |
325 | setValue(obj&: prj, key: "compileCommands" , value: thepathjson.value(i: 0)); |
326 | } |
327 | result.append(value: prj); |
328 | pro->deref(); |
329 | } |
330 | return result; |
331 | } |
332 | |
333 | int main(int argc, char **argv) |
334 | { |
335 | QCoreApplication app(argc, argv); |
336 | QStringList args = app.arguments(); |
337 | QStringList proFiles; |
338 | QStringList translationsVariables = { u"TRANSLATIONS"_s }; |
339 | QString outDir = QDir::currentPath(); |
340 | QHash<QString, QString> outDirMap; |
341 | QString outputFilePath; |
342 | int proDebug = 0; |
343 | |
344 | for (int i = 1; i < args.size(); ++i) { |
345 | QString arg = args.at(i); |
346 | if (arg == QLatin1String("-help" ) |
347 | || arg == QLatin1String("--help" ) |
348 | || arg == QLatin1String("-h" )) { |
349 | printUsage(); |
350 | return 0; |
351 | } else if (arg == QLatin1String("-out" )) { |
352 | ++i; |
353 | if (i == argc) { |
354 | printErr(out: u"The option -out requires a parameter.\n"_s ); |
355 | return 1; |
356 | } |
357 | outputFilePath = args[i]; |
358 | } else if (arg == QLatin1String("-silent" )) { |
359 | evalHandler.verbose = false; |
360 | } else if (arg == QLatin1String("-pro-debug" )) { |
361 | proDebug++; |
362 | } else if (arg == QLatin1String("-version" )) { |
363 | printOut(QStringLiteral("lprodump version %1\n" ).arg(a: QLatin1String(QT_VERSION_STR))); |
364 | return 0; |
365 | } else if (arg == QLatin1String("-pro" )) { |
366 | ++i; |
367 | if (i == argc) { |
368 | printErr(QStringLiteral("The -pro option should be followed by a filename of .pro file.\n" )); |
369 | return 1; |
370 | } |
371 | QString file = QDir::cleanPath(path: QFileInfo(args[i]).absoluteFilePath()); |
372 | proFiles += file; |
373 | outDirMap[file] = outDir; |
374 | } else if (arg == QLatin1String("-pro-out" )) { |
375 | ++i; |
376 | if (i == argc) { |
377 | printErr(QStringLiteral("The -pro-out option should be followed by a directory name.\n" )); |
378 | return 1; |
379 | } |
380 | outDir = QDir::cleanPath(path: QFileInfo(args[i]).absoluteFilePath()); |
381 | } else if (arg == u"-translations-variables"_s ) { |
382 | ++i; |
383 | if (i == argc) { |
384 | printErr(out: u"The -translations-variables option must be followed by a "_s |
385 | u"comma-separated list of variable names.\n"_s ); |
386 | return 1; |
387 | } |
388 | translationsVariables = args.at(i).split(sep: QLatin1Char(',')); |
389 | } else if (arg.startsWith(s: QLatin1String("-" )) && arg != QLatin1String("-" )) { |
390 | printErr(QStringLiteral("Unrecognized option '%1'.\n" ).arg(a: arg)); |
391 | return 1; |
392 | } else { |
393 | QFileInfo fi(arg); |
394 | if (!fi.exists()) { |
395 | printErr(QStringLiteral("lprodump error: File '%1' does not exist.\n" ).arg(a: arg)); |
396 | return 1; |
397 | } |
398 | if (!isProOrPriFile(filePath: arg)) { |
399 | printErr(QStringLiteral("lprodump error: '%1' is neither a .pro nor a .pri file.\n" ) |
400 | .arg(a: arg)); |
401 | return 1; |
402 | } |
403 | QString cleanFile = QDir::cleanPath(path: fi.absoluteFilePath()); |
404 | proFiles << cleanFile; |
405 | outDirMap[cleanFile] = outDir; |
406 | } |
407 | } // for args |
408 | |
409 | if (proFiles.isEmpty()) { |
410 | printUsage(); |
411 | return 1; |
412 | } |
413 | |
414 | bool fail = false; |
415 | ProFileGlobals option; |
416 | option.qmake_abslocation = QString::fromLocal8Bit(ba: qgetenv(varName: "QMAKE" )); |
417 | if (option.qmake_abslocation.isEmpty()) { |
418 | option.qmake_abslocation = QLibraryInfo::path(p: QLibraryInfo::BinariesPath) |
419 | + QLatin1String("/qmake" ); |
420 | } |
421 | option.debugLevel = proDebug; |
422 | option.initProperties(); |
423 | option.setCommandLineArguments(pwd: QDir::currentPath(), |
424 | args: QStringList() << QLatin1String("CONFIG+=lupdate_run" )); |
425 | QMakeVfs vfs; |
426 | QMakeParser parser(0, &vfs, &evalHandler); |
427 | |
428 | QJsonArray results = processProjects(topLevel: true, proFiles, translationsVariables, outDirMap, option: &option, |
429 | vfs: &vfs, parser: &parser, fail: &fail); |
430 | if (fail) |
431 | return 1; |
432 | |
433 | const QByteArray output = QJsonDocument(results).toJson(format: QJsonDocument::Compact); |
434 | if (outputFilePath.isEmpty()) { |
435 | puts(s: output.constData()); |
436 | } else { |
437 | QFile f(outputFilePath); |
438 | if (!f.open(flags: QIODevice::WriteOnly)) { |
439 | printErr(QStringLiteral("lprodump error: Cannot open %1 for writing.\n" ).arg(a: outputFilePath)); |
440 | return 1; |
441 | } |
442 | f.write(data: output); |
443 | f.write(data: "\n" ); |
444 | } |
445 | return 0; |
446 | } |
447 | |