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 
printOut(const QString & out)51 static void printOut(const QString &out)
52 {
53     std::cout << qPrintable(out);
54 }
55 
printErr(const QString & out)56 static void printErr(const QString &out)
57 {
58     std::cerr << qPrintable(out);
59 }
60 
toJsonValue(const QJsonValue & v)61 static QJsonValue toJsonValue(const QJsonValue &v)
62 {
63     return v;
64 }
65 
toJsonValue(const QString & s)66 static QJsonValue toJsonValue(const QString &s)
67 {
68     return QJsonValue(s);
69 }
70 
toJsonValue(const QStringList & lst)71 static QJsonValue toJsonValue(const QStringList &lst)
72 {
73     return QJsonArray::fromStringList(lst);
74 }
75 
76 template <class T>
setValue(QJsonObject & obj,const char * key,T value)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 
printUsage()86 static void printUsage()
87 {
88     printOut(LD::tr(
89         "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 
print(const QString & fileName,int lineNo,const QString & msg)112 static void print(const QString &fileName, int lineNo, const QString &msg)
113 {
114     if (lineNo > 0)
115         printErr(QString::fromLatin1("WARNING: %1:%2: %3\n").arg(fileName, QString::number(lineNo), msg));
116     else if (lineNo)
117         printErr(QString::fromLatin1("WARNING: %1: %2\n").arg(fileName, msg));
118     else
119         printErr(QString::fromLatin1("WARNING: %1\n").arg(msg));
120 }
121 
122 class EvalHandler : public QMakeHandler {
123 public:
message(int type,const QString & msg,const QString & fileName,int lineNo)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 
fileMessage(int type,const QString & msg)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(QLatin1String("WARNING: ") + msg + QLatin1Char('\n'));
135         }
136     }
137 
aboutToEval(ProFile *,ProFile *,EvalFileType)138     virtual void aboutToEval(ProFile *, ProFile *, EvalFileType) {}
doneWithEval(ProFile *)139     virtual void doneWithEval(ProFile *) {}
140 
141     bool verbose = true;
142 };
143 
144 static EvalHandler evalHandler;
145 
isSupportedExtension(const QString & ext)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 
getResources(const QString & resourceFile,QMakeVfs * vfs)153 static QStringList getResources(const QString &resourceFile, QMakeVfs *vfs)
154 {
155     Q_ASSERT(vfs);
156     if (!vfs->exists(resourceFile, QMakeVfs::VfsCumulative))
157         return QStringList();
158     QString content;
159     QString errStr;
160     if (vfs->readFile(vfs->idForFileName(resourceFile, QMakeVfs::VfsCumulative),
161                       &content, &errStr) != QMakeVfs::ReadOk) {
162         printErr(LD::tr("lprodump error: Cannot read %1: %2\n").arg(resourceFile, errStr));
163         return QStringList();
164     }
165     const ReadQrcResult rqr = readQrcFile(resourceFile, content);
166     if (rqr.hasError()) {
167         printErr(LD::tr("lprodump error: %1:%2: %3\n")
168                  .arg(resourceFile, QString::number(rqr.line), rqr.errorString));
169     }
170     return rqr.files;
171 }
172 
getSources(const char * var,const char * vvar,const QStringList & baseVPaths,const QString & projectDir,const ProFileEvaluator & visitor)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(QLatin1String(vvar), projectDir);
177     vPaths += baseVPaths;
178     vPaths.removeDuplicates();
179     return visitor.absoluteFileValues(QLatin1String(var), projectDir, vPaths, 0);
180 }
181 
getSources(const ProFileEvaluator & visitor,const QString & projectDir,const QStringList & excludes,QMakeVfs * vfs)182 static QStringList getSources(const ProFileEvaluator &visitor, const QString &projectDir,
183                               const QStringList &excludes, QMakeVfs *vfs)
184 {
185     QStringList baseVPaths;
186     baseVPaths += visitor.absolutePathValues(QLatin1String("VPATH"), projectDir);
187     baseVPaths << projectDir; // QMAKE_ABSOLUTE_SOURCE_PATH
188     baseVPaths.removeDuplicates();
189 
190     QStringList sourceFiles;
191 
192     // app/lib template
193     sourceFiles += getSources("SOURCES", "VPATH_SOURCES", baseVPaths, projectDir, visitor);
194     sourceFiles += getSources("HEADERS", "VPATH_HEADERS", baseVPaths, projectDir, visitor);
195 
196     sourceFiles += getSources("FORMS", "VPATH_FORMS", baseVPaths, projectDir, visitor);
197 
198     QStringList resourceFiles = getSources("RESOURCES", "VPATH_RESOURCES", baseVPaths, projectDir, visitor);
199     foreach (const QString &resource, resourceFiles)
200         sourceFiles += getResources(resource, vfs);
201 
202     QStringList installs = visitor.values(QLatin1String("INSTALLS"))
203                          + visitor.values(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(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(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(*it))
241                 it = sourceFiles.erase(it);
242             else
243                 ++it;
244         }
245     }
246 
247     return sourceFiles;
248 }
249 
getExcludes(const ProFileEvaluator & visitor,const QString & projectDirPath)250 QStringList getExcludes(const ProFileEvaluator &visitor, const QString &projectDirPath)
251 {
252     const QStringList trExcludes = visitor.values(QLatin1String("TR_EXCLUDE"));
253     QStringList excludes;
254     excludes.reserve(trExcludes.size());
255     const QDir projectDir(projectDirPath);
256     for (const QString &ex : trExcludes)
257         excludes << QDir::cleanPath(projectDir.absoluteFilePath(ex));
258     return excludes;
259 }
260 
excludeProjects(const ProFileEvaluator & visitor,QStringList * subProjects)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(*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 
processProject(const QString & proFile,ProFileGlobals * option,QMakeVfs * vfs,QMakeParser * parser,ProFileEvaluator & visitor)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(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(QLatin1String("SUBDIRS"));
289         excludeProjects(visitor, &subProjects);
290         QStringList subProFiles;
291         QDir proDir(proPath);
292         foreach (const QString &subdir, subProjects) {
293             QString realdir = visitor.value(subdir + QLatin1String(".subdir"));
294             if (realdir.isEmpty())
295                 realdir = visitor.value(subdir + QLatin1String(".file"));
296             if (realdir.isEmpty())
297                 realdir = subdir;
298             QString subPro = QDir::cleanPath(proDir.absoluteFilePath(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(false, subProFiles,
308                                                 QHash<QString, QString>(), option, vfs, parser,
309                                                 nullptr);
310         if (!subResults.isEmpty())
311             setValue(result, "subProjects", subResults);
312     } else {
313         const QStringList excludes = getExcludes(visitor, proPath);
314         const QStringList sourceFiles = getSources(visitor, proPath, excludes, vfs);
315         setValue(result, "includePaths",
316                  visitor.absolutePathValues(QLatin1String("INCLUDEPATH"), proPath));
317         setValue(result, "excluded", excludes);
318         setValue(result, "sources", sourceFiles);
319     }
320     return result;
321 }
322 
processProjects(bool topLevel,const QStringList & proFiles,const QHash<QString,QString> & outDirMap,ProFileGlobals * option,QMakeVfs * vfs,QMakeParser * parser,bool * fail)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(QFileInfo(proFile).path(), outDirMap[proFile]);
331 
332         ProFile *pro;
333         if (!(pro = parser->parsedProFile(proFile, 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(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(prj, "projectFile", proFile);
351         if (visitor.contains(QLatin1String("TRANSLATIONS"))) {
352             QStringList tsFiles;
353             QDir proDir(QFileInfo(proFile).path());
354             const QStringList translations = visitor.values(QLatin1String("TRANSLATIONS"));
355             for (const QString &tsFile : translations)
356                 tsFiles << proDir.filePath(tsFile);
357             setValue(prj, "translations", tsFiles);
358         }
359         result.append(prj);
360         pro->deref();
361     }
362     return result;
363 }
364 
main(int argc,char ** argv)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(LD::tr("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(LD::tr("lprodump version %1\n").arg(QLatin1String(QT_VERSION_STR)));
395             return 0;
396         } else if (arg == QLatin1String("-pro")) {
397             ++i;
398             if (i == argc) {
399                 printErr(LD::tr("The -pro option should be followed by a filename of .pro file.\n"));
400                 return 1;
401             }
402             QString file = QDir::cleanPath(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(LD::tr("The -pro-out option should be followed by a directory name.\n"));
409                 return 1;
410             }
411             outDir = QDir::cleanPath(QFileInfo(args[i]).absoluteFilePath());
412         } else if (arg.startsWith(QLatin1String("-")) && arg != QLatin1String("-")) {
413             printErr(LD::tr("Unrecognized option '%1'.\n").arg(arg));
414             return 1;
415         } else {
416             QFileInfo fi(arg);
417             if (!fi.exists()) {
418                 printErr(LD::tr("lprodump error: File '%1' does not exist.\n").arg(arg));
419                 return 1;
420             }
421             if (!isProOrPriFile(arg)) {
422                 printErr(LD::tr("lprodump error: '%1' is neither a .pro nor a .pri file.\n")
423                          .arg(arg));
424                 return 1;
425             }
426             QString cleanFile = QDir::cleanPath(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(qgetenv("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(QDir::currentPath(),
445                                    QStringList() << QLatin1String("CONFIG+=lupdate_run"));
446     QMakeVfs vfs;
447     QMakeParser parser(0, &vfs, &evalHandler);
448 
449     QJsonArray results = processProjects(true, proFiles, outDirMap, &option, &vfs,
450                                          &parser, &fail);
451     if (fail)
452         return 1;
453 
454     const QByteArray output = QJsonDocument(results).toJson(QJsonDocument::Compact);
455     if (outputFilePath.isEmpty()) {
456         puts(output.constData());
457     } else {
458         QFile f(outputFilePath);
459         if (!f.open(QIODevice::WriteOnly)) {
460             printErr(LD::tr("lprodump error: Cannot open %1 for writing.\n").arg(outputFilePath));
461             return 1;
462         }
463         f.write(output);
464         f.write("\n");
465     }
466     return 0;
467 }
468