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