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