| 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 | |